copied workfiles tool to pype tools

This commit is contained in:
iLLiCiTiT 2021-01-05 19:23:57 +01:00
parent e5d39665d8
commit 6e95f79dcf
5 changed files with 1193 additions and 0 deletions

View 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
```

View file

@ -0,0 +1,7 @@
from .app import (
show
)
__all__ = [
"show"
]

893
pype/tools/workfiles/app.py Normal file
View file

@ -0,0 +1,893 @@
import sys
import os
import copy
import getpass
import shutil
import logging
from ...vendor import Qt
from ...vendor.Qt import QtWidgets, QtCore
from ... import style, io, api, pipeline
from .. import lib as tools_lib
from ..widgets import AssetWidget
from ..models import TasksModel
from ..delegates import PrettyTimeDelegate
from .model import FilesModel
from .view import FilesView
from pype.api import Anatomy
log = logging.getLogger(__name__)
module = sys.modules[__name__]
module.window = None
class NameWindow(QtWidgets.QDialog):
"""Name Window to define a unique filename inside a root folder
The filename will be based on the "workfile" template defined in the
project["config"]["template"].
"""
def __init__(self, parent, root, session=None):
super(NameWindow, self).__init__(parent=parent)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint)
self.result = None
self.host = api.registered_host()
self.root = root
self.work_file = None
if session is None:
# Fallback to active session
session = api.Session
# Set work file data for template formatting
project = io.find_one({
"type": "project"
})
self.data = {
"project": {
"name": project["name"],
"code": project["data"].get("code")
},
"asset": session["AVALON_ASSET"],
"task": session["AVALON_TASK"],
"version": 1,
"user": getpass.getuser(),
"comment": ""
}
# Define work files template
anatomy = Anatomy(project["name"])
self.template = anatomy.templates["work"]["file"]
self.widgets = {
"preview": QtWidgets.QLabel("Preview filename"),
"comment": QtWidgets.QLineEdit(),
"version": QtWidgets.QWidget(),
"versionValue": QtWidgets.QSpinBox(),
"versionCheck": QtWidgets.QCheckBox("Next Available Version"),
"inputs": QtWidgets.QWidget(),
"buttons": QtWidgets.QWidget(),
"okButton": QtWidgets.QPushButton("Ok"),
"cancelButton": QtWidgets.QPushButton("Cancel")
}
# Build version
self.widgets["versionValue"].setMinimum(1)
self.widgets["versionValue"].setMaximum(9999)
self.widgets["versionCheck"].setCheckState(QtCore.Qt.CheckState(2))
layout = QtWidgets.QHBoxLayout(self.widgets["version"])
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.widgets["versionValue"])
layout.addWidget(self.widgets["versionCheck"])
# Build buttons
layout = QtWidgets.QHBoxLayout(self.widgets["buttons"])
layout.addWidget(self.widgets["okButton"])
layout.addWidget(self.widgets["cancelButton"])
# Build inputs
layout = QtWidgets.QFormLayout(self.widgets["inputs"])
layout.addRow("Version:", self.widgets["version"])
layout.addRow("Comment:", self.widgets["comment"])
layout.addRow("Preview:", self.widgets["preview"])
# Build layout
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.widgets["inputs"])
layout.addWidget(self.widgets["buttons"])
self.widgets["versionValue"].valueChanged.connect(
self.on_version_spinbox_changed
)
self.widgets["versionCheck"].stateChanged.connect(
self.on_version_checkbox_changed
)
self.widgets["comment"].textChanged.connect(self.on_comment_changed)
self.widgets["okButton"].pressed.connect(self.on_ok_pressed)
self.widgets["cancelButton"].pressed.connect(self.on_cancel_pressed)
# Allow "Enter" key to accept the save.
self.widgets["okButton"].setDefault(True)
# Force default focus to comment, some hosts didn't automatically
# apply focus to this line edit (e.g. Houdini)
self.widgets["comment"].setFocus()
self.refresh()
def on_version_spinbox_changed(self, value):
self.data["version"] = value
self.refresh()
def on_version_checkbox_changed(self, value):
self.refresh()
def on_comment_changed(self, text):
self.data["comment"] = text
self.refresh()
def on_ok_pressed(self):
self.result = self.work_file.replace("\\", "/")
self.close()
def on_cancel_pressed(self):
self.close()
def get_result(self):
return self.result
def get_work_file(self, template=None):
data = copy.deepcopy(self.data)
template = template or self.template
# Define saving file extension
current_file = self.host.current_file()
if current_file:
# Match the extension of current file
_, extension = os.path.splitext(current_file)
else:
# Fall back to the first extension supported for this host.
extension = self.host.file_extensions()[0]
data["ext"] = extension
if not data["comment"]:
data.pop("comment", None)
return api.format_template_with_optional_keys(data, template)
def refresh(self):
# Since the version can be padded with "{version:0>4}" we only search
# for "{version".
if "{version" not in self.template:
# todo: hide the full row
self.widgets["version"].setVisible(False)
# Build comment
if "{comment}" not in self.template:
# todo: hide the full row
self.widgets["comment"].setVisible(False)
if self.widgets["versionCheck"].isChecked():
self.widgets["versionValue"].setEnabled(False)
extensions = self.host.file_extensions()
data = copy.deepcopy(self.data)
template = str(self.template)
if not data["comment"]:
data.pop("comment", None)
version = api.last_workfile_with_version(
self.root, template, data, extensions
)[1]
if version is None:
version = 1
else:
version += 1
self.data["version"] = version
# safety check
path = os.path.join(self.root, self.get_work_file())
assert not os.path.exists(path), \
"This is a bug, file exists: %s" % path
else:
self.widgets["versionValue"].setEnabled(True)
self.data["version"] = self.widgets["versionValue"].value()
self.work_file = self.get_work_file()
preview = self.widgets["preview"]
ok = self.widgets["okButton"]
preview.setText(
"<font color='green'>{0}</font>".format(self.work_file)
)
if os.path.exists(os.path.join(self.root, self.work_file)):
preview.setText(
"<font color='red'>Cannot create \"{0}\" because file exists!"
"</font>".format(self.work_file)
)
ok.setEnabled(False)
else:
ok.setEnabled(True)
class TasksWidget(QtWidgets.QWidget):
"""Widget showing active Tasks"""
task_changed = QtCore.Signal()
def __init__(self, parent=None):
super(TasksWidget, self).__init__(parent)
self.setContentsMargins(0, 0, 0, 0)
view = QtWidgets.QTreeView()
view.setIndentation(0)
model = TasksModel(io)
view.setModel(model)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(view)
# Hide the default tasks "count" as we don't need that data here.
view.setColumnHidden(1, True)
selection = view.selectionModel()
selection.currentChanged.connect(self.task_changed)
self.models = {
"tasks": model
}
self.widgets = {
"view": view,
}
self._last_selected_task = None
def set_asset(self, asset):
if asset is None:
# Asset deselected
return
# Try and preserve the last selected task and reselect it
# after switching assets. If there's no currently selected
# asset keep whatever the "last selected" was prior to it.
current = self.get_current_task()
if current:
self._last_selected_task = current
self.models["tasks"].set_assets(asset_docs=[asset])
if self._last_selected_task:
self.select_task(self._last_selected_task)
# Force a task changed emit.
self.task_changed.emit()
def select_task(self, task):
"""Select a task by name.
If the task does not exist in the current model then selection is only
cleared.
Args:
task (str): Name of the task to select.
"""
# Clear selection
view = self.widgets["view"]
model = view.model()
selection_model = view.selectionModel()
selection_model.clearSelection()
# Select the task
mode = selection_model.Select | selection_model.Rows
for row in range(model.rowCount(QtCore.QModelIndex())):
index = model.index(row, 0, QtCore.QModelIndex())
name = index.data(QtCore.Qt.DisplayRole)
if name == task:
selection_model.select(index, mode)
# Set the currently active index
view.setCurrentIndex(index)
def get_current_task(self):
"""Return name of task at current index (selected)
Returns:
str: Name of the current task.
"""
view = self.widgets["view"]
index = view.currentIndex()
index = index.sibling(index.row(), 0) # ensure column zero for name
selection = view.selectionModel()
if selection.isSelected(index):
# Ignore when the current task is not selected as the "No task"
# placeholder might be the current index even though it's
# disallowed to be selected. So we only return if it is selected.
return index.data(QtCore.Qt.DisplayRole)
class FilesWidget(QtWidgets.QWidget):
"""A widget displaying files that allows to save and open files."""
def __init__(self, parent=None):
super(FilesWidget, self).__init__(parent=parent)
# Setup
self._asset = None
self._task = None
self.root = None
self.host = api.registered_host()
# Whether to automatically select the latest modified
# file on a refresh of the files model.
self.auto_select_latest_modified = True
# Avoid crash in Blender and store the message box
# (setting parent doesn't work as it hides the message box)
self._messagebox = None
widgets = {
"filter": QtWidgets.QLineEdit(),
"list": FilesView(),
"open": QtWidgets.QPushButton("Open"),
"browse": QtWidgets.QPushButton("Browse"),
"save": QtWidgets.QPushButton("Save As")
}
delegates = {
"time": PrettyTimeDelegate()
}
# Create the files model
extensions = set(self.host.file_extensions())
self.model = FilesModel(file_extensions=extensions)
self.proxy = QtCore.QSortFilterProxyModel()
self.proxy.setSourceModel(self.model)
self.proxy.setDynamicSortFilter(True)
self.proxy.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
# Set up the file list tree view
widgets["list"].setModel(self.proxy)
widgets["list"].setSortingEnabled(True)
widgets["list"].setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
# Date modified delegate
widgets["list"].setItemDelegateForColumn(1, delegates["time"])
widgets["list"].setIndentation(3) # smaller indentation
# Default to a wider first filename column it is what we mostly care
# about and the date modified is relatively small anyway.
widgets["list"].setColumnWidth(0, 330)
widgets["filter"].textChanged.connect(self.proxy.setFilterFixedString)
widgets["filter"].setPlaceholderText("Filter files..")
# Home Page
# Build buttons widget for files widget
buttons = QtWidgets.QWidget()
layout = QtWidgets.QHBoxLayout(buttons)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(widgets["open"])
layout.addWidget(widgets["browse"])
layout.addWidget(widgets["save"])
# Build files widgets for home page
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(widgets["filter"])
layout.addWidget(widgets["list"])
layout.addWidget(buttons)
widgets["list"].doubleClickedLeft.connect(self.on_open_pressed)
widgets["list"].customContextMenuRequested.connect(
self.on_context_menu
)
widgets["open"].pressed.connect(self.on_open_pressed)
widgets["browse"].pressed.connect(self.on_browse_pressed)
widgets["save"].pressed.connect(self.on_save_as_pressed)
self.widgets = widgets
self.delegates = delegates
def set_asset_task(self, asset, task):
self._asset = asset
self._task = task
# Define a custom session so we can query the work root
# for a "Work area" that is not our current Session.
# This way we can browse it even before we enter it.
if self._asset and self._task:
session = self._get_session()
self.root = self.host.work_root(session)
exists = os.path.exists(self.root)
self.widgets["browse"].setEnabled(exists)
self.widgets["open"].setEnabled(exists)
self.model.set_root(self.root)
else:
self.model.set_root(None)
def _get_session(self):
"""Return a modified session for the current asset and task"""
session = api.Session.copy()
changes = pipeline.compute_session_changes(session,
asset=self._asset,
task=self._task)
session.update(changes)
return session
def _enter_session(self):
"""Enter the asset and task session currently selected"""
session = api.Session.copy()
changes = pipeline.compute_session_changes(session,
asset=self._asset,
task=self._task)
if not changes:
# Return early if we're already in the right Session context
# to avoid any unwanted Task Changed callbacks to be triggered.
return
api.update_current_task(asset=self._asset, task=self._task)
def open_file(self, filepath):
host = self.host
if host.has_unsaved_changes():
result = self.save_changes_prompt()
if result is None:
# Cancel operation
return False
if result:
current_file = host.current_file()
if not current_file:
# If the user requested to save the current scene
# we can't actually automatically do so if the current
# file has not been saved with a name yet. So we'll have
# to opt out.
log.error("Can't save scene with no filename. Please "
"first save your work file using 'Save As'.")
return
# Save current scene, continue to open file
host.save_file(current_file)
else:
# Don't save, continue to open file
pass
self._enter_session()
host.open_file(filepath)
self.window().close()
def save_changes_prompt(self):
self._messagebox = QtWidgets.QMessageBox()
messagebox = self._messagebox
messagebox.setWindowFlags(QtCore.Qt.FramelessWindowHint)
messagebox.setIcon(messagebox.Warning)
messagebox.setWindowTitle("Unsaved Changes!")
messagebox.setText(
"There are unsaved changes to the current file."
"\nDo you want to save the changes?"
)
messagebox.setStandardButtons(
messagebox.Yes | messagebox.No | messagebox.Cancel
)
# Parenting the QMessageBox to the Widget seems to crash
# so we skip parenting and explicitly apply the stylesheet.
messagebox.setStyleSheet(style.load_stylesheet())
result = messagebox.exec_()
if result == messagebox.Yes:
return True
elif result == messagebox.No:
return False
else:
return None
def get_filename(self):
"""Show save dialog to define filename for save or duplicate
Returns:
str: The filename to create.
"""
session = self._get_session()
window = NameWindow(parent=self,
root=self.root,
session=session)
window.exec_()
return window.get_result()
def on_duplicate_pressed(self):
work_file = self.get_filename()
if not work_file:
return
src = self._get_selected_filepath()
dst = os.path.join(
self.root, work_file
)
shutil.copy(src, dst)
self.refresh()
def _get_selected_filepath(self):
"""Return current filepath selected in view"""
model = self.model
view = self.widgets["list"]
selection = view.selectionModel()
index = selection.currentIndex()
if not index.isValid():
return
return index.data(model.FilePathRole)
def on_open_pressed(self):
path = self._get_selected_filepath()
if not path:
print("No file selected to open..")
return
self.open_file(path)
def on_browse_pressed(self):
filter = " *".join(self.host.file_extensions())
filter = "Work File (*{0})".format(filter)
kwargs = {
"caption": "Work Files",
"filter": filter
}
if Qt.__binding__ in ("PySide", "PySide2"):
kwargs["dir"] = self.root
else:
kwargs["directory"] = self.root
work_file = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
if not work_file:
return
self.open_file(work_file)
def on_save_as_pressed(self):
work_file = self.get_filename()
if not work_file:
return
# Initialize work directory if it has not been initialized before
if not os.path.exists(self.root):
log.debug("Initializing Work Directory: %s", self.root)
self.initialize_work_directory()
if not os.path.exists(self.root):
# Failed to initialize Work Directory
log.error("Failed to initialize Work Directory: "
"%s", self.root)
return
file_path = os.path.join(self.root, work_file)
self._enter_session() # Make sure we are in the right session
self.host.save_file(file_path)
self.set_asset_task(self._asset, self._task)
self.refresh()
def initialize_work_directory(self):
"""Initialize Work Directory.
This is used when the Work Directory does not exist yet.
This finds the current AVALON_APP_NAME and tries to triggers its
`.toml` initialization step. Note that this will only be valid
whenever `AVALON_APP_NAME` is actually set in the current session.
"""
# Inputs (from the switched session and running app)
session = api.Session.copy()
changes = pipeline.compute_session_changes(session,
asset=self._asset,
task=self._task)
session.update(changes)
# Find the application definition
app_name = os.environ.get("AVALON_APP_NAME")
if not app_name:
log.error("No AVALON_APP_NAME session variable is set. "
"Unable to initialize app Work Directory.")
return
app_definition = pipeline.lib.get_application(app_name)
App = type("app_%s" % app_name,
(pipeline.Application,),
{"config": app_definition.copy()})
# Initialize within the new session's environment
app = App()
env = app.environ(session)
app.initialize(env)
# Force a full to the asset as opposed to just self.refresh() so
# that it will actually check again whether the Work directory exists
self.set_asset_task(self._asset, self._task)
def refresh(self):
"""Refresh listed files for current selection in the interface"""
self.model.refresh()
if self.auto_select_latest_modified:
tools_lib.schedule(self._select_last_modified_file,
100)
def on_context_menu(self, point):
view = self.widgets["list"]
index = view.indexAt(point)
if not index.isValid():
return
is_enabled = index.data(FilesModel.IsEnabled)
if not is_enabled:
return
menu = QtWidgets.QMenu(self)
# Duplicate
action = QtWidgets.QAction("Duplicate", menu)
tip = "Duplicate selected file."
action.setToolTip(tip)
action.setStatusTip(tip)
action.triggered.connect(self.on_duplicate_pressed)
menu.addAction(action)
# Show the context action menu
global_point = view.mapToGlobal(point)
action = menu.exec_(global_point)
if not action:
return
def _select_last_modified_file(self):
"""Utility function to select the file with latest date modified"""
role = self.model.DateModifiedRole
view = self.widgets["list"]
model = view.model()
highest_index = None
highest = 0
for row in range(model.rowCount()):
index = model.index(row, 0, parent=QtCore.QModelIndex())
if not index.isValid():
continue
modified = index.data(role)
if modified is not None and modified > highest:
highest_index = index
highest = modified
if highest_index:
view.setCurrentIndex(highest_index)
class Window(QtWidgets.QMainWindow):
"""Work Files Window"""
title = "Work Files"
def __init__(self, parent=None):
super(Window, self).__init__(parent=parent)
self.setWindowTitle(self.title)
self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.WindowCloseButtonHint)
pages = {
"home": QtWidgets.QWidget()
}
widgets = {
"pages": QtWidgets.QStackedWidget(),
"body": QtWidgets.QWidget(),
"assets": AssetWidget(io),
"tasks": TasksWidget(),
"files": FilesWidget()
}
self.setCentralWidget(widgets["pages"])
widgets["pages"].addWidget(pages["home"])
# Build home
layout = QtWidgets.QVBoxLayout(pages["home"])
layout.addWidget(widgets["body"])
# Build home - body
layout = QtWidgets.QVBoxLayout(widgets["body"])
split = QtWidgets.QSplitter()
split.addWidget(widgets["assets"])
split.addWidget(widgets["tasks"])
split.addWidget(widgets["files"])
split.setStretchFactor(0, 1)
split.setStretchFactor(1, 1)
split.setStretchFactor(2, 3)
layout.addWidget(split)
# Add top margin for tasks to align it visually with files as
# the files widget has a filter field which tasks does not.
widgets["tasks"].setContentsMargins(0, 32, 0, 0)
# Connect signals
widgets["assets"].current_changed.connect(self.on_asset_changed)
widgets["tasks"].task_changed.connect(self.on_task_changed)
self.widgets = widgets
self.refresh()
# Force focus on the open button by default, required for Houdini.
self.widgets["files"].widgets["open"].setFocus()
self.resize(900, 600)
def keyPressEvent(self, event):
"""Custom keyPressEvent.
Override keyPressEvent to do nothing so that Maya's panels won't
take focus when pressing "SHIFT" whilst mouse is over viewport or
outliner. This way users don't accidently perform Maya commands
whilst trying to name an instance.
"""
def on_task_changed(self):
# Since we query the disk give it slightly more delay
tools_lib.schedule(self._on_task_changed, 100, channel="mongo")
def on_asset_changed(self):
tools_lib.schedule(self._on_asset_changed, 50, channel="mongo")
def set_context(self, context):
if "asset" in context:
asset = context["asset"]
asset_document = io.find_one(
{
"name": asset,
"type": "asset"
},
{
"data.tasks": 1
}
)
# Select the asset
self.widgets["assets"].select_assets([asset], expand=True)
# Force a refresh on Tasks?
self.widgets["tasks"].set_asset(asset_document)
if "task" in context:
self.widgets["tasks"].select_task(context["task"])
def refresh(self):
# Refresh asset widget
self.widgets["assets"].refresh()
self._on_task_changed()
def _on_asset_changed(self):
asset = self.widgets["assets"].get_selected_assets() or None
if not asset:
# Force disable the other widgets if no
# active selection
self.widgets["tasks"].setEnabled(False)
self.widgets["files"].setEnabled(False)
else:
asset = asset[0]
self.widgets["tasks"].setEnabled(True)
self.widgets["tasks"].set_asset(asset)
def _on_task_changed(self):
asset = self.widgets["assets"].get_selected_assets() or None
if asset is not None:
asset = asset[0]
task = self.widgets["tasks"].get_current_task()
self.widgets["tasks"].setEnabled(bool(asset))
self.widgets["files"].setEnabled(all([bool(task), bool(asset)]))
files = self.widgets["files"]
files.set_asset_task(asset, task)
files.refresh()
def validate_host_requirements(host):
if host is None:
raise RuntimeError("No registered host.")
# Verify the host has implemented the api for Work Files
required = [
"open_file",
"save_file",
"current_file",
"has_unsaved_changes",
"work_root",
"file_extensions",
]
missing = []
for name in required:
if not hasattr(host, name):
missing.append(name)
if missing:
raise RuntimeError(
"Host is missing required Work Files interfaces: "
"%s (host: %s)" % (", ".join(missing), host)
)
return True
def show(root=None, debug=False, parent=None, use_context=True, save=True):
"""Show Work Files GUI"""
# todo: remove `root` argument to show()
try:
module.window.close()
del(module.window)
except (AttributeError, RuntimeError):
pass
host = api.registered_host()
validate_host_requirements(host)
if debug:
api.Session["AVALON_ASSET"] = "Mock"
api.Session["AVALON_TASK"] = "Testing"
with tools_lib.application():
window = Window(parent=parent)
window.refresh()
if use_context:
context = {"asset": api.Session["AVALON_ASSET"],
"silo": api.Session["AVALON_SILO"],
"task": api.Session["AVALON_TASK"]}
window.set_context(context)
window.widgets["files"].widgets["save"].setEnabled(save)
window.show()
window.setStyleSheet(style.load_stylesheet())
module.window = window
# Pull window to the front.
module.window.raise_()
module.window.activateWindow()

View file

@ -0,0 +1,134 @@
import os
import logging
from ... import style
from ...vendor.Qt import QtCore
from ...vendor import qtawesome
from ..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,
"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 f in os.listdir(root):
path = os.path.join(root, f)
if os.path.isdir(path):
continue
if extensions and os.path.splitext(f)[1] not in extensions:
continue
modified = os.path.getmtime(path)
item = Item({
"filename": f,
"date": modified,
"filepath": path
})
self.add_child(item)
self.endResetModel()
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"]
else:
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)

View file

@ -0,0 +1,16 @@
from ...vendor.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)