mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge pull request #877 from pypeclub/feature/653-store-workfiles-information-in-the-db
Store workfiles information in the db
This commit is contained in:
commit
dd7dfd57da
11 changed files with 1852 additions and 24 deletions
|
|
@ -11,7 +11,9 @@ from pype.api import (
|
|||
from pype.lib import (
|
||||
env_value_to_bool,
|
||||
PreLaunchHook,
|
||||
ApplicationLaunchFailed
|
||||
ApplicationLaunchFailed,
|
||||
get_workdir_data,
|
||||
get_workdir_with_workdir_data,
|
||||
)
|
||||
|
||||
import acre
|
||||
|
|
@ -140,17 +142,15 @@ class GlobalHostDataHook(PreLaunchHook):
|
|||
)
|
||||
return
|
||||
|
||||
workdir_data = self._prepare_workdir_data(
|
||||
project_doc, asset_doc, task_name
|
||||
workdir_data = get_workdir_data(
|
||||
project_doc, asset_doc, task_name, self.host_name
|
||||
)
|
||||
self.data["workdir_data"] = workdir_data
|
||||
|
||||
hierarchy = workdir_data["hierarchy"]
|
||||
anatomy = self.data["anatomy"]
|
||||
|
||||
try:
|
||||
anatomy_filled = anatomy.format(workdir_data)
|
||||
workdir = os.path.normpath(anatomy_filled["work"]["folder"])
|
||||
workdir = get_workdir_with_workdir_data(workdir_data, anatomy)
|
||||
if not os.path.exists(workdir):
|
||||
self.log.debug(
|
||||
"Creating workdir folder: \"{}\"".format(workdir)
|
||||
|
|
@ -168,7 +168,6 @@ class GlobalHostDataHook(PreLaunchHook):
|
|||
"AVALON_TASK": task_name,
|
||||
"AVALON_APP": self.host_name,
|
||||
"AVALON_APP_NAME": self.app_name,
|
||||
"AVALON_HIERARCHY": hierarchy,
|
||||
"AVALON_WORKDIR": workdir
|
||||
}
|
||||
self.log.debug(
|
||||
|
|
@ -180,21 +179,6 @@ class GlobalHostDataHook(PreLaunchHook):
|
|||
|
||||
self.prepare_last_workfile(workdir)
|
||||
|
||||
def _prepare_workdir_data(self, project_doc, asset_doc, task_name):
|
||||
hierarchy = "/".join(asset_doc["data"]["parents"])
|
||||
|
||||
data = {
|
||||
"project": {
|
||||
"name": project_doc["name"],
|
||||
"code": project_doc["data"].get("code")
|
||||
},
|
||||
"task": task_name,
|
||||
"asset": asset_doc["name"],
|
||||
"app": self.host_name,
|
||||
"hierarchy": hierarchy
|
||||
}
|
||||
return data
|
||||
|
||||
def prepare_last_workfile(self, workdir):
|
||||
"""last workfile workflow preparation.
|
||||
|
||||
|
|
|
|||
|
|
@ -13,12 +13,14 @@ self._menu = os.environ.get('PYPE_STUDIO_NAME') or "Pype"
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_menu():
|
||||
def _get_menu(menu_name=None):
|
||||
"""Return the menu instance if it currently exists in Maya"""
|
||||
|
||||
if menu_name is None:
|
||||
menu_name = self._menu
|
||||
widgets = dict((
|
||||
w.objectName(), w) for w in QtWidgets.QApplication.allWidgets())
|
||||
menu = widgets.get(self._menu)
|
||||
menu = widgets.get(menu_name)
|
||||
return menu
|
||||
|
||||
|
||||
|
|
@ -40,10 +42,51 @@ def deferred():
|
|||
command=lambda *args: mayalookassigner.show()
|
||||
)
|
||||
|
||||
def modify_workfiles():
|
||||
from pype.tools import workfiles
|
||||
|
||||
def launch_workfiles_app(*_args, **_kwargs):
|
||||
workfiles.show(
|
||||
os.path.join(
|
||||
cmds.workspace(query=True, rootDirectory=True),
|
||||
cmds.workspace(fileRuleEntry="scene")
|
||||
),
|
||||
parent=pipeline._parent
|
||||
)
|
||||
|
||||
# Find the pipeline menu
|
||||
top_menu = _get_menu(pipeline._menu)
|
||||
|
||||
# Try to find workfile tool action in the menu
|
||||
workfile_action = None
|
||||
for action in top_menu.actions():
|
||||
if action.text() == "Work Files":
|
||||
workfile_action = action
|
||||
break
|
||||
|
||||
# Add at the top of menu if "Work Files" action was not found
|
||||
after_action = ""
|
||||
if workfile_action:
|
||||
# Use action's object name for `insertAfter` argument
|
||||
after_action = workfile_action.objectName()
|
||||
|
||||
# Insert action to menu
|
||||
cmds.menuItem(
|
||||
"Work Files",
|
||||
parent=pipeline._menu,
|
||||
command=launch_workfiles_app,
|
||||
insertAfter=after_action
|
||||
)
|
||||
|
||||
# Remove replaced action
|
||||
if workfile_action:
|
||||
top_menu.removeAction(workfile_action)
|
||||
|
||||
log.info("Attempting to install scripts menu..")
|
||||
|
||||
add_build_workfiles_item()
|
||||
add_look_assigner_item()
|
||||
modify_workfiles()
|
||||
|
||||
try:
|
||||
import scriptsmenu.launchformaya as launchformaya
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import os
|
||||
import nuke
|
||||
from avalon.api import Session
|
||||
|
||||
from pype.hosts.nuke import lib
|
||||
from ...lib import BuildWorkfile
|
||||
from pype.api import Logger
|
||||
from pype.tools import workfiles
|
||||
|
||||
log = Logger().get_logger(__name__)
|
||||
|
||||
|
|
@ -12,6 +14,24 @@ def install():
|
|||
menubar = nuke.menu("Nuke")
|
||||
menu = menubar.findItem(Session["AVALON_LABEL"])
|
||||
workfile_settings = lib.WorkfileSettings
|
||||
|
||||
# replace reset resolution from avalon core to pype's
|
||||
name = "Work Files..."
|
||||
rm_item = [
|
||||
(i, item) for i, item in enumerate(menu.items()) if name in item.name()
|
||||
][0]
|
||||
|
||||
log.debug("Changing Item: {}".format(rm_item))
|
||||
|
||||
menu.removeItem(rm_item[1].name())
|
||||
menu.addCommand(
|
||||
name,
|
||||
lambda: workfiles.show(
|
||||
os.environ["AVALON_WORKDIR"]
|
||||
),
|
||||
index=(rm_item[0])
|
||||
)
|
||||
|
||||
# replace reset resolution from avalon core to pype's
|
||||
name = "Reset Resolution"
|
||||
new_name = "Set Resolution"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,15 @@ from .avalon_context import (
|
|||
get_hierarchy,
|
||||
get_linked_assets,
|
||||
get_latest_version,
|
||||
|
||||
get_workdir_data,
|
||||
get_workdir,
|
||||
get_workdir_with_workdir_data,
|
||||
|
||||
create_workfile_doc,
|
||||
save_workfile_data_to_doc,
|
||||
get_workfile_doc,
|
||||
|
||||
BuildWorkfile
|
||||
)
|
||||
|
||||
|
|
@ -103,6 +112,15 @@ __all__ = [
|
|||
"get_hierarchy",
|
||||
"get_linked_assets",
|
||||
"get_latest_version",
|
||||
|
||||
"get_workdir_data",
|
||||
"get_workdir",
|
||||
"get_workdir_with_workdir_data",
|
||||
|
||||
"create_workfile_doc",
|
||||
"save_workfile_data_to_doc",
|
||||
"get_workfile_doc",
|
||||
|
||||
"BuildWorkfile",
|
||||
|
||||
"ApplicationLaunchFailed",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import os
|
||||
import json
|
||||
import re
|
||||
import copy
|
||||
import logging
|
||||
import collections
|
||||
import functools
|
||||
|
||||
from pype.settings import get_project_settings
|
||||
from .anatomy import Anatomy
|
||||
|
||||
# avalon module is not imported at the top
|
||||
# - may not be in path at the time of pype.lib initialization
|
||||
|
|
@ -246,6 +248,229 @@ def get_latest_version(asset_name, subset_name, dbcon=None, project_name=None):
|
|||
return version_doc
|
||||
|
||||
|
||||
def get_workdir_data(project_doc, asset_doc, task_name, host_name):
|
||||
"""Prepare data for workdir template filling from entered information.
|
||||
|
||||
Args:
|
||||
project_doc (dict): Mongo document of project from MongoDB.
|
||||
asset_doc (dict): Mongo document of asset from MongoDB.
|
||||
task_name (str): Task name for which are workdir data preapred.
|
||||
host_name (str): Host which is used to workdir. This is required
|
||||
because workdir template may contain `{app}` key.
|
||||
|
||||
Returns:
|
||||
dict: Data prepared for filling workdir template.
|
||||
"""
|
||||
hierarchy = "/".join(asset_doc["data"]["parents"])
|
||||
|
||||
data = {
|
||||
"project": {
|
||||
"name": project_doc["name"],
|
||||
"code": project_doc["data"].get("code")
|
||||
},
|
||||
"task": task_name,
|
||||
"asset": asset_doc["name"],
|
||||
"app": host_name,
|
||||
"hierarchy": hierarchy
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
def get_workdir_with_workdir_data(
|
||||
workdir_data, anatomy=None, project_name=None, template_key=None
|
||||
):
|
||||
"""Fill workdir path from entered data and project's anatomy.
|
||||
|
||||
It is possible to pass only project's name instead of project's anatomy but
|
||||
one of them **must** be entered. It is preffered to enter anatomy if is
|
||||
available as initialization of a new Anatomy object may be time consuming.
|
||||
|
||||
Args:
|
||||
workdir_data (dict): Data to fill workdir template.
|
||||
anatomy (Anatomy): Anatomy object for specific project. Optional if
|
||||
`project_name` is entered.
|
||||
project_name (str): Project's name. Optional if `anatomy` is entered
|
||||
otherwise Anatomy object is created with using the project name.
|
||||
template_key (str): Key of work templates in anatomy templates. By
|
||||
default is seto to `"work"`.
|
||||
|
||||
Returns:
|
||||
TemplateResult: Workdir path.
|
||||
|
||||
Raises:
|
||||
ValueError: When both `anatomy` and `project_name` are set to None.
|
||||
"""
|
||||
if not anatomy and not project_name:
|
||||
raise ValueError((
|
||||
"Missing required arguments one of `project_name` or `anatomy`"
|
||||
" must be entered."
|
||||
))
|
||||
|
||||
if not anatomy:
|
||||
anatomy = Anatomy(project_name)
|
||||
|
||||
if not template_key:
|
||||
template_key = "work"
|
||||
|
||||
anatomy_filled = anatomy.format(workdir_data)
|
||||
# Output is TemplateResult object which contain usefull data
|
||||
return anatomy_filled[template_key]["folder"]
|
||||
|
||||
|
||||
def get_workdir(
|
||||
project_doc,
|
||||
asset_doc,
|
||||
task_name,
|
||||
host_name,
|
||||
anatomy=None,
|
||||
template_key=None
|
||||
):
|
||||
"""Fill workdir path from entered data and project's anatomy.
|
||||
|
||||
Args:
|
||||
project_doc (dict): Mongo document of project from MongoDB.
|
||||
asset_doc (dict): Mongo document of asset from MongoDB.
|
||||
task_name (str): Task name for which are workdir data preapred.
|
||||
host_name (str): Host which is used to workdir. This is required
|
||||
because workdir template may contain `{app}` key. In `Session`
|
||||
is stored under `AVALON_APP` key.
|
||||
anatomy (Anatomy): Optional argument. Anatomy object is created using
|
||||
project name from `project_doc`. It is preffered to pass this
|
||||
argument as initialization of a new Anatomy object may be time
|
||||
consuming.
|
||||
template_key (str): Key of work templates in anatomy templates. Default
|
||||
value is defined in `get_workdir_with_workdir_data`.
|
||||
|
||||
Returns:
|
||||
TemplateResult: Workdir path.
|
||||
"""
|
||||
if not anatomy:
|
||||
anatomy = Anatomy(project_doc["name"])
|
||||
|
||||
workdir_data = get_workdir_data(
|
||||
project_doc, asset_doc, task_name, host_name
|
||||
)
|
||||
# Output is TemplateResult object which contain usefull data
|
||||
return get_workdir_with_workdir_data(workdir_data, anatomy, template_key)
|
||||
|
||||
|
||||
@with_avalon
|
||||
def get_workfile_doc(asset_id, task_name, filename, dbcon=None):
|
||||
"""Return workfile document for entered context.
|
||||
|
||||
Do not use this method to get more than one document. In that cases use
|
||||
custom query as this will return documents from database one by one.
|
||||
|
||||
Args:
|
||||
asset_id (ObjectId): Mongo ID of an asset under which workfile belongs.
|
||||
task_name (str): Name of task under which the workfile belongs.
|
||||
filename (str): Name of a workfile.
|
||||
dbcon (AvalonMongoDB): Optionally enter avalon AvalonMongoDB object and
|
||||
`avalon.io` is used if not entered.
|
||||
|
||||
Returns:
|
||||
dict: Workfile document or None.
|
||||
"""
|
||||
# Use avalon.io if dbcon is not entered
|
||||
if not dbcon:
|
||||
dbcon = avalon.io
|
||||
|
||||
return dbcon.find_one({
|
||||
"type": "workfile",
|
||||
"parent": asset_id,
|
||||
"task_name": task_name,
|
||||
"filename": filename
|
||||
})
|
||||
|
||||
|
||||
@with_avalon
|
||||
def create_workfile_doc(asset_doc, task_name, filename, workdir, dbcon=None):
|
||||
"""Creates or replace workfile document in mongo.
|
||||
|
||||
Do not use this method to update data. This method will remove all
|
||||
additional data from existing document.
|
||||
|
||||
Args:
|
||||
asset_doc (dict): Document of asset under which workfile belongs.
|
||||
task_name (str): Name of task for which is workfile related to.
|
||||
filename (str): Filename of workfile.
|
||||
workdir (str): Path to directory where `filename` is located.
|
||||
dbcon (AvalonMongoDB): Optionally enter avalon AvalonMongoDB object and
|
||||
`avalon.io` is used if not entered.
|
||||
"""
|
||||
# Use avalon.io if dbcon is not entered
|
||||
if not dbcon:
|
||||
dbcon = avalon.io
|
||||
|
||||
# Filter of workfile document
|
||||
doc_filter = {
|
||||
"type": "workfile",
|
||||
"parent": asset_doc["_id"],
|
||||
"task_name": task_name,
|
||||
"filename": filename
|
||||
}
|
||||
# Document data are copy of filter
|
||||
doc_data = copy.deepcopy(doc_filter)
|
||||
|
||||
# Prepare project for workdir data
|
||||
project_doc = dbcon.find_one({"type": "project"})
|
||||
workdir_data = get_workdir_data(
|
||||
project_doc, asset_doc, task_name, dbcon.Session["AVALON_APP"]
|
||||
)
|
||||
# Prepare anatomy
|
||||
anatomy = Anatomy(project_doc["name"])
|
||||
# Get workdir path (result is anatomy.TemplateResult)
|
||||
template_workdir = get_workdir_with_workdir_data(workdir_data, anatomy)
|
||||
template_workdir_path = str(template_workdir).replace("\\", "/")
|
||||
|
||||
# Replace slashses in workdir path where workfile is located
|
||||
mod_workdir = workdir.replace("\\", "/")
|
||||
|
||||
# Replace workdir from templates with rootless workdir
|
||||
rootles_workdir = mod_workdir.replace(
|
||||
template_workdir_path,
|
||||
template_workdir.rootless.replace("\\", "/")
|
||||
)
|
||||
|
||||
doc_data["schema"] = "pype:workfile-1.0"
|
||||
doc_data["files"] = ["/".join([rootles_workdir, filename])]
|
||||
doc_data["data"] = {}
|
||||
|
||||
dbcon.replace_one(
|
||||
doc_filter,
|
||||
doc_data,
|
||||
upsert=True
|
||||
)
|
||||
|
||||
|
||||
@with_avalon
|
||||
def save_workfile_data_to_doc(workfile_doc, data, dbcon=None):
|
||||
if not workfile_doc:
|
||||
# TODO add log message
|
||||
return
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
# Use avalon.io if dbcon is not entered
|
||||
if not dbcon:
|
||||
dbcon = avalon.io
|
||||
|
||||
# Convert data to mongo modification keys/values
|
||||
# - this is naive implementation which does not expect nested
|
||||
# dictionaries
|
||||
set_data = {}
|
||||
for key, value in data.items():
|
||||
new_key = "data.{}".format(key)
|
||||
set_data[new_key] = value
|
||||
|
||||
# Update workfile document with data
|
||||
dbcon.update_one(
|
||||
{"_id": workfile_doc["_id"]},
|
||||
{"$set": set_data}
|
||||
)
|
||||
|
||||
|
||||
class BuildWorkfile:
|
||||
"""Wrapper for build workfile process.
|
||||
|
||||
|
|
|
|||
143
pype/tools/workfiles/README.md
Normal file
143
pype/tools/workfiles/README.md
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
# Workfiles App
|
||||
|
||||
The Workfiles app facilitates easy saving, creation and launching of work files.
|
||||
|
||||
The current supported hosts are:
|
||||
|
||||
- Maya
|
||||
- Houdini
|
||||
- Fusion
|
||||
|
||||
The app is available inside hosts via. the ```Avalon > Work Files``` menu.
|
||||
|
||||
## Enabling Workfiles on launch
|
||||
|
||||
By default the Workfiles app will not launch on startup, so it has to be explicitly enabled in a config.
|
||||
|
||||
```python
|
||||
workfiles.show()
|
||||
```
|
||||
|
||||
## Naming Files
|
||||
|
||||
Workfiles app enables user to easily save and create new work files.
|
||||
|
||||
The user is presented with a two parameters; ```version``` and ```comment```. The name of the work file is determined from a template.
|
||||
|
||||
### ```Next Available Version```
|
||||
|
||||
Will search for the next version number that is not in use.
|
||||
|
||||
## Templates
|
||||
|
||||
The default template for work files is ```{task[name]}_v{version:0>4}<_{comment}>```. Launching Maya on an animation task and creating a version 1 will result in ```animation_v0001.ma```. Adding "blocking" to the optional comment input will result in ```animation_v0001_blocking.ma```.
|
||||
|
||||
This template can be customized per project with the ```workfile``` template.
|
||||
|
||||
There are other variables to customize the template with:
|
||||
|
||||
```python
|
||||
{
|
||||
"project": project, # The project data from the database.
|
||||
"asset": asset, # The asset data from the database.
|
||||
"task": {
|
||||
"label": label, # Label of task chosen.
|
||||
"name": name # Sanitize version of the label.
|
||||
},
|
||||
"user": user, # Name of the user on the machine.
|
||||
"version": version, # Chosen version of the user.
|
||||
"comment": comment, # Chosen comment of the user.
|
||||
}
|
||||
```
|
||||
|
||||
### Optional template groups
|
||||
|
||||
The default template contains an optional template group ```<_{comment}>```. If any template group (```{comment}```) within angle bracket ```<>``` does not exist, the whole optional group is discarded.
|
||||
|
||||
|
||||
## Implementing a new host integration for Work Files
|
||||
|
||||
For the Work Files tool to work with a new host integration the host must
|
||||
implement the following functions:
|
||||
|
||||
- `file_extensions()`: The files the host should allow to open and show in the Work Files view.
|
||||
- `open_file(filepath)`: Open a file.
|
||||
- `save_file(filepath)`: Save the current file. This should return None if it failed to save, and return the path if it succeeded
|
||||
- `has_unsaved_changes()`: Return whether the current scene has unsaved changes.
|
||||
- `current_file()`: The path to the current file. None if not saved.
|
||||
- `work_root()`: The path to where the work files for this app should be saved.
|
||||
|
||||
Here's an example code layout:
|
||||
|
||||
```python
|
||||
def file_extensions():
|
||||
"""Return the filename extension formats that should be shown.
|
||||
|
||||
Note:
|
||||
The first entry in the list will be used as the default file
|
||||
format to save to when the current scene is not saved yet.
|
||||
|
||||
Returns:
|
||||
list: A list of the file extensions supported by Work Files.
|
||||
|
||||
"""
|
||||
return list()
|
||||
|
||||
|
||||
def has_unsaved_changes():
|
||||
"""Return whether current file has unsaved modifications."""
|
||||
|
||||
|
||||
def save_file(filepath):
|
||||
"""Save to filepath.
|
||||
|
||||
This should return None if it failed to save, and return the path if it
|
||||
succeeded.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def open_file(filepath):
|
||||
"""Open file"""
|
||||
pass
|
||||
|
||||
|
||||
def current_file():
|
||||
"""Return path to currently open file or None if not saved.
|
||||
|
||||
Returns:
|
||||
str or None: The full path to current file or None when not saved.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def work_root():
|
||||
"""Return the default root for the Host to browse in for Work Files
|
||||
|
||||
Returns:
|
||||
str: The path to look in.
|
||||
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
#### Work Files Scenes root (AVALON_SCENEDIR)
|
||||
|
||||
Whenever the host application has no built-in implementation that defines
|
||||
where scene files should be saved to then the Work Files API for that host
|
||||
should fall back to the `AVALON_SCENEDIR` variable in `api.Session`.
|
||||
|
||||
When `AVALON_SCENEDIR` is set the directory is the relative folder inside the
|
||||
`AVALON_WORKDIR`. Otherwise, when it is not set or empty it should fall back
|
||||
to the Work Directory's root, `AVALON_WORKDIR`
|
||||
|
||||
```python
|
||||
AVALON_WORKDIR="/path/to/work"
|
||||
AVALON_SCENEDIR="scenes"
|
||||
# Result: /path/to/work/scenes
|
||||
|
||||
AVALON_WORKDIR="/path/to/work"
|
||||
AVALON_SCENEDIR=None
|
||||
# Result: /path/to/work
|
||||
```
|
||||
9
pype/tools/workfiles/__init__.py
Normal file
9
pype/tools/workfiles/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from .app import (
|
||||
show,
|
||||
Window
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"show",
|
||||
"Window"
|
||||
]
|
||||
1166
pype/tools/workfiles/app.py
Normal file
1166
pype/tools/workfiles/app.py
Normal file
File diff suppressed because it is too large
Load diff
153
pype/tools/workfiles/model.py
Normal file
153
pype/tools/workfiles/model.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import os
|
||||
import logging
|
||||
|
||||
from Qt import QtCore
|
||||
|
||||
from avalon import style
|
||||
from avalon.vendor import qtawesome
|
||||
from avalon.tools.models import TreeModel, Item
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FilesModel(TreeModel):
|
||||
"""Model listing files with specified extensions in a root folder"""
|
||||
Columns = ["filename", "date"]
|
||||
|
||||
FileNameRole = QtCore.Qt.UserRole + 2
|
||||
DateModifiedRole = QtCore.Qt.UserRole + 3
|
||||
FilePathRole = QtCore.Qt.UserRole + 4
|
||||
IsEnabled = QtCore.Qt.UserRole + 5
|
||||
|
||||
def __init__(self, file_extensions, parent=None):
|
||||
super(FilesModel, self).__init__(parent=parent)
|
||||
|
||||
self._root = None
|
||||
self._file_extensions = file_extensions
|
||||
self._icons = {
|
||||
"file": qtawesome.icon("fa.file-o", color=style.colors.default)
|
||||
}
|
||||
|
||||
def set_root(self, root):
|
||||
self._root = root
|
||||
self.refresh()
|
||||
|
||||
def _add_empty(self):
|
||||
item = Item()
|
||||
item.update({
|
||||
# Put a display message in 'filename'
|
||||
"filename": "No files found.",
|
||||
# Not-selectable
|
||||
"enabled": False,
|
||||
"date": None,
|
||||
"filepath": None
|
||||
})
|
||||
|
||||
self.add_child(item)
|
||||
|
||||
def refresh(self):
|
||||
self.clear()
|
||||
self.beginResetModel()
|
||||
|
||||
root = self._root
|
||||
|
||||
if not root:
|
||||
self.endResetModel()
|
||||
return
|
||||
|
||||
if not os.path.exists(root):
|
||||
# Add Work Area does not exist placeholder
|
||||
log.debug("Work Area does not exist: %s", root)
|
||||
message = "Work Area does not exist. Use Save As to create it."
|
||||
item = Item({
|
||||
"filename": message,
|
||||
"date": None,
|
||||
"filepath": None,
|
||||
"enabled": False,
|
||||
"icon": qtawesome.icon("fa.times", color=style.colors.mid)
|
||||
})
|
||||
self.add_child(item)
|
||||
self.endResetModel()
|
||||
return
|
||||
|
||||
extensions = self._file_extensions
|
||||
|
||||
for filename in os.listdir(root):
|
||||
path = os.path.join(root, filename)
|
||||
if os.path.isdir(path):
|
||||
continue
|
||||
|
||||
ext = os.path.splitext(filename)[1]
|
||||
if extensions and ext not in extensions:
|
||||
continue
|
||||
|
||||
modified = os.path.getmtime(path)
|
||||
|
||||
item = Item({
|
||||
"filename": filename,
|
||||
"date": modified,
|
||||
"filepath": path
|
||||
})
|
||||
|
||||
self.add_child(item)
|
||||
|
||||
if self.rowCount() == 0:
|
||||
self._add_empty()
|
||||
|
||||
self.endResetModel()
|
||||
|
||||
def has_filenames(self):
|
||||
for item in self._root_item.children():
|
||||
if item.get("enabled", True):
|
||||
return True
|
||||
return False
|
||||
|
||||
def rowCount(self, parent=None):
|
||||
if parent is None or not parent.isValid():
|
||||
parent_item = self._root_item
|
||||
else:
|
||||
parent_item = parent.internalPointer()
|
||||
return parent_item.childCount()
|
||||
|
||||
def data(self, index, role):
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
# Add icon to filename column
|
||||
item = index.internalPointer()
|
||||
if index.column() == 0:
|
||||
if item["filepath"]:
|
||||
return self._icons["file"]
|
||||
return item.get("icon", None)
|
||||
|
||||
if role == self.FileNameRole:
|
||||
item = index.internalPointer()
|
||||
return item["filename"]
|
||||
|
||||
if role == self.DateModifiedRole:
|
||||
item = index.internalPointer()
|
||||
return item["date"]
|
||||
|
||||
if role == self.FilePathRole:
|
||||
item = index.internalPointer()
|
||||
return item["filepath"]
|
||||
|
||||
if role == self.IsEnabled:
|
||||
item = index.internalPointer()
|
||||
return item.get("enabled", True)
|
||||
|
||||
return super(FilesModel, self).data(index, role)
|
||||
|
||||
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 "Name"
|
||||
elif section == 1:
|
||||
return "Date modified"
|
||||
|
||||
return super(FilesModel, self).headerData(section, orientation, role)
|
||||
15
pype/tools/workfiles/view.py
Normal file
15
pype/tools/workfiles/view.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
from Qt import QtWidgets, QtCore
|
||||
|
||||
|
||||
class FilesView(QtWidgets.QTreeView):
|
||||
doubleClickedLeft = QtCore.Signal()
|
||||
doubleClickedRight = QtCore.Signal()
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self.doubleClickedLeft.emit()
|
||||
|
||||
elif event.button() == QtCore.Qt.RightButton:
|
||||
self.doubleClickedRight.emit()
|
||||
|
||||
return super(FilesView, self).mouseDoubleClickEvent(event)
|
||||
52
schema/workfile-1.0.json
Normal file
52
schema/workfile-1.0.json
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
|
||||
"title": "pype:workfile-1.0",
|
||||
"description": "Workfile additional information.",
|
||||
|
||||
"type": "object",
|
||||
|
||||
"additionalProperties": true,
|
||||
|
||||
"required": [
|
||||
"schema",
|
||||
"type",
|
||||
"filename",
|
||||
"task_name",
|
||||
"parent"
|
||||
],
|
||||
|
||||
"properties": {
|
||||
"schema": {
|
||||
"description": "Schema identifier for payload",
|
||||
"type": "string",
|
||||
"enum": ["pype:workfile-1.0"],
|
||||
"example": "pype:workfile-1.0"
|
||||
},
|
||||
"type": {
|
||||
"description": "The type of document",
|
||||
"type": "string",
|
||||
"enum": ["workfile"],
|
||||
"example": "workfile"
|
||||
},
|
||||
"parent": {
|
||||
"description": "Unique identifier to parent document",
|
||||
"example": "592c33475f8c1b064c4d1696"
|
||||
},
|
||||
"filename": {
|
||||
"description": "Workfile's filename",
|
||||
"type": "string",
|
||||
"example": "kuba_each_case_Alpaca_01_animation_v001.ma"
|
||||
},
|
||||
"task_name": {
|
||||
"description": "Task name",
|
||||
"type": "string",
|
||||
"example": "animation"
|
||||
},
|
||||
"data": {
|
||||
"description": "Document metadata",
|
||||
"type": "object",
|
||||
"example": {"key": "value"}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue