mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 21:32:15 +01:00
Merge branch 'develop' into feature/OP-7176_Use-folder-path-as-unique-identifier
This commit is contained in:
commit
6e8f142e52
30 changed files with 535 additions and 1200 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
||||
|
|
|
|||
179
openpype/tools/ayon_utils/models/selection.py
Normal file
179
openpype/tools/ayon_utils/models/selection.py
Normal 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(),
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
from .hierarchy import EntitiesModel
|
||||
from .selection import SelectionModel
|
||||
from .workfiles import WorkfilesModel
|
||||
|
||||
|
||||
__all__ = (
|
||||
"SelectionModel",
|
||||
"EntitiesModel",
|
||||
"WorkfilesModel",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue