Merge branch 'develop' into feature/OP-7176_Use-folder-path-as-unique-identifier

This commit is contained in:
Jakub Trllo 2023-11-16 11:23:58 +01:00
commit 6e8f142e52
30 changed files with 535 additions and 1200 deletions

View file

@ -28,7 +28,7 @@ from .lib import imprint
VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx"]
def asset_name(
def prepare_scene_name(
asset: str, subset: str, namespace: Optional[str] = None
) -> str:
"""Return a consistent name for an asset."""
@ -225,7 +225,7 @@ class BaseCreator(Creator):
bpy.context.scene.collection.children.link(instances)
# Create asset group
name = asset_name(instance_data["asset"], subset_name)
name = prepare_scene_name(instance_data["asset"], subset_name)
if self.create_as_asset_group:
# Create instance as empty
instance_node = bpy.data.objects.new(name=name, object_data=None)
@ -298,7 +298,9 @@ class BaseCreator(Creator):
"subset" in changes.changed_keys
or "asset" in changes.changed_keys
):
name = asset_name(asset=data["asset"], subset=data["subset"])
name = prepare_scene_name(
asset=data["asset"], subset=data["subset"]
)
node.name = name
imprint(node, data)
@ -454,7 +456,7 @@ class AssetLoader(LoaderPlugin):
asset, subset
)
namespace = namespace or f"{asset}_{unique_number}"
name = name or asset_name(
name = name or prepare_scene_name(
asset, subset, unique_number
)
@ -483,7 +485,9 @@ class AssetLoader(LoaderPlugin):
# asset = context["asset"]["name"]
# subset = context["subset"]["name"]
# instance_name = asset_name(asset, subset, unique_number) + '_CON'
# instance_name = prepare_scene_name(
# asset, subset, unique_number
# ) + '_CON'
# return self._get_instance_collection(instance_name, nodes)

View file

@ -22,7 +22,7 @@ class CreateAction(plugin.BaseCreator):
)
# Get instance name
name = plugin.asset_name(instance_data["asset"], subset_name)
name = plugin.prepare_scene_name(instance_data["asset"], subset_name)
if pre_create_data.get("use_selection"):
for obj in lib.get_selection():

View file

@ -7,7 +7,7 @@ def append_workfile(context, fname, do_import):
asset = context['asset']['name']
subset = context['subset']['name']
group_name = plugin.asset_name(asset, subset)
group_name = plugin.prepare_scene_name(asset, subset)
# We need to preserve the original names of the scenes, otherwise,
# if there are duplicate names in the current workfile, the imported

View file

@ -137,9 +137,9 @@ class CacheModelLoader(plugin.AssetLoader):
asset = context["asset"]["name"]
subset = context["subset"]["name"]
asset_name = plugin.asset_name(asset, subset)
asset_name = plugin.prepare_scene_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.asset_name(asset, subset, unique_number)
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
containers = bpy.data.collections.get(AVALON_CONTAINERS)

View file

@ -7,7 +7,7 @@ from typing import Dict, List, Optional
import bpy
from openpype.pipeline import get_representation_path
import openpype.hosts.blender.api.plugin
from openpype.hosts.blender.api import plugin
from openpype.hosts.blender.api.pipeline import (
containerise_existing,
AVALON_PROPERTY,
@ -16,7 +16,7 @@ from openpype.hosts.blender.api.pipeline import (
logger = logging.getLogger("openpype").getChild("blender").getChild("load_action")
class BlendActionLoader(openpype.hosts.blender.api.plugin.AssetLoader):
class BlendActionLoader(plugin.AssetLoader):
"""Load action from a .blend file.
Warning:
@ -46,8 +46,8 @@ class BlendActionLoader(openpype.hosts.blender.api.plugin.AssetLoader):
libpath = self.filepath_from_context(context)
asset = context["asset"]["name"]
subset = context["subset"]["name"]
lib_container = openpype.hosts.blender.api.plugin.asset_name(asset, subset)
container_name = openpype.hosts.blender.api.plugin.asset_name(
lib_container = plugin.prepare_scene_name(asset, subset)
container_name = plugin.prepare_scene_name(
asset, subset, namespace
)
@ -152,7 +152,7 @@ class BlendActionLoader(openpype.hosts.blender.api.plugin.AssetLoader):
assert libpath.is_file(), (
f"The file doesn't exist: {libpath}"
)
assert extension in openpype.hosts.blender.api.plugin.VALID_EXTENSIONS, (
assert extension in plugin.VALID_EXTENSIONS, (
f"Unsupported file: {libpath}"
)

View file

@ -42,9 +42,9 @@ class AudioLoader(plugin.AssetLoader):
asset = context["asset"]["name"]
subset = context["subset"]["name"]
asset_name = plugin.asset_name(asset, subset)
asset_name = plugin.prepare_scene_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.asset_name(asset, subset, unique_number)
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)

View file

@ -133,9 +133,9 @@ class BlendLoader(plugin.AssetLoader):
representation = str(context["representation"]["_id"])
asset_name = plugin.asset_name(asset, subset)
asset_name = plugin.prepare_scene_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.asset_name(asset, subset, unique_number)
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)

View file

@ -85,9 +85,9 @@ class BlendSceneLoader(plugin.AssetLoader):
except ValueError:
family = "model"
asset_name = plugin.asset_name(asset, subset)
asset_name = plugin.prepare_scene_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.asset_name(asset, subset, unique_number)
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)

View file

@ -87,9 +87,9 @@ class AbcCameraLoader(plugin.AssetLoader):
asset = context["asset"]["name"]
subset = context["subset"]["name"]
asset_name = plugin.asset_name(asset, subset)
asset_name = plugin.prepare_scene_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.asset_name(asset, subset, unique_number)
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)

View file

@ -90,9 +90,9 @@ class FbxCameraLoader(plugin.AssetLoader):
asset = context["asset"]["name"]
subset = context["subset"]["name"]
asset_name = plugin.asset_name(asset, subset)
asset_name = plugin.prepare_scene_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.asset_name(asset, subset, unique_number)
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)

View file

@ -134,9 +134,9 @@ class FbxModelLoader(plugin.AssetLoader):
asset = context["asset"]["name"]
subset = context["subset"]["name"]
asset_name = plugin.asset_name(asset, subset)
asset_name = plugin.prepare_scene_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.asset_name(asset, subset, unique_number)
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)

View file

@ -149,9 +149,9 @@ class JsonLayoutLoader(plugin.AssetLoader):
asset = context["asset"]["name"]
subset = context["subset"]["name"]
asset_name = plugin.asset_name(asset, subset)
asset_name = plugin.prepare_scene_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.asset_name(asset, subset, unique_number)
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)

View file

@ -96,14 +96,14 @@ class BlendLookLoader(plugin.AssetLoader):
asset = context["asset"]["name"]
subset = context["subset"]["name"]
lib_container = plugin.asset_name(
lib_container = plugin.prepare_scene_name(
asset, subset
)
unique_number = plugin.get_unique_number(
asset, subset
)
namespace = namespace or f"{asset}_{unique_number}"
container_name = plugin.asset_name(
container_name = plugin.prepare_scene_name(
asset, subset, unique_number
)

View file

@ -13,6 +13,7 @@ from .hierarchy import (
HIERARCHY_MODEL_SENDER,
)
from .thumbnails import ThumbnailsModel
from .selection import HierarchyExpectedSelection
__all__ = (
@ -29,4 +30,6 @@ __all__ = (
"HIERARCHY_MODEL_SENDER",
"ThumbnailsModel",
"HierarchyExpectedSelection",
)

View file

@ -81,11 +81,11 @@ class NestedCacheItem:
"""Helper for cached items stored in nested structure.
Example:
>>> cache = NestedCacheItem(levels=2)
>>> cache = NestedCacheItem(levels=2, default_factory=lambda: 0)
>>> cache["a"]["b"].is_valid
False
>>> cache["a"]["b"].get_data()
None
0
>>> cache["a"]["b"] = 1
>>> cache["a"]["b"].is_valid
True
@ -167,8 +167,51 @@ class NestedCacheItem:
return self[key]
def cached_count(self):
"""Amount of cached items.
Returns:
int: Amount of cached items.
"""
return len(self._data_by_key)
def clear_key(self, key):
"""Clear cached item by key.
Args:
key (str): Key of the cache item.
"""
self._data_by_key.pop(key, None)
def clear_invalid(self):
"""Clear all invalid cache items.
Note:
To clear all cache items use 'reset'.
"""
changed = {}
children_are_nested = self._levels > 1
for key, cache in tuple(self._data_by_key.items()):
if children_are_nested:
output = cache.clear_invalid()
if output:
changed[key] = output
if not cache.cached_count():
self._data_by_key.pop(key)
elif not cache.is_valid:
changed[key] = cache.get_data()
self._data_by_key.pop(key)
return changed
def reset(self):
"""Reset cache."""
"""Reset cache.
Note:
To clear only invalid cache items use 'clear_invalid'.
"""
self._data_by_key = {}

View file

@ -0,0 +1,179 @@
class _ExampleController:
def emit_event(self, topic, data, **kwargs):
pass
class HierarchyExpectedSelection:
"""Base skeleton of expected selection model.
Expected selection model holds information about which entities should be
selected. The order of selection is very important as change of project
will affect what folders are available in folders UI and so on. Because
of that should expected selection model know what is current entity
to select.
If any of 'handle_project', 'handle_folder' or 'handle_task' is set to
'False' expected selection data won't contain information about the
entity type at all. Also if project is not handled then it is not
necessary to call 'expected_project_selected'. Same goes for folder and
task.
Model is triggering event with 'expected_selection_changed' topic and
data > data structure is matching 'get_expected_selection_data' method.
Questions:
Require '_ExampleController' as abstraction?
Args:
controller (Any): Controller object. ('_ExampleController')
handle_project (bool): Project can be considered as can have expected
selection.
handle_folder (bool): Folder can be considered as can have expected
selection.
handle_task (bool): Task can be considered as can have expected
selection.
"""
def __init__(
self,
controller,
handle_project=True,
handle_folder=True,
handle_task=True
):
self._project_name = None
self._folder_id = None
self._task_name = None
self._project_selected = True
self._folder_selected = True
self._task_selected = True
self._controller = controller
self._handle_project = handle_project
self._handle_folder = handle_folder
self._handle_task = handle_task
def set_expected_selection(
self,
project_name=None,
folder_id=None,
task_name=None
):
"""Sets expected selection.
Args:
project_name (Optional[str]): Project name.
folder_id (Optional[str]): Folder id.
task_name (Optional[str]): Task name.
"""
self._project_name = project_name
self._folder_id = folder_id
self._task_name = task_name
self._project_selected = not self._handle_project
self._folder_selected = not self._handle_folder
self._task_selected = not self._handle_task
self._emit_change()
def get_expected_selection_data(self):
project_current = False
folder_current = False
task_current = False
if not self._project_selected:
project_current = True
elif not self._folder_selected:
folder_current = True
elif not self._task_selected:
task_current = True
data = {}
if self._handle_project:
data["project"] = {
"name": self._project_name,
"current": project_current,
"selected": self._project_selected,
}
if self._handle_folder:
data["folder"] = {
"id": self._folder_id,
"current": folder_current,
"selected": self._folder_selected,
}
if self._handle_task:
data["task"] = {
"name": self._task_name,
"current": task_current,
"selected": self._task_selected,
}
return data
def is_expected_project_selected(self, project_name):
if not self._handle_project:
return True
return project_name == self._project_name and self._project_selected
def is_expected_folder_selected(self, folder_id):
if not self._handle_folder:
return True
return folder_id == self._folder_id and self._folder_selected
def expected_project_selected(self, project_name):
"""UI selected requested project.
Other entity types can be requested for selection.
Args:
project_name (str): Name of project.
"""
if project_name != self._project_name:
return False
self._project_selected = True
self._emit_change()
return True
def expected_folder_selected(self, folder_id):
"""UI selected requested folder.
Other entity types can be requested for selection.
Args:
folder_id (str): Folder id.
"""
if folder_id != self._folder_id:
return False
self._folder_selected = True
self._emit_change()
return True
def expected_task_selected(self, folder_id, task_name):
"""UI selected requested task.
Other entity types can be requested for selection.
Because task name is not unique across project a folder id is also
required to confirm the right task has been selected.
Args:
folder_id (str): Folder id.
task_name (str): Task name.
"""
if self._folder_id != folder_id:
return False
if task_name != self._task_name:
return False
self._task_selected = True
self._emit_change()
return True
def _emit_change(self):
self._controller.emit_event(
"expected_selection_changed",
self.get_expected_selection_data(),
)

View file

@ -503,17 +503,6 @@ class ProjectsCombobox(QtWidgets.QWidget):
self._projects_model.set_current_context_project(project_name)
self._projects_proxy_model.invalidateFilter()
def _update_select_item_visiblity(self, **kwargs):
if not self._select_item_visible:
return
if "project_name" not in kwargs:
project_name = self.get_selected_project_name()
else:
project_name = kwargs.get("project_name")
# Hide the item if a project is selected
self._projects_model.set_selected_project(project_name)
def set_select_item_visible(self, visible):
self._select_item_visible = visible
self._projects_model.set_select_item_visible(visible)
@ -534,6 +523,17 @@ class ProjectsCombobox(QtWidgets.QWidget):
def set_library_filter_enabled(self, enabled):
return self._projects_proxy_model.set_library_filter_enabled(enabled)
def _update_select_item_visiblity(self, **kwargs):
if not self._select_item_visible:
return
if "project_name" not in kwargs:
project_name = self.get_selected_project_name()
else:
project_name = kwargs.get("project_name")
# Hide the item if a project is selected
self._projects_model.set_selected_project(project_name)
def _on_current_index_changed(self, idx):
if not self._listen_selection_change:
return

View file

@ -443,8 +443,11 @@ class AbstractWorkfilesBackend(AbstractWorkfilesCommon):
pass
@abstractmethod
def get_project_entity(self):
"""Get current project entity.
def get_project_entity(self, project_name):
"""Get project entity by name.
Args:
project_name (str): Project name.
Returns:
dict[str, Any]: Project entity data.
@ -453,10 +456,11 @@ class AbstractWorkfilesBackend(AbstractWorkfilesCommon):
pass
@abstractmethod
def get_folder_entity(self, folder_id):
def get_folder_entity(self, project_name, folder_id):
"""Get folder entity by id.
Args:
project_name (str): Project name.
folder_id (str): Folder id.
Returns:
@ -466,10 +470,11 @@ class AbstractWorkfilesBackend(AbstractWorkfilesCommon):
pass
@abstractmethod
def get_task_entity(self, task_id):
def get_task_entity(self, project_name, task_id):
"""Get task entity by id.
Args:
project_name (str): Project name.
task_id (str): Task id.
Returns:
@ -574,12 +579,10 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
pass
@abstractmethod
def set_selected_task(self, folder_id, task_id, task_name):
def set_selected_task(self, task_id, task_name):
"""Change selected task.
Args:
folder_id (Union[str, None]): Folder id or None if no folder
is selected.
task_id (Union[str, None]): Task id or None if no task
is selected.
task_name (Union[str, None]): Task name or None if no task
@ -711,21 +714,27 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
pass
@abstractmethod
def expected_representation_selected(self, representation_id):
def expected_representation_selected(
self, folder_id, task_name, representation_id
):
"""Expected representation was selected in UI.
Args:
folder_id (str): Folder id under which representation is.
task_name (str): Task name under which representation is.
representation_id (str): Representation id which was selected.
"""
pass
@abstractmethod
def expected_workfile_selected(self, workfile_path):
def expected_workfile_selected(self, folder_id, task_name, workfile_name):
"""Expected workfile was selected in UI.
Args:
workfile_path (str): Workfile path which was selected.
folder_id (str): Folder id under which workfile is.
task_name (str): Task name under which workfile is.
workfile_name (str): Workfile filename which was selected.
"""
pass
@ -738,7 +747,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
# Model functions
@abstractmethod
def get_folder_items(self, sender):
def get_folder_items(self, project_name, sender):
"""Folder items to visualize project hierarchy.
This function may trigger events 'folders.refresh.started' and
@ -746,6 +755,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
That may help to avoid re-refresh of folder items in UI elements.
Args:
project_name (str): Project name for which are folders requested.
sender (str): Who requested folder items.
Returns:
@ -756,7 +766,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
pass
@abstractmethod
def get_task_items(self, folder_id, sender):
def get_task_items(self, project_name, folder_id, sender):
"""Task items.
This function may trigger events 'tasks.refresh.started' and
@ -764,6 +774,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
That may help to avoid re-refresh of task items in UI elements.
Args:
project_name (str): Project name for which are tasks requested.
folder_id (str): Folder ID for which are tasks requested.
sender (str): Who requested folder items.
@ -892,22 +903,25 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
At this moment the only information which can be saved about
workfile is 'note'.
When 'note' is 'None' it is only validated if workfile info exists,
and if not then creates one with empty note.
Args:
folder_id (str): Folder id.
task_id (str): Task id.
filepath (str): Workfile path.
note (str): Note.
note (Union[str, None]): Note.
"""
pass
# General commands
@abstractmethod
def refresh(self):
"""Refresh everything, models, ui etc.
def reset(self):
"""Reset everything, models, ui etc.
Triggers 'controller.refresh.started' event at the beginning and
'controller.refresh.finished' at the end.
Triggers 'controller.reset.started' event at the beginning and
'controller.reset.finished' at the end.
"""
pass

View file

@ -16,93 +16,120 @@ from openpype.pipeline.context_tools import (
)
from openpype.pipeline.workfile import create_workdir_extra_folders
from openpype.tools.ayon_utils.models import (
HierarchyModel,
HierarchyExpectedSelection,
ProjectsModel,
)
from .abstract import (
AbstractWorkfilesFrontend,
AbstractWorkfilesBackend,
)
from .models import SelectionModel, EntitiesModel, WorkfilesModel
from .models import SelectionModel, WorkfilesModel
class ExpectedSelection:
def __init__(self):
self._folder_id = None
self._task_name = None
class WorkfilesToolExpectedSelection(HierarchyExpectedSelection):
def __init__(self, controller):
super(WorkfilesToolExpectedSelection, self).__init__(
controller,
handle_project=False,
handle_folder=True,
handle_task=True,
)
self._workfile_name = None
self._representation_id = None
self._folder_selected = True
self._task_selected = True
self._workfile_name_selected = True
self._representation_id_selected = True
self._workfile_selected = True
self._representation_selected = True
def set_expected_selection(
self,
folder_id,
task_name,
project_name=None,
folder_id=None,
task_name=None,
workfile_name=None,
representation_id=None
representation_id=None,
):
self._folder_id = folder_id
self._task_name = task_name
self._workfile_name = workfile_name
self._representation_id = representation_id
self._folder_selected = False
self._task_selected = False
self._workfile_name_selected = workfile_name is None
self._representation_id_selected = representation_id is None
self._workfile_selected = False
self._representation_selected = False
super(WorkfilesToolExpectedSelection, self).set_expected_selection(
project_name,
folder_id,
task_name,
)
def get_expected_selection_data(self):
return {
"folder_id": self._folder_id,
"task_name": self._task_name,
"workfile_name": self._workfile_name,
"representation_id": self._representation_id,
"folder_selected": self._folder_selected,
"task_selected": self._task_selected,
"workfile_name_selected": self._workfile_name_selected,
"representation_id_selected": self._representation_id_selected,
data = super(
WorkfilesToolExpectedSelection, self
).get_expected_selection_data()
_is_current = (
self._project_selected
and self._folder_selected
and self._task_selected
)
workfile_is_current = False
repre_is_current = False
if _is_current:
workfile_is_current = not self._workfile_selected
repre_is_current = not self._representation_selected
data["workfile"] = {
"name": self._workfile_name,
"current": workfile_is_current,
"selected": self._workfile_selected,
}
data["representation"] = {
"id": self._representation_id,
"current": repre_is_current,
"selected": self._workfile_selected,
}
return data
def is_expected_folder_selected(self, folder_id):
return folder_id == self._folder_id and self._folder_selected
def is_expected_workfile_selected(self, workfile_name):
return (
workfile_name == self._workfile_name
and self._workfile_selected
)
def is_expected_task_selected(self, folder_id, task_name):
if not self.is_expected_folder_selected(folder_id):
return False
return task_name == self._task_name and self._task_selected
def is_expected_representation_selected(self, representation_id):
return (
representation_id == self._representation_id
and self._representation_selected
)
def expected_folder_selected(self, folder_id):
def expected_workfile_selected(self, folder_id, task_name, workfile_name):
if folder_id != self._folder_id:
return False
self._folder_selected = True
return True
def expected_task_selected(self, folder_id, task_name):
if not self.is_expected_folder_selected(folder_id):
return False
if task_name != self._task_name:
return False
self._task_selected = True
return True
def expected_workfile_selected(self, folder_id, task_name, workfile_name):
if not self.is_expected_task_selected(folder_id, task_name):
return False
if workfile_name != self._workfile_name:
return False
self._workfile_name_selected = True
self._workfile_selected = True
self._emit_change()
return True
def expected_representation_selected(
self, folder_id, task_name, representation_id
):
if not self.is_expected_task_selected(folder_id, task_name):
if folder_id != self._folder_id:
return False
if task_name != self._task_name:
return False
if representation_id != self._representation_id:
return False
self._representation_id_selected = True
self._representation_selected = True
self._emit_change()
return True
@ -136,9 +163,9 @@ class BaseWorkfileController(
# Expected selected folder and task
self._expected_selection = self._create_expected_selection_obj()
self._selection_model = self._create_selection_model()
self._entities_model = self._create_entities_model()
self._projects_model = self._create_projects_model()
self._hierarchy_model = self._create_hierarchy_model()
self._workfiles_model = self._create_workfiles_model()
@property
@ -151,13 +178,16 @@ class BaseWorkfileController(
return self._host_is_valid
def _create_expected_selection_obj(self):
return ExpectedSelection()
return WorkfilesToolExpectedSelection(self)
def _create_projects_model(self):
return ProjectsModel(self)
def _create_selection_model(self):
return SelectionModel(self)
def _create_entities_model(self):
return EntitiesModel(self)
def _create_hierarchy_model(self):
return HierarchyModel(self)
def _create_workfiles_model(self):
return WorkfilesModel(self)
@ -193,14 +223,17 @@ class BaseWorkfileController(
self._project_anatomy = Anatomy(self.get_current_project_name())
return self._project_anatomy
def get_project_entity(self):
return self._entities_model.get_project_entity()
def get_project_entity(self, project_name):
return self._projects_model.get_project_entity(
project_name)
def get_folder_entity(self, folder_id):
return self._entities_model.get_folder_entity(folder_id)
def get_folder_entity(self, project_name, folder_id):
return self._hierarchy_model.get_folder_entity(
project_name, folder_id)
def get_task_entity(self, task_id):
return self._entities_model.get_task_entity(task_id)
def get_task_entity(self, project_name, task_id):
return self._hierarchy_model.get_task_entity(
project_name, task_id)
# ---------------------------------
# Implementation of abstract methods
@ -293,9 +326,8 @@ class BaseWorkfileController(
def get_selected_task_name(self):
return self._selection_model.get_selected_task_name()
def set_selected_task(self, folder_id, task_id, task_name):
return self._selection_model.set_selected_task(
folder_id, task_id, task_name)
def set_selected_task(self, task_id, task_name):
return self._selection_model.set_selected_task(task_id, task_name)
def get_selected_workfile_path(self):
return self._selection_model.get_selected_workfile_path()
@ -318,7 +350,11 @@ class BaseWorkfileController(
representation_id=None
):
self._expected_selection.set_expected_selection(
folder_id, task_name, workfile_name, representation_id
self.get_current_project_name(),
folder_id,
task_name,
workfile_name,
representation_id
)
self._trigger_expected_selection_changed()
@ -355,11 +391,13 @@ class BaseWorkfileController(
)
# Model functions
def get_folder_items(self, sender):
return self._entities_model.get_folder_items(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, folder_id, sender):
return self._entities_model.get_tasks_items(folder_id, sender)
def get_task_items(self, project_name, folder_id, sender=None):
return self._hierarchy_model.get_task_items(
project_name, folder_id, sender
)
def get_workarea_dir_by_context(self, folder_id, task_id):
return self._workfiles_model.get_workarea_dir_by_context(
@ -394,7 +432,9 @@ class BaseWorkfileController(
def get_published_file_items(self, folder_id, task_id):
task_name = None
if task_id:
task = self.get_task_entity(task_id)
task = self.get_task_entity(
self.get_current_project_name(), task_id
)
task_name = task.get("name")
return self._workfiles_model.get_published_file_items(
@ -410,21 +450,27 @@ class BaseWorkfileController(
folder_id, task_id, filepath, note
)
def refresh(self):
def reset(self):
if not self._host_is_valid:
self._emit_event("controller.refresh.started")
self._emit_event("controller.refresh.finished")
self._emit_event("controller.reset.started")
self._emit_event("controller.reset.finished")
return
expected_folder_id = self.get_selected_folder_id()
expected_task_name = self.get_selected_task_name()
expected_work_path = self.get_selected_workfile_path()
expected_repre_id = self.get_selected_representation_id()
expected_work_name = None
if expected_work_path:
expected_work_name = os.path.basename(expected_work_path)
self._emit_event("controller.refresh.started")
self._emit_event("controller.reset.started")
context = self._get_host_current_context()
project_name = context["project_name"]
folder_name = context["asset_name"]
task_name = context["task_name"]
current_file = self.get_current_workfile()
folder_id = None
if folder_name:
folder = ayon_api.get_folder_by_path(project_name, folder_name)
@ -439,18 +485,25 @@ class BaseWorkfileController(
self._current_folder_id = folder_id
self._current_task_name = task_name
self._projects_model.reset()
self._hierarchy_model.reset()
if not expected_folder_id:
expected_folder_id = folder_id
expected_task_name = task_name
if current_file:
expected_work_name = os.path.basename(current_file)
self._emit_event("controller.reset.finished")
self._expected_selection.set_expected_selection(
expected_folder_id, expected_task_name
project_name,
expected_folder_id,
expected_task_name,
expected_work_name,
expected_repre_id,
)
self._entities_model.refresh()
self._emit_event("controller.refresh.finished")
# Controller actions
def open_workfile(self, folder_id, task_id, filepath):
self._emit_event("open_workfile.started")
@ -579,9 +632,9 @@ class BaseWorkfileController(
self, project_name, folder_id, task_id, folder=None, task=None
):
if folder is None:
folder = self.get_folder_entity(folder_id)
folder = self.get_folder_entity(project_name, folder_id)
if task is None:
task = self.get_task_entity(task_id)
task = self.get_task_entity(project_name, task_id)
# NOTE keys should be OpenPype compatible
return {
"project_name": project_name,
@ -633,8 +686,8 @@ class BaseWorkfileController(
):
# Trigger before save event
project_name = self.get_current_project_name()
folder = self.get_folder_entity(folder_id)
task = self.get_task_entity(task_id)
folder = self.get_folder_entity(project_name, folder_id)
task = self.get_task_entity(project_name, task_id)
task_name = task["name"]
# QUESTION should the data be different for 'before' and 'after'?
@ -674,6 +727,9 @@ class BaseWorkfileController(
else:
self._host_save_workfile(dst_filepath)
# Make sure workfile info exists
self.save_workfile_info(folder_id, task_id, dst_filepath, None)
# Create extra folders
create_workdir_extra_folders(
workdir,
@ -685,4 +741,4 @@ class BaseWorkfileController(
# Trigger after save events
emit_event("workfile.save.after", event_data, source="workfiles.tool")
self.refresh()
self.reset()

View file

@ -1,10 +1,8 @@
from .hierarchy import EntitiesModel
from .selection import SelectionModel
from .workfiles import WorkfilesModel
__all__ = (
"SelectionModel",
"EntitiesModel",
"WorkfilesModel",
)

View file

@ -1,236 +0,0 @@
"""Hierarchy model that handles folders and tasks.
The model can be extracted for common usage. In that case it will be required
to add more handling of project name changes.
"""
import time
import collections
import contextlib
import ayon_api
from openpype.tools.ayon_workfiles.abstract import (
FolderItem,
TaskItem,
)
def _get_task_items_from_tasks(tasks):
"""
Returns:
TaskItem: Task item.
"""
output = []
for task in tasks:
folder_id = task["folderId"]
output.append(TaskItem(
task["id"],
task["name"],
task["type"],
folder_id,
None,
None
))
return output
def _get_folder_item_from_hierarchy_item(item):
return FolderItem(
item["id"],
item["parentId"],
item["name"],
item["label"],
None,
None,
)
class CacheItem:
def __init__(self, lifetime=120):
self._lifetime = lifetime
self._last_update = None
self._data = None
@property
def is_valid(self):
if self._last_update is None:
return False
return (time.time() - self._last_update) < self._lifetime
def set_invalid(self, data=None):
self._last_update = None
self._data = data
def get_data(self):
return self._data
def update_data(self, data):
self._data = data
self._last_update = time.time()
class EntitiesModel(object):
event_source = "entities.model"
def __init__(self, controller):
project_cache = CacheItem()
project_cache.set_invalid({})
folders_cache = CacheItem()
folders_cache.set_invalid({})
self._project_cache = project_cache
self._folders_cache = folders_cache
self._tasks_cache = {}
self._folders_by_id = {}
self._tasks_by_id = {}
self._folders_refreshing = False
self._tasks_refreshing = set()
self._controller = controller
def reset(self):
self._project_cache.set_invalid({})
self._folders_cache.set_invalid({})
self._tasks_cache = {}
self._folders_by_id = {}
self._tasks_by_id = {}
def refresh(self):
self._refresh_folders_cache()
def get_project_entity(self):
if not self._project_cache.is_valid:
project_name = self._controller.get_current_project_name()
project_entity = ayon_api.get_project(project_name)
self._project_cache.update_data(project_entity)
return self._project_cache.get_data()
def get_folder_items(self, sender):
if not self._folders_cache.is_valid:
self._refresh_folders_cache(sender)
return self._folders_cache.get_data()
def get_tasks_items(self, folder_id, sender):
if not folder_id:
return []
task_cache = self._tasks_cache.get(folder_id)
if task_cache is None or not task_cache.is_valid:
self._refresh_tasks_cache(folder_id, sender)
task_cache = self._tasks_cache.get(folder_id)
return task_cache.get_data()
def get_folder_entity(self, folder_id):
if folder_id not in self._folders_by_id:
entity = None
if folder_id:
project_name = self._controller.get_current_project_name()
entity = ayon_api.get_folder_by_id(project_name, folder_id)
self._folders_by_id[folder_id] = entity
return self._folders_by_id[folder_id]
def get_task_entity(self, task_id):
if task_id not in self._tasks_by_id:
entity = None
if task_id:
project_name = self._controller.get_current_project_name()
entity = ayon_api.get_task_by_id(project_name, task_id)
self._tasks_by_id[task_id] = entity
return self._tasks_by_id[task_id]
@contextlib.contextmanager
def _folder_refresh_event_manager(self, project_name, sender):
self._folders_refreshing = True
self._controller.emit_event(
"folders.refresh.started",
{"project_name": project_name, "sender": sender},
self.event_source
)
try:
yield
finally:
self._controller.emit_event(
"folders.refresh.finished",
{"project_name": project_name, "sender": sender},
self.event_source
)
self._folders_refreshing = False
@contextlib.contextmanager
def _task_refresh_event_manager(
self, project_name, folder_id, sender
):
self._tasks_refreshing.add(folder_id)
self._controller.emit_event(
"tasks.refresh.started",
{
"project_name": project_name,
"folder_id": folder_id,
"sender": sender,
},
self.event_source
)
try:
yield
finally:
self._controller.emit_event(
"tasks.refresh.finished",
{
"project_name": project_name,
"folder_id": folder_id,
"sender": sender,
},
self.event_source
)
self._tasks_refreshing.discard(folder_id)
def _refresh_folders_cache(self, sender=None):
if self._folders_refreshing:
return
project_name = self._controller.get_current_project_name()
with self._folder_refresh_event_manager(project_name, sender):
folder_items = self._query_folders(project_name)
self._folders_cache.update_data(folder_items)
def _query_folders(self, project_name):
hierarchy = ayon_api.get_folders_hierarchy(project_name)
folder_items = {}
hierachy_queue = collections.deque(hierarchy["hierarchy"])
while hierachy_queue:
item = hierachy_queue.popleft()
folder_item = _get_folder_item_from_hierarchy_item(item)
folder_items[folder_item.entity_id] = folder_item
hierachy_queue.extend(item["children"] or [])
return folder_items
def _refresh_tasks_cache(self, folder_id, sender=None):
if folder_id in self._tasks_refreshing:
return
project_name = self._controller.get_current_project_name()
with self._task_refresh_event_manager(
project_name, folder_id, sender
):
cache_item = self._tasks_cache.get(folder_id)
if cache_item is None:
cache_item = CacheItem()
self._tasks_cache[folder_id] = cache_item
task_items = self._query_tasks(project_name, folder_id)
cache_item.update_data(task_items)
def _query_tasks(self, project_name, folder_id):
tasks = list(ayon_api.get_tasks(
project_name,
folder_ids=[folder_id],
fields={"id", "name", "label", "folderId", "type"}
))
return _get_task_items_from_tasks(tasks)

View file

@ -4,7 +4,7 @@ class SelectionModel(object):
Triggering events:
- "selection.folder.changed"
- "selection.task.changed"
- "workarea.selection.changed"
- "selection.workarea.changed"
- "selection.representation.changed"
"""
@ -29,7 +29,10 @@ class SelectionModel(object):
self._folder_id = folder_id
self._controller.emit_event(
"selection.folder.changed",
{"folder_id": folder_id},
{
"project_name": self._controller.get_current_project_name(),
"folder_id": folder_id
},
self.event_source
)
@ -39,10 +42,7 @@ class SelectionModel(object):
def get_selected_task_id(self):
return self._task_id
def set_selected_task(self, folder_id, task_id, task_name):
if folder_id != self._folder_id:
self.set_selected_folder(folder_id)
def set_selected_task(self, task_id, task_name):
if task_id == self._task_id:
return
@ -51,7 +51,8 @@ class SelectionModel(object):
self._controller.emit_event(
"selection.task.changed",
{
"folder_id": folder_id,
"project_name": self._controller.get_current_project_name(),
"folder_id": self._folder_id,
"task_name": task_name,
"task_id": task_id
},
@ -67,8 +68,9 @@ class SelectionModel(object):
self._workfile_path = path
self._controller.emit_event(
"workarea.selection.changed",
"selection.workarea.changed",
{
"project_name": self._controller.get_current_project_name(),
"path": path,
"folder_id": self._folder_id,
"task_name": self._task_name,
@ -86,6 +88,9 @@ class SelectionModel(object):
self._representation_id = representation_id
self._controller.emit_event(
"selection.representation.changed",
{"representation_id": representation_id},
{
"project_name": self._controller.get_current_project_name(),
"representation_id": representation_id,
},
self.event_source
)

View file

@ -148,7 +148,9 @@ class WorkareaModel:
def _get_folder_data(self, folder_id):
fill_data = self._fill_data_by_folder_id.get(folder_id)
if fill_data is None:
folder = self._controller.get_folder_entity(folder_id)
folder = self._controller.get_folder_entity(
self.project_name, folder_id
)
fill_data = get_folder_template_data(folder)
self._fill_data_by_folder_id[folder_id] = fill_data
return copy.deepcopy(fill_data)
@ -156,7 +158,9 @@ class WorkareaModel:
def _get_task_data(self, project_entity, folder_id, task_id):
task_data = self._task_data_by_folder_id.setdefault(folder_id, {})
if task_id not in task_data:
task = self._controller.get_task_entity(task_id)
task = self._controller.get_task_entity(
self.project_name, task_id
)
if task:
task_data[task_id] = get_task_template_data(
project_entity, task)
@ -167,8 +171,9 @@ class WorkareaModel:
return {}
base_data = self._get_base_data()
project_name = base_data["project"]["name"]
folder_data = self._get_folder_data(folder_id)
project_entity = self._controller.get_project_entity()
project_entity = self._controller.get_project_entity(project_name)
task_data = self._get_task_data(project_entity, folder_id, task_id)
base_data.update(folder_data)
@ -292,9 +297,13 @@ class WorkareaModel:
folder = None
task = None
if folder_id:
folder = self._controller.get_folder_entity(folder_id)
folder = self._controller.get_folder_entity(
self.project_name, folder_id
)
if task_id:
task = self._controller.get_task_entity(task_id)
task = self._controller.get_task_entity(
self.project_name, task_id
)
if not folder or not task:
return {
@ -491,10 +500,13 @@ class WorkfileEntitiesModel:
)
if not workfile_info:
self._cache[identifier] = self._create_workfile_info_entity(
task_id, rootless_path, note)
task_id, rootless_path, note or "")
self._items.pop(identifier, None)
return
if note is None:
return
new_workfile_info = copy.deepcopy(workfile_info)
attrib = new_workfile_info.setdefault("attrib", {})
attrib["description"] = note

View file

@ -69,7 +69,7 @@ class FilesWidget(QtWidgets.QWidget):
main_layout.addWidget(btns_widget, 0)
controller.register_event_callback(
"workarea.selection.changed",
"selection.workarea.changed",
self._on_workarea_path_changed
)
controller.register_event_callback(

View file

@ -59,14 +59,6 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
self._add_empty_item()
def _clear_items(self):
self._remove_missing_context_item()
self._remove_empty_item()
if self._items_by_id:
root = self.invisibleRootItem()
root.removeRows(0, root.rowCount())
self._items_by_id = {}
def set_published_mode(self, published_mode):
if self._published_mode == published_mode:
return
@ -89,6 +81,18 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
return QtCore.QModelIndex()
return self.indexFromItem(item)
def refresh(self):
if self._published_mode:
self._fill_items()
def _clear_items(self):
self._remove_missing_context_item()
self._remove_empty_item()
if self._items_by_id:
root = self.invisibleRootItem()
root.removeRows(0, root.rowCount())
self._items_by_id = {}
def _get_missing_context_item(self):
if self._missing_context_item is None:
message = "Select folder"
@ -149,7 +153,6 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
def _on_folder_changed(self, event):
self._last_folder_id = event["folder_id"]
self._last_task_id = None
if self._context_select_mode:
return
@ -356,14 +359,13 @@ class PublishedFilesWidget(QtWidgets.QWidget):
self.save_as_requested.emit()
def _on_expected_selection_change(self, event):
if (
event["representation_id_selected"]
or not event["folder_selected"]
or (event["task_name"] and not event["task_selected"])
):
repre_info = event["representation"]
if not repre_info["current"]:
return
representation_id = event["representation_id"]
self._model.refresh()
representation_id = repre_info["id"]
selected_repre_id = self.get_selected_repre_id()
if (
representation_id is not None
@ -376,5 +378,5 @@ class PublishedFilesWidget(QtWidgets.QWidget):
self._view.setCurrentIndex(proxy_index)
self._controller.expected_representation_selected(
event["folder_id"], event["task_name"], representation_id
event["folder"]["id"], event["task"]["name"], representation_id
)

View file

@ -28,6 +28,10 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
self.setHeaderData(0, QtCore.Qt.Horizontal, "Name")
self.setHeaderData(1, QtCore.Qt.Horizontal, "Date Modified")
controller.register_event_callback(
"selection.folder.changed",
self._on_folder_changed
)
controller.register_event_callback(
"selection.task.changed",
self._on_task_changed
@ -63,6 +67,10 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
return QtCore.QModelIndex()
return self.indexFromItem(item)
def refresh(self):
if not self._published_mode:
self._fill_items()
def _get_missing_context_item(self):
if self._missing_context_item is None:
message = "Select folder and task"
@ -129,6 +137,11 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
root_item.takeRow(self._empty_root_item.row())
self._empty_item_used = False
def _on_folder_changed(self, event):
self._selected_folder_id = event["folder_id"]
if not self._published_mode:
self._fill_items()
def _on_task_changed(self, event):
self._selected_folder_id = event["folder_id"]
self._selected_task_id = event["task_id"]
@ -362,10 +375,13 @@ class WorkAreaFilesWidget(QtWidgets.QWidget):
self.duplicate_requested.emit()
def _on_expected_selection_change(self, event):
if event["workfile_name_selected"]:
workfile_info = event["workfile"]
if not workfile_info["current"]:
return
workfile_name = event["workfile_name"]
self._model.refresh()
workfile_name = workfile_info["name"]
if (
workfile_name is not None
and workfile_name != self._get_selected_info()["filename"]
@ -376,5 +392,5 @@ class WorkAreaFilesWidget(QtWidgets.QWidget):
self._view.setCurrentIndex(proxy_index)
self._controller.expected_workfile_selected(
event["folder_id"], event["task_name"], workfile_name
event["folder"]["id"], event["task"]["name"], workfile_name
)

View file

@ -1,324 +0,0 @@
import uuid
import collections
import qtawesome
from qtpy import QtWidgets, QtGui, QtCore
from openpype.tools.utils import (
RecursiveSortFilterProxyModel,
DeselectableTreeView,
)
from .constants import ITEM_ID_ROLE, ITEM_NAME_ROLE
SENDER_NAME = "qt_folders_model"
class FoldersRefreshThread(QtCore.QThread):
"""Thread for refreshing folders.
Call controller to get folders and emit signal when finished.
Args:
controller (AbstractWorkfilesFrontend): The control object.
"""
refresh_finished = QtCore.Signal(str)
def __init__(self, controller):
super(FoldersRefreshThread, self).__init__()
self._id = uuid.uuid4().hex
self._controller = controller
self._result = None
@property
def id(self):
"""Thread id.
Returns:
str: Unique id of the thread.
"""
return self._id
def run(self):
self._result = self._controller.get_folder_items(SENDER_NAME)
self.refresh_finished.emit(self.id)
def get_result(self):
return self._result
class FoldersModel(QtGui.QStandardItemModel):
"""Folders model which cares about refresh of folders.
Args:
controller (AbstractWorkfilesFrontend): The control object.
"""
refreshed = QtCore.Signal()
def __init__(self, controller):
super(FoldersModel, self).__init__()
self._controller = controller
self._items_by_id = {}
self._parent_id_by_id = {}
self._refresh_threads = {}
self._current_refresh_thread = None
self._has_content = False
self._is_refreshing = False
@property
def is_refreshing(self):
"""Model is refreshing.
Returns:
bool: True if model is refreshing.
"""
return self._is_refreshing
@property
def has_content(self):
"""Has at least one folder.
Returns:
bool: True if model has at least one folder.
"""
return self._has_content
def clear(self):
self._items_by_id = {}
self._parent_id_by_id = {}
self._has_content = False
super(FoldersModel, self).clear()
def get_index_by_id(self, item_id):
"""Get index by folder id.
Returns:
QtCore.QModelIndex: Index of the folder. Can be invalid if folder
is not available.
"""
item = self._items_by_id.get(item_id)
if item is None:
return QtCore.QModelIndex()
return self.indexFromItem(item)
def refresh(self):
"""Refresh folders items.
Refresh start thread because it can cause that controller can
start query from database if folders are not cached.
"""
self._is_refreshing = True
thread = FoldersRefreshThread(self._controller)
self._current_refresh_thread = thread.id
self._refresh_threads[thread.id] = thread
thread.refresh_finished.connect(self._on_refresh_thread)
thread.start()
def _on_refresh_thread(self, thread_id):
"""Callback when refresh thread is finished.
Technically can be running multiple refresh threads at the same time,
to avoid using values from wrong thread, we check if thread id is
current refresh thread id.
Folders are stored by id.
Args:
thread_id (str): Thread id.
"""
thread = self._refresh_threads.pop(thread_id)
if thread_id != self._current_refresh_thread:
return
folder_items_by_id = thread.get_result()
if not folder_items_by_id:
if folder_items_by_id is not None:
self.clear()
self._is_refreshing = False
return
self._has_content = True
folder_ids = set(folder_items_by_id)
ids_to_remove = set(self._items_by_id) - folder_ids
folder_items_by_parent = collections.defaultdict(list)
for folder_item in folder_items_by_id.values():
folder_items_by_parent[folder_item.parent_id].append(folder_item)
hierarchy_queue = collections.deque()
hierarchy_queue.append(None)
while hierarchy_queue:
parent_id = hierarchy_queue.popleft()
folder_items = folder_items_by_parent[parent_id]
if parent_id is None:
parent_item = self.invisibleRootItem()
else:
parent_item = self._items_by_id[parent_id]
new_items = []
for folder_item in folder_items:
item_id = folder_item.entity_id
item = self._items_by_id.get(item_id)
if item is None:
is_new = True
item = QtGui.QStandardItem()
item.setEditable(False)
else:
is_new = self._parent_id_by_id[item_id] != parent_id
icon = qtawesome.icon(
folder_item.icon_name,
color=folder_item.icon_color,
)
item.setData(item_id, ITEM_ID_ROLE)
item.setData(folder_item.name, ITEM_NAME_ROLE)
item.setData(folder_item.label, QtCore.Qt.DisplayRole)
item.setData(icon, QtCore.Qt.DecorationRole)
if is_new:
new_items.append(item)
self._items_by_id[item_id] = item
self._parent_id_by_id[item_id] = parent_id
hierarchy_queue.append(item_id)
if new_items:
parent_item.appendRows(new_items)
for item_id in ids_to_remove:
item = self._items_by_id[item_id]
parent_id = self._parent_id_by_id[item_id]
if parent_id is None:
parent_item = self.invisibleRootItem()
else:
parent_item = self._items_by_id[parent_id]
parent_item.takeChild(item.row())
for item_id in ids_to_remove:
self._items_by_id.pop(item_id)
self._parent_id_by_id.pop(item_id)
self._is_refreshing = False
self.refreshed.emit()
class FoldersWidget(QtWidgets.QWidget):
"""Folders widget.
Widget that handles folders view, model and selection.
Args:
controller (AbstractWorkfilesFrontend): The control object.
parent (QtWidgets.QWidget): The parent widget.
"""
def __init__(self, controller, parent):
super(FoldersWidget, self).__init__(parent)
folders_view = DeselectableTreeView(self)
folders_view.setHeaderHidden(True)
folders_model = FoldersModel(controller)
folders_proxy_model = RecursiveSortFilterProxyModel()
folders_proxy_model.setSourceModel(folders_model)
folders_view.setModel(folders_proxy_model)
main_layout = QtWidgets.QHBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(folders_view, 1)
controller.register_event_callback(
"folders.refresh.finished",
self._on_folders_refresh_finished
)
controller.register_event_callback(
"controller.refresh.finished",
self._on_controller_refresh
)
controller.register_event_callback(
"expected_selection_changed",
self._on_expected_selection_change
)
selection_model = folders_view.selectionModel()
selection_model.selectionChanged.connect(self._on_selection_change)
folders_model.refreshed.connect(self._on_model_refresh)
self._controller = controller
self._folders_view = folders_view
self._folders_model = folders_model
self._folders_proxy_model = folders_proxy_model
self._expected_selection = None
def set_name_filter(self, name):
self._folders_proxy_model.setFilterFixedString(name)
def _clear(self):
self._folders_model.clear()
def _on_folders_refresh_finished(self, event):
if event["sender"] != SENDER_NAME:
self._folders_model.refresh()
def _on_controller_refresh(self):
self._update_expected_selection()
def _update_expected_selection(self, expected_data=None):
if expected_data is None:
expected_data = self._controller.get_expected_selection_data()
# We're done
if expected_data["folder_selected"]:
return
folder_id = expected_data["folder_id"]
self._expected_selection = folder_id
if not self._folders_model.is_refreshing:
self._set_expected_selection()
def _set_expected_selection(self):
folder_id = self._expected_selection
self._expected_selection = None
if (
folder_id is not None
and folder_id != self._get_selected_item_id()
):
index = self._folders_model.get_index_by_id(folder_id)
if index.isValid():
proxy_index = self._folders_proxy_model.mapFromSource(index)
self._folders_view.setCurrentIndex(proxy_index)
self._controller.expected_folder_selected(folder_id)
def _on_model_refresh(self):
if self._expected_selection:
self._set_expected_selection()
self._folders_proxy_model.sort(0)
def _on_expected_selection_change(self, event):
self._update_expected_selection(event.data)
def _get_selected_item_id(self):
selection_model = self._folders_view.selectionModel()
for index in selection_model.selectedIndexes():
item_id = index.data(ITEM_ID_ROLE)
if item_id is not None:
return item_id
return None
def _on_selection_change(self):
item_id = self._get_selected_item_id()
self._controller.set_selected_folder(item_id)

View file

@ -66,7 +66,7 @@ class SidePanelWidget(QtWidgets.QWidget):
btn_note_save.clicked.connect(self._on_save_click)
controller.register_event_callback(
"workarea.selection.changed", self._on_selection_change
"selection.workarea.changed", self._on_selection_change
)
self._details_input = details_input

View file

@ -1,420 +0,0 @@
import uuid
import qtawesome
from qtpy import QtWidgets, QtGui, QtCore
from openpype.style import get_disabled_entity_icon_color
from openpype.tools.utils import DeselectableTreeView
from .constants import (
ITEM_NAME_ROLE,
ITEM_ID_ROLE,
PARENT_ID_ROLE,
)
SENDER_NAME = "qt_tasks_model"
class RefreshThread(QtCore.QThread):
"""Thread for refreshing tasks.
Call controller to get tasks and emit signal when finished.
Args:
controller (AbstractWorkfilesFrontend): The control object.
folder_id (str): Folder id.
"""
refresh_finished = QtCore.Signal(str)
def __init__(self, controller, folder_id):
super(RefreshThread, self).__init__()
self._id = uuid.uuid4().hex
self._controller = controller
self._folder_id = folder_id
self._result = None
@property
def id(self):
return self._id
def run(self):
self._result = self._controller.get_task_items(
self._folder_id, SENDER_NAME)
self.refresh_finished.emit(self.id)
def get_result(self):
return self._result
class TasksModel(QtGui.QStandardItemModel):
"""Tasks model which cares about refresh of tasks by folder id.
Args:
controller (AbstractWorkfilesFrontend): The control object.
"""
refreshed = QtCore.Signal()
def __init__(self, controller):
super(TasksModel, self).__init__()
self._controller = controller
self._items_by_name = {}
self._has_content = False
self._is_refreshing = False
self._invalid_selection_item_used = False
self._invalid_selection_item = None
self._empty_tasks_item_used = False
self._empty_tasks_item = None
self._last_folder_id = None
self._refresh_threads = {}
self._current_refresh_thread = None
# Initial state
self._add_invalid_selection_item()
def clear(self):
self._items_by_name = {}
self._has_content = False
self._remove_invalid_items()
super(TasksModel, self).clear()
def refresh(self, folder_id):
"""Refresh tasks for folder.
Args:
folder_id (Union[str, None]): Folder id.
"""
self._refresh(folder_id)
def get_index_by_name(self, task_name):
"""Find item by name and return its index.
Returns:
QtCore.QModelIndex: Index of item. Is invalid if task is not
found by name.
"""
item = self._items_by_name.get(task_name)
if item is None:
return QtCore.QModelIndex()
return self.indexFromItem(item)
def get_last_folder_id(self):
"""Get last refreshed folder id.
Returns:
Union[str, None]: Folder id.
"""
return self._last_folder_id
def _get_invalid_selection_item(self):
if self._invalid_selection_item is None:
item = QtGui.QStandardItem("Select a folder")
item.setFlags(QtCore.Qt.NoItemFlags)
icon = qtawesome.icon(
"fa.times",
color=get_disabled_entity_icon_color()
)
item.setData(icon, QtCore.Qt.DecorationRole)
self._invalid_selection_item = item
return self._invalid_selection_item
def _get_empty_task_item(self):
if self._empty_tasks_item is None:
item = QtGui.QStandardItem("No task")
icon = qtawesome.icon(
"fa.exclamation-circle",
color=get_disabled_entity_icon_color()
)
item.setData(icon, QtCore.Qt.DecorationRole)
item.setFlags(QtCore.Qt.NoItemFlags)
self._empty_tasks_item = item
return self._empty_tasks_item
def _add_invalid_item(self, item):
self.clear()
root_item = self.invisibleRootItem()
root_item.appendRow(item)
def _remove_invalid_item(self, item):
root_item = self.invisibleRootItem()
root_item.takeRow(item.row())
def _remove_invalid_items(self):
self._remove_invalid_selection_item()
self._remove_empty_task_item()
def _add_invalid_selection_item(self):
if not self._invalid_selection_item_used:
self._add_invalid_item(self._get_invalid_selection_item())
self._invalid_selection_item_used = True
def _remove_invalid_selection_item(self):
if self._invalid_selection_item:
self._remove_invalid_item(self._get_invalid_selection_item())
self._invalid_selection_item_used = False
def _add_empty_task_item(self):
if not self._empty_tasks_item_used:
self._add_invalid_item(self._get_empty_task_item())
self._empty_tasks_item_used = True
def _remove_empty_task_item(self):
if self._empty_tasks_item_used:
self._remove_invalid_item(self._get_empty_task_item())
self._empty_tasks_item_used = False
def _refresh(self, folder_id):
self._is_refreshing = True
self._last_folder_id = folder_id
if not folder_id:
self._add_invalid_selection_item()
self._current_refresh_thread = None
self._is_refreshing = False
self.refreshed.emit()
return
thread = RefreshThread(self._controller, folder_id)
self._current_refresh_thread = thread.id
self._refresh_threads[thread.id] = thread
thread.refresh_finished.connect(self._on_refresh_thread)
thread.start()
def _on_refresh_thread(self, thread_id):
"""Callback when refresh thread is finished.
Technically can be running multiple refresh threads at the same time,
to avoid using values from wrong thread, we check if thread id is
current refresh thread id.
Tasks are stored by name, so if a folder has same task name as
previously selected folder it keeps the selection.
Args:
thread_id (str): Thread id.
"""
thread = self._refresh_threads.pop(thread_id)
if thread_id != self._current_refresh_thread:
return
task_items = thread.get_result()
# Task items are refreshed
if task_items is None:
return
# No tasks are available on folder
if not task_items:
self._add_empty_task_item()
return
self._remove_invalid_items()
new_items = []
new_names = set()
for task_item in task_items:
name = task_item.name
new_names.add(name)
item = self._items_by_name.get(name)
if item is None:
item = QtGui.QStandardItem()
item.setEditable(False)
new_items.append(item)
self._items_by_name[name] = item
# TODO cache locally
icon = qtawesome.icon(
task_item.icon_name,
color=task_item.icon_color,
)
item.setData(task_item.label, QtCore.Qt.DisplayRole)
item.setData(name, ITEM_NAME_ROLE)
item.setData(task_item.id, ITEM_ID_ROLE)
item.setData(task_item.parent_id, PARENT_ID_ROLE)
item.setData(icon, QtCore.Qt.DecorationRole)
root_item = self.invisibleRootItem()
for name in set(self._items_by_name) - new_names:
item = self._items_by_name.pop(name)
root_item.removeRow(item.row())
if new_items:
root_item.appendRows(new_items)
self._has_content = root_item.rowCount() > 0
self._is_refreshing = False
self.refreshed.emit()
@property
def is_refreshing(self):
"""Model is refreshing.
Returns:
bool: Model is refreshing
"""
return self._is_refreshing
@property
def has_content(self):
"""Model has content.
Returns:
bools: Have at least one task.
"""
return self._has_content
def headerData(self, section, orientation, role):
# Show nice labels in the header
if (
role == QtCore.Qt.DisplayRole
and orientation == QtCore.Qt.Horizontal
):
if section == 0:
return "Tasks"
return super(TasksModel, self).headerData(
section, orientation, role
)
class TasksWidget(QtWidgets.QWidget):
"""Tasks widget.
Widget that handles tasks view, model and selection.
Args:
controller (AbstractWorkfilesFrontend): Workfiles controller.
"""
def __init__(self, controller, parent):
super(TasksWidget, self).__init__(parent)
tasks_view = DeselectableTreeView(self)
tasks_view.setIndentation(0)
tasks_model = TasksModel(controller)
tasks_proxy_model = QtCore.QSortFilterProxyModel()
tasks_proxy_model.setSourceModel(tasks_model)
tasks_view.setModel(tasks_proxy_model)
main_layout = QtWidgets.QHBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(tasks_view, 1)
controller.register_event_callback(
"tasks.refresh.finished",
self._on_tasks_refresh_finished
)
controller.register_event_callback(
"selection.folder.changed",
self._folder_selection_changed
)
controller.register_event_callback(
"expected_selection_changed",
self._on_expected_selection_change
)
selection_model = tasks_view.selectionModel()
selection_model.selectionChanged.connect(self._on_selection_change)
tasks_model.refreshed.connect(self._on_tasks_model_refresh)
self._controller = controller
self._tasks_view = tasks_view
self._tasks_model = tasks_model
self._tasks_proxy_model = tasks_proxy_model
self._selected_folder_id = None
self._expected_selection_data = None
def _clear(self):
self._tasks_model.clear()
def _on_tasks_refresh_finished(self, event):
"""Tasks were refreshed in controller.
Ignore if refresh was triggered by tasks model, or refreshed folder is
not the same as currently selected folder.
Args:
event (Event): Event object.
"""
# Refresh only if current folder id is the same
if (
event["sender"] == SENDER_NAME
or event["folder_id"] != self._selected_folder_id
):
return
self._tasks_model.refresh(self._selected_folder_id)
def _folder_selection_changed(self, event):
self._selected_folder_id = event["folder_id"]
self._tasks_model.refresh(self._selected_folder_id)
def _on_tasks_model_refresh(self):
if not self._set_expected_selection():
self._on_selection_change()
self._tasks_proxy_model.sort(0)
def _set_expected_selection(self):
if self._expected_selection_data is None:
return False
folder_id = self._expected_selection_data["folder_id"]
task_name = self._expected_selection_data["task_name"]
self._expected_selection_data = None
model_folder_id = self._tasks_model.get_last_folder_id()
if folder_id != model_folder_id:
return False
if task_name is not None:
index = self._tasks_model.get_index_by_name(task_name)
if index.isValid():
proxy_index = self._tasks_proxy_model.mapFromSource(index)
self._tasks_view.setCurrentIndex(proxy_index)
self._controller.expected_task_selected(folder_id, task_name)
return True
def _on_expected_selection_change(self, event):
if event["task_selected"] or not event["folder_selected"]:
return
model_folder_id = self._tasks_model.get_last_folder_id()
folder_id = event["folder_id"]
self._expected_selection_data = {
"task_name": event["task_name"],
"folder_id": folder_id,
}
if folder_id != model_folder_id or self._tasks_model.is_refreshing:
return
self._set_expected_selection()
def _get_selected_item_ids(self):
selection_model = self._tasks_view.selectionModel()
for index in selection_model.selectedIndexes():
task_id = index.data(ITEM_ID_ROLE)
task_name = index.data(ITEM_NAME_ROLE)
parent_id = index.data(PARENT_ID_ROLE)
if task_name is not None:
return parent_id, task_id, task_name
return self._selected_folder_id, None, None
def _on_selection_change(self):
# Don't trigger task change during refresh
# - a task was deselected if that happens
# - can cause crash triggered during tasks refreshing
if self._tasks_model.is_refreshing:
return
parent_id, task_id, task_name = self._get_selected_item_ids()
self._controller.set_selected_task(parent_id, task_id, task_name)

View file

@ -5,32 +5,16 @@ from openpype.tools.utils import (
PlaceholderLineEdit,
MessageOverlayObject,
)
from openpype.tools.utils.lib import get_qta_icon_by_name_and_color
from openpype.tools.ayon_utils.widgets import FoldersWidget, TasksWidget
from openpype.tools.ayon_workfiles.control import BaseWorkfileController
from openpype.tools.utils import GoToCurrentButton, RefreshButton
from .side_panel import SidePanelWidget
from .folders_widget import FoldersWidget
from .tasks_widget import TasksWidget
from .files_widget import FilesWidget
from .utils import BaseOverlayFrame
# TODO move to utils
# from openpype.tools.utils.lib import (
# get_refresh_icon, get_go_to_current_icon)
def get_refresh_icon():
return get_qta_icon_by_name_and_color(
"fa.refresh", style.get_default_tools_icon_color()
)
def get_go_to_current_icon():
return get_qta_icon_by_name_and_color(
"fa.arrow-down", style.get_default_tools_icon_color()
)
class InvalidHostOverlay(BaseOverlayFrame):
def __init__(self, parent):
super(InvalidHostOverlay, self).__init__(parent)
@ -80,7 +64,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
self._default_window_flags = flags
self._folder_widget = None
self._folders_widget = None
self._folder_filter_input = None
self._files_widget = None
@ -100,7 +84,9 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
home_body_widget = QtWidgets.QWidget(home_page_widget)
col_1_widget = self._create_col_1_widget(controller, parent)
tasks_widget = TasksWidget(controller, home_body_widget)
tasks_widget = TasksWidget(
controller, home_body_widget, handle_expected_selection=True
)
col_3_widget = self._create_col_3_widget(controller, home_body_widget)
side_panel = SidePanelWidget(controller, home_body_widget)
@ -151,11 +137,11 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
self._on_open_finished
)
controller.register_event_callback(
"controller.refresh.started",
"controller.reset.started",
self._on_controller_refresh_started,
)
controller.register_event_callback(
"controller.refresh.finished",
"controller.reset.finished",
self._on_controller_refresh_finished,
)
@ -188,19 +174,12 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
folder_filter_input = PlaceholderLineEdit(header_widget)
folder_filter_input.setPlaceholderText("Filter folders..")
go_to_current_btn = QtWidgets.QPushButton(header_widget)
go_to_current_btn.setIcon(get_go_to_current_icon())
go_to_current_btn_sp = go_to_current_btn.sizePolicy()
go_to_current_btn_sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum)
go_to_current_btn.setSizePolicy(go_to_current_btn_sp)
go_to_current_btn = GoToCurrentButton(header_widget)
refresh_btn = RefreshButton(header_widget)
refresh_btn = QtWidgets.QPushButton(header_widget)
refresh_btn.setIcon(get_refresh_icon())
refresh_btn_sp = refresh_btn.sizePolicy()
refresh_btn_sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum)
refresh_btn.setSizePolicy(refresh_btn_sp)
folder_widget = FoldersWidget(controller, col_widget)
folder_widget = FoldersWidget(
controller, col_widget, handle_expected_selection=True
)
header_layout = QtWidgets.QHBoxLayout(header_widget)
header_layout.setContentsMargins(0, 0, 0, 0)
@ -218,7 +197,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
refresh_btn.clicked.connect(self._on_refresh_clicked)
self._folder_filter_input = folder_filter_input
self._folder_widget = folder_widget
self._folders_widget = folder_widget
return col_widget
@ -300,7 +279,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
def refresh(self):
"""Trigger refresh of workfiles tool controller."""
self._controller.refresh()
self._controller.reset()
def showEvent(self, event):
super(WorkfilesToolWindow, self).showEvent(event)
@ -338,7 +317,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
self._side_panel.set_published_mode(published_mode)
def _on_folder_filter_change(self, text):
self._folder_widget.set_name_filter(text)
self._folders_widget.set_name_filter(text)
def _on_go_to_current_clicked(self):
self._controller.go_to_current_context()
@ -357,6 +336,10 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
if not self._host_is_valid:
return
self._folders_widget.set_project_name(
self._controller.get_current_project_name()
)
def _on_save_as_finished(self, event):
if event["failed"]:
self._overlay_messages_widget.add_message(