separated files widget and Window into separated files

This commit is contained in:
Jakub Trllo 2022-03-21 11:03:33 +01:00
parent 30fe1b30a2
commit 6dbb48d4e6
5 changed files with 787 additions and 771 deletions

View file

@ -1,9 +1,12 @@
from .window import Window
from .app import (
show,
Window
validate_host_requirements,
)
__all__ = [
"Window",
"show",
"Window"
"validate_host_requirements",
]

View file

@ -1,41 +1,10 @@
import sys
import os
import shutil
import logging
import datetime
import Qt
from Qt import QtWidgets, QtCore
from avalon import io, api
from avalon import api
from openpype import style
from openpype.tools.utils.lib import (
qt_app_context
)
from openpype.tools.utils import PlaceholderLineEdit
from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget
from openpype.tools.utils.tasks_widget import TasksWidget
from openpype.tools.utils.delegates import PrettyTimeDelegate
from openpype.lib import (
emit_event,
Anatomy,
get_workfile_doc,
create_workfile_doc,
save_workfile_data_to_doc,
get_workfile_template_key,
create_workdir_extra_folders,
)
from openpype.lib.avalon_context import (
update_current_task,
compute_session_changes
)
from .model import (
WorkAreaFilesModel,
FILEPATH_ROLE,
DATE_MODIFIED_ROLE,
)
from .save_as_dialog import SaveAsDialog
from .view import FilesView
from openpype.tools.utils import qt_app_context
from .window import Window
log = logging.getLogger(__name__)
@ -43,726 +12,6 @@ module = sys.modules[__name__]
module.window = None
class FilesWidget(QtWidgets.QWidget):
"""A widget displaying files that allows to save and open files."""
file_selected = QtCore.Signal(str)
workfile_created = QtCore.Signal(str)
file_opened = QtCore.Signal()
def __init__(self, parent=None):
super(FilesWidget, self).__init__(parent=parent)
# Setup
self._asset_id = None
self._asset_doc = None
self._task_name = None
self._task_type = None
# Pype's anatomy object for current project
self.anatomy = Anatomy(io.Session["AVALON_PROJECT"])
# Template key used to get work template from anatomy templates
self.template_key = "work"
# This is not root but workfile directory
self._workfiles_root = None
self._workdir_path = 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
files_view = FilesView(self)
# Create the Files model
extensions = set(self.host.file_extensions())
files_model = WorkAreaFilesModel(extensions)
# Create proxy model for files to be able sort and filter
proxy_model = QtCore.QSortFilterProxyModel()
proxy_model.setSourceModel(files_model)
proxy_model.setDynamicSortFilter(True)
proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
# Set up the file list tree view
files_view.setModel(proxy_model)
files_view.setSortingEnabled(True)
files_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
# Date modified delegate
time_delegate = PrettyTimeDelegate()
files_view.setItemDelegateForColumn(1, time_delegate)
files_view.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.
files_view.setColumnWidth(0, 330)
# Filtering input
filter_input = PlaceholderLineEdit(self)
filter_input.setPlaceholderText("Filter files..")
filter_input.textChanged.connect(proxy_model.setFilterFixedString)
# Home Page
# Build buttons widget for files widget
btns_widget = QtWidgets.QWidget(self)
btn_save = QtWidgets.QPushButton("Save As", btns_widget)
btn_browse = QtWidgets.QPushButton("Browse", btns_widget)
btn_open = QtWidgets.QPushButton("Open", btns_widget)
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
btns_layout.setContentsMargins(0, 0, 0, 0)
btns_layout.addWidget(btn_open)
btns_layout.addWidget(btn_browse)
btns_layout.addWidget(btn_save)
# Build files widgets for home page
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(filter_input)
main_layout.addWidget(files_view)
main_layout.addWidget(btns_widget)
# Register signal callbacks
files_view.doubleClickedLeft.connect(self.on_open_pressed)
files_view.customContextMenuRequested.connect(self.on_context_menu)
files_view.selectionModel().selectionChanged.connect(
self.on_file_select
)
btn_open.pressed.connect(self.on_open_pressed)
btn_browse.pressed.connect(self.on_browse_pressed)
btn_save.pressed.connect(self.on_save_as_pressed)
# Store attributes
self.time_delegate = time_delegate
self.filter_input = filter_input
self.files_view = files_view
self.files_model = files_model
self.btns_widget = btns_widget
self.btn_open = btn_open
self.btn_browse = btn_browse
self.btn_save = btn_save
def set_asset_task(self, asset_id, task_name, task_type):
if asset_id != self._asset_id:
self._asset_doc = None
self._asset_id = asset_id
self._task_name = task_name
self._task_type = task_type
# 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_id and self._task_name and self._task_type:
session = self._get_session()
self._workdir_path = session["AVALON_WORKDIR"]
self._workfiles_root = self.host.work_root(session)
self.files_model.set_root(self._workfiles_root)
else:
self.files_model.set_root(None)
# Disable/Enable buttons based on available files in model
has_valid_items = self.files_model.has_valid_items()
self.btn_browse.setEnabled(has_valid_items)
self.btn_open.setEnabled(has_valid_items)
if not has_valid_items:
# Manually trigger file selection
self.on_file_select()
def _get_asset_doc(self):
if self._asset_id is None:
return None
if self._asset_doc is None:
self._asset_doc = io.find_one({"_id": self._asset_id})
return self._asset_doc
def _get_session(self):
"""Return a modified session for the current asset and task"""
session = api.Session.copy()
self.template_key = get_workfile_template_key(
self._task_type,
session["AVALON_APP"],
project_name=session["AVALON_PROJECT"]
)
changes = compute_session_changes(
session,
asset=self._get_asset_doc(),
task=self._task_name,
template_key=self.template_key
)
session.update(changes)
return session
def _enter_session(self):
"""Enter the asset and task session currently selected"""
session = api.Session.copy()
changes = compute_session_changes(
session,
asset=self._get_asset_doc(),
task=self._task_name,
template_key=self.template_key
)
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
update_current_task(
asset=self._get_asset_doc(),
task=self._task_name,
template_key=self.template_key
)
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
# Save first if has changes
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)
self._enter_session()
host.open_file(filepath)
self.file_opened.emit()
def save_changes_prompt(self):
self._messagebox = messagebox = QtWidgets.QMessageBox(parent=self)
messagebox.setWindowFlags(messagebox.windowFlags() |
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
)
result = messagebox.exec_()
if result == messagebox.Yes:
return True
if result == messagebox.No:
return False
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 = SaveAsDialog(
parent=self,
root=self._workfiles_root,
anatomy=self.anatomy,
template_key=self.template_key,
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._workfiles_root, work_file)
shutil.copy(src, dst)
self.workfile_created.emit(dst)
self.refresh()
def _get_selected_filepath(self):
"""Return current filepath selected in view"""
selection = self.files_view.selectionModel()
index = selection.currentIndex()
if not index.isValid():
return
return index.data(FILEPATH_ROLE)
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):
ext_filter = "Work File (*{0})".format(
" *".join(self.host.file_extensions())
)
kwargs = {
"caption": "Work Files",
"filter": ext_filter
}
if Qt.__binding__ in ("PySide", "PySide2"):
kwargs["dir"] = self._workfiles_root
else:
kwargs["directory"] = self._workfiles_root
work_file = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
if work_file:
self.open_file(work_file)
def on_save_as_pressed(self):
work_filename = self.get_filename()
if not work_filename:
return
# Trigger before save event
emit_event(
"workfile.save.before",
{"filename": work_filename, "workdir_path": self._workdir_path},
source="workfiles.tool"
)
# Make sure workfiles root is updated
# - this triggers 'workio.work_root(...)' which may change value of
# '_workfiles_root'
self.set_asset_task(
self._asset_id, self._task_name, self._task_type
)
# Create workfiles root folder
if not os.path.exists(self._workfiles_root):
log.debug("Initializing Work Directory: %s", self._workfiles_root)
os.makedirs(self._workfiles_root)
# Update session if context has changed
self._enter_session()
# Prepare full path to workfile and save it
filepath = os.path.join(
os.path.normpath(self._workfiles_root), work_filename
)
self.host.save_file(filepath)
# Create extra folders
create_workdir_extra_folders(
self._workdir_path,
api.Session["AVALON_APP"],
self._task_type,
self._task_name,
api.Session["AVALON_PROJECT"]
)
# Trigger after save events
emit_event(
"workfile.save.after",
{"filename": work_filename, "workdir_path": self._workdir_path},
source="workfiles.tool"
)
self.workfile_created.emit(filepath)
# Refresh files model
self.refresh()
def on_file_select(self):
self.file_selected.emit(self._get_selected_filepath())
def refresh(self):
"""Refresh listed files for current selection in the interface"""
self.files_model.refresh()
if self.auto_select_latest_modified:
self._select_last_modified_file()
def on_context_menu(self, point):
index = self._workarea_files_view.indexAt(point)
if not index.isValid():
return
if not index.flags() & QtCore.Qt.ItemIsEnabled:
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 = self.files_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"""
model = self.files_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(DATE_MODIFIED_ROLE)
if modified is not None and modified > highest:
highest_index = index
highest = modified
if highest_index:
self.files_view.setCurrentIndex(highest_index)
class SidePanelWidget(QtWidgets.QWidget):
save_clicked = QtCore.Signal()
def __init__(self, parent=None):
super(SidePanelWidget, self).__init__(parent)
details_label = QtWidgets.QLabel("Details", self)
details_input = QtWidgets.QPlainTextEdit(self)
details_input.setReadOnly(True)
note_label = QtWidgets.QLabel("Artist note", self)
note_input = QtWidgets.QPlainTextEdit(self)
btn_note_save = QtWidgets.QPushButton("Save note", self)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(details_label, 0)
main_layout.addWidget(details_input, 0)
main_layout.addWidget(note_label, 0)
main_layout.addWidget(note_input, 1)
main_layout.addWidget(btn_note_save, alignment=QtCore.Qt.AlignRight)
note_input.textChanged.connect(self.on_note_change)
btn_note_save.clicked.connect(self.on_save_click)
self.details_input = details_input
self.note_input = note_input
self.btn_note_save = btn_note_save
self._orig_note = ""
self._workfile_doc = None
def on_note_change(self):
text = self.note_input.toPlainText()
self.btn_note_save.setEnabled(self._orig_note != text)
def on_save_click(self):
self._orig_note = self.note_input.toPlainText()
self.on_note_change()
self.save_clicked.emit()
def set_context(self, asset_id, task_name, filepath, workfile_doc):
# Check if asset, task and file are selected
# NOTE workfile document is not requirement
enabled = bool(asset_id) and bool(task_name) and bool(filepath)
self.details_input.setEnabled(enabled)
self.note_input.setEnabled(enabled)
self.btn_note_save.setEnabled(enabled)
# Make sure workfile doc is overridden
self._workfile_doc = workfile_doc
# Disable inputs and remove texts if any required arguments are missing
if not enabled:
self._orig_note = ""
self.details_input.setPlainText("")
self.note_input.setPlainText("")
return
orig_note = ""
if workfile_doc:
orig_note = workfile_doc["data"].get("note") or orig_note
self._orig_note = orig_note
self.note_input.setPlainText(orig_note)
# Set as empty string
self.details_input.setPlainText("")
filestat = os.stat(filepath)
size_ending_mapping = {
"KB": 1024 ** 1,
"MB": 1024 ** 2,
"GB": 1024 ** 3
}
size = filestat.st_size
ending = "B"
for _ending, _size in size_ending_mapping.items():
if filestat.st_size < _size:
break
size = filestat.st_size / _size
ending = _ending
# Append html string
datetime_format = "%b %d %Y %H:%M:%S"
creation_time = datetime.datetime.fromtimestamp(filestat.st_ctime)
modification_time = datetime.datetime.fromtimestamp(filestat.st_mtime)
lines = (
"<b>Size:</b>",
"{:.2f} {}".format(size, ending),
"<b>Created:</b>",
creation_time.strftime(datetime_format),
"<b>Modified:</b>",
modification_time.strftime(datetime_format)
)
self.details_input.appendHtml("<br>".join(lines))
def get_workfile_data(self):
data = {
"note": self.note_input.toPlainText()
}
return self._workfile_doc, data
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)
window_flags = QtCore.Qt.Window | QtCore.Qt.WindowCloseButtonHint
if not parent:
window_flags |= QtCore.Qt.WindowStaysOnTopHint
self.setWindowFlags(window_flags)
# Create pages widget and set it as central widget
pages_widget = QtWidgets.QStackedWidget(self)
self.setCentralWidget(pages_widget)
home_page_widget = QtWidgets.QWidget(pages_widget)
home_body_widget = QtWidgets.QWidget(home_page_widget)
assets_widget = SingleSelectAssetsWidget(io, parent=home_body_widget)
assets_widget.set_current_asset_btn_visibility(True)
tasks_widget = TasksWidget(io, home_body_widget)
files_widget = FilesWidget(home_body_widget)
side_panel = SidePanelWidget(home_body_widget)
pages_widget.addWidget(home_page_widget)
# Build home
home_page_layout = QtWidgets.QVBoxLayout(home_page_widget)
home_page_layout.addWidget(home_body_widget)
# Build home - body
body_layout = QtWidgets.QVBoxLayout(home_body_widget)
split_widget = QtWidgets.QSplitter(home_body_widget)
split_widget.addWidget(assets_widget)
split_widget.addWidget(tasks_widget)
split_widget.addWidget(files_widget)
split_widget.addWidget(side_panel)
split_widget.setSizes([255, 160, 455, 175])
body_layout.addWidget(split_widget)
# Add top margin for tasks to align it visually with files as
# the files widget has a filter field which tasks does not.
tasks_widget.setContentsMargins(0, 32, 0, 0)
# Set context after asset widget is refreshed
# - to do so it is necessary to wait until refresh is done
set_context_timer = QtCore.QTimer()
set_context_timer.setInterval(100)
# Connect signals
set_context_timer.timeout.connect(self._on_context_set_timeout)
assets_widget.selection_changed.connect(self._on_asset_changed)
tasks_widget.task_changed.connect(self._on_task_changed)
files_widget.file_selected.connect(self.on_file_select)
files_widget.workfile_created.connect(self.on_workfile_create)
files_widget.file_opened.connect(self._on_file_opened)
side_panel.save_clicked.connect(self.on_side_panel_save)
self._set_context_timer = set_context_timer
self.home_page_widget = home_page_widget
self.pages_widget = pages_widget
self.home_body_widget = home_body_widget
self.split_widget = split_widget
self.assets_widget = assets_widget
self.tasks_widget = tasks_widget
self.files_widget = files_widget
self.side_panel = side_panel
# Force focus on the open button by default, required for Houdini.
files_widget.btn_open.setFocus()
self.resize(1200, 600)
self._first_show = True
self._context_to_set = None
def showEvent(self, event):
super(Window, self).showEvent(event)
if self._first_show:
self._first_show = False
self.refresh()
self.setStyleSheet(style.load_stylesheet())
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 accidentally perform Maya commands
whilst trying to name an instance.
"""
def set_save_enabled(self, enabled):
self.files_widget.btn_save.setEnabled(enabled)
def on_file_select(self, filepath):
asset_id = self.assets_widget.get_selected_asset_id()
task_name = self.tasks_widget.get_selected_task_name()
workfile_doc = None
if asset_id and task_name and filepath:
filename = os.path.split(filepath)[1]
workfile_doc = get_workfile_doc(
asset_id, task_name, filename, io
)
self.side_panel.set_context(
asset_id, task_name, filepath, workfile_doc
)
def on_workfile_create(self, filepath):
self._create_workfile_doc(filepath)
def _on_file_opened(self):
self.close()
def on_side_panel_save(self):
workfile_doc, data = self.side_panel.get_workfile_data()
if not workfile_doc:
filepath = self.files_widget._get_selected_filepath()
self._create_workfile_doc(filepath, force=True)
workfile_doc = self._get_current_workfile_doc()
save_workfile_data_to_doc(workfile_doc, data, io)
def _get_current_workfile_doc(self, filepath=None):
if filepath is None:
filepath = self.files_widget._get_selected_filepath()
task_name = self.tasks_widget.get_selected_task_name()
asset_id = self.assets_widget.get_selected_asset_id()
if not task_name or not asset_id or not filepath:
return
filename = os.path.split(filepath)[1]
return get_workfile_doc(
asset_id, task_name, filename, io
)
def _create_workfile_doc(self, filepath, force=False):
workfile_doc = None
if not force:
workfile_doc = self._get_current_workfile_doc(filepath)
if not workfile_doc:
workdir, filename = os.path.split(filepath)
asset_id = self.assets_widget.get_selected_asset_id()
asset_doc = io.find_one({"_id": asset_id})
task_name = self.tasks_widget.get_selected_task_name()
create_workfile_doc(asset_doc, task_name, filename, workdir, io)
def refresh(self):
# Refresh asset widget
self.assets_widget.refresh()
self._on_task_changed()
def set_context(self, context):
self._context_to_set = context
self._set_context_timer.start()
def _on_context_set_timeout(self):
if self._context_to_set is None:
self._set_context_timer.stop()
return
if self.assets_widget.refreshing:
return
self._context_to_set, context = None, self._context_to_set
if "asset" in context:
asset_doc = io.find_one(
{
"name": context["asset"],
"type": "asset"
},
{"_id": 1}
) or {}
asset_id = asset_doc.get("_id")
# Select the asset
self.assets_widget.select_asset(asset_id)
self.tasks_widget.set_asset_id(asset_id)
if "task" in context:
self.tasks_widget.select_task_name(context["task"])
self._on_task_changed()
def _on_asset_changed(self):
asset_id = self.assets_widget.get_selected_asset_id()
if asset_id:
self.tasks_widget.setEnabled(True)
else:
# Force disable the other widgets if no
# active selection
self.tasks_widget.setEnabled(False)
self.files_widget.setEnabled(False)
self.tasks_widget.set_asset_id(asset_id)
def _on_task_changed(self):
asset_id = self.assets_widget.get_selected_asset_id()
task_name = self.tasks_widget.get_selected_task_name()
task_type = self.tasks_widget.get_selected_task_type()
asset_is_valid = asset_id is not None
self.tasks_widget.setEnabled(asset_is_valid)
self.files_widget.setEnabled(bool(task_name) and asset_is_valid)
self.files_widget.set_asset_task(asset_id, task_name, task_type)
self.files_widget.refresh()
def validate_host_requirements(host):
if host is None:
raise RuntimeError("No registered host.")

View file

@ -0,0 +1,445 @@
import os
import logging
import shutil
import Qt
from Qt import QtWidgets, QtCore
from avalon import io, api
from openpype.tools.utils import PlaceholderLineEdit
from openpype.tools.utils.delegates import PrettyTimeDelegate
from openpype.lib import (
emit_event,
Anatomy,
get_workfile_template_key,
create_workdir_extra_folders,
)
from openpype.lib.avalon_context import (
update_current_task,
compute_session_changes
)
from .model import (
WorkAreaFilesModel,
FILEPATH_ROLE,
DATE_MODIFIED_ROLE,
)
from .save_as_dialog import SaveAsDialog
log = logging.getLogger(__name__)
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)
class FilesWidget(QtWidgets.QWidget):
"""A widget displaying files that allows to save and open files."""
file_selected = QtCore.Signal(str)
workfile_created = QtCore.Signal(str)
file_opened = QtCore.Signal()
def __init__(self, parent=None):
super(FilesWidget, self).__init__(parent=parent)
# Setup
self._asset_id = None
self._asset_doc = None
self._task_name = None
self._task_type = None
# Pype's anatomy object for current project
self.anatomy = Anatomy(io.Session["AVALON_PROJECT"])
# Template key used to get work template from anatomy templates
self.template_key = "work"
# This is not root but workfile directory
self._workfiles_root = None
self._workdir_path = 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
files_view = FilesView(self)
# Create the Files model
extensions = set(self.host.file_extensions())
files_model = WorkAreaFilesModel(extensions)
# Create proxy model for files to be able sort and filter
proxy_model = QtCore.QSortFilterProxyModel()
proxy_model.setSourceModel(files_model)
proxy_model.setDynamicSortFilter(True)
proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
# Set up the file list tree view
files_view.setModel(proxy_model)
files_view.setSortingEnabled(True)
files_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
# Date modified delegate
time_delegate = PrettyTimeDelegate()
files_view.setItemDelegateForColumn(1, time_delegate)
files_view.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.
files_view.setColumnWidth(0, 330)
# Filtering input
filter_input = PlaceholderLineEdit(self)
filter_input.setPlaceholderText("Filter files..")
filter_input.textChanged.connect(proxy_model.setFilterFixedString)
# Home Page
# Build buttons widget for files widget
btns_widget = QtWidgets.QWidget(self)
btn_save = QtWidgets.QPushButton("Save As", btns_widget)
btn_browse = QtWidgets.QPushButton("Browse", btns_widget)
btn_open = QtWidgets.QPushButton("Open", btns_widget)
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
btns_layout.setContentsMargins(0, 0, 0, 0)
btns_layout.addWidget(btn_open)
btns_layout.addWidget(btn_browse)
btns_layout.addWidget(btn_save)
# Build files widgets for home page
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(filter_input)
main_layout.addWidget(files_view)
main_layout.addWidget(btns_widget)
# Register signal callbacks
files_view.doubleClickedLeft.connect(self.on_open_pressed)
files_view.customContextMenuRequested.connect(self.on_context_menu)
files_view.selectionModel().selectionChanged.connect(
self.on_file_select
)
btn_open.pressed.connect(self.on_open_pressed)
btn_browse.pressed.connect(self.on_browse_pressed)
btn_save.pressed.connect(self.on_save_as_pressed)
# Store attributes
self.time_delegate = time_delegate
self.filter_input = filter_input
self.files_view = files_view
self.files_model = files_model
self.btns_widget = btns_widget
self.btn_open = btn_open
self.btn_browse = btn_browse
self.btn_save = btn_save
def set_asset_task(self, asset_id, task_name, task_type):
if asset_id != self._asset_id:
self._asset_doc = None
self._asset_id = asset_id
self._task_name = task_name
self._task_type = task_type
# 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_id and self._task_name and self._task_type:
session = self._get_session()
self._workdir_path = session["AVALON_WORKDIR"]
self._workfiles_root = self.host.work_root(session)
self.files_model.set_root(self._workfiles_root)
else:
self.files_model.set_root(None)
# Disable/Enable buttons based on available files in model
has_valid_items = self.files_model.has_valid_items()
self.btn_browse.setEnabled(has_valid_items)
self.btn_open.setEnabled(has_valid_items)
if not has_valid_items:
# Manually trigger file selection
self.on_file_select()
def _get_asset_doc(self):
if self._asset_id is None:
return None
if self._asset_doc is None:
self._asset_doc = io.find_one({"_id": self._asset_id})
return self._asset_doc
def _get_session(self):
"""Return a modified session for the current asset and task"""
session = api.Session.copy()
self.template_key = get_workfile_template_key(
self._task_type,
session["AVALON_APP"],
project_name=session["AVALON_PROJECT"]
)
changes = compute_session_changes(
session,
asset=self._get_asset_doc(),
task=self._task_name,
template_key=self.template_key
)
session.update(changes)
return session
def _enter_session(self):
"""Enter the asset and task session currently selected"""
session = api.Session.copy()
changes = compute_session_changes(
session,
asset=self._get_asset_doc(),
task=self._task_name,
template_key=self.template_key
)
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
update_current_task(
asset=self._get_asset_doc(),
task=self._task_name,
template_key=self.template_key
)
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
# Save first if has changes
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)
self._enter_session()
host.open_file(filepath)
self.file_opened.emit()
def save_changes_prompt(self):
self._messagebox = messagebox = QtWidgets.QMessageBox(parent=self)
messagebox.setWindowFlags(messagebox.windowFlags() |
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
)
result = messagebox.exec_()
if result == messagebox.Yes:
return True
if result == messagebox.No:
return False
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 = SaveAsDialog(
parent=self,
root=self._workfiles_root,
anatomy=self.anatomy,
template_key=self.template_key,
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._workfiles_root, work_file)
shutil.copy(src, dst)
self.workfile_created.emit(dst)
self.refresh()
def _get_selected_filepath(self):
"""Return current filepath selected in view"""
selection = self.files_view.selectionModel()
index = selection.currentIndex()
if not index.isValid():
return
return index.data(FILEPATH_ROLE)
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):
ext_filter = "Work File (*{0})".format(
" *".join(self.host.file_extensions())
)
kwargs = {
"caption": "Work Files",
"filter": ext_filter
}
if Qt.__binding__ in ("PySide", "PySide2"):
kwargs["dir"] = self._workfiles_root
else:
kwargs["directory"] = self._workfiles_root
work_file = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
if work_file:
self.open_file(work_file)
def on_save_as_pressed(self):
work_filename = self.get_filename()
if not work_filename:
return
# Trigger before save event
emit_event(
"workfile.save.before",
{"filename": work_filename, "workdir_path": self._workdir_path},
source="workfiles.tool"
)
# Make sure workfiles root is updated
# - this triggers 'workio.work_root(...)' which may change value of
# '_workfiles_root'
self.set_asset_task(
self._asset_id, self._task_name, self._task_type
)
# Create workfiles root folder
if not os.path.exists(self._workfiles_root):
log.debug("Initializing Work Directory: %s", self._workfiles_root)
os.makedirs(self._workfiles_root)
# Update session if context has changed
self._enter_session()
# Prepare full path to workfile and save it
filepath = os.path.join(
os.path.normpath(self._workfiles_root), work_filename
)
self.host.save_file(filepath)
# Create extra folders
create_workdir_extra_folders(
self._workdir_path,
api.Session["AVALON_APP"],
self._task_type,
self._task_name,
api.Session["AVALON_PROJECT"]
)
# Trigger after save events
emit_event(
"workfile.save.after",
{"filename": work_filename, "workdir_path": self._workdir_path},
source="workfiles.tool"
)
self.workfile_created.emit(filepath)
# Refresh files model
self.refresh()
def on_file_select(self):
self.file_selected.emit(self._get_selected_filepath())
def refresh(self):
"""Refresh listed files for current selection in the interface"""
self.files_model.refresh()
if self.auto_select_latest_modified:
self._select_last_modified_file()
def on_context_menu(self, point):
index = self._workarea_files_view.indexAt(point)
if not index.isValid():
return
if not index.flags() & QtCore.Qt.ItemIsEnabled:
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 = self.files_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"""
model = self.files_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(DATE_MODIFIED_ROLE)
if modified is not None and modified > highest:
highest_index = index
highest = modified
if highest_index:
self.files_view.setCurrentIndex(highest_index)

View file

@ -1,15 +0,0 @@
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)

View file

@ -0,0 +1,334 @@
import os
import datetime
from Qt import QtCore, QtWidgets
from avalon import io
from openpype import style
from openpype.lib import (
get_workfile_doc,
create_workfile_doc,
save_workfile_data_to_doc,
)
from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget
from openpype.tools.utils.tasks_widget import TasksWidget
from .files_widget import FilesWidget
class SidePanelWidget(QtWidgets.QWidget):
save_clicked = QtCore.Signal()
def __init__(self, parent=None):
super(SidePanelWidget, self).__init__(parent)
details_label = QtWidgets.QLabel("Details", self)
details_input = QtWidgets.QPlainTextEdit(self)
details_input.setReadOnly(True)
note_label = QtWidgets.QLabel("Artist note", self)
note_input = QtWidgets.QPlainTextEdit(self)
btn_note_save = QtWidgets.QPushButton("Save note", self)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(details_label, 0)
main_layout.addWidget(details_input, 0)
main_layout.addWidget(note_label, 0)
main_layout.addWidget(note_input, 1)
main_layout.addWidget(btn_note_save, alignment=QtCore.Qt.AlignRight)
note_input.textChanged.connect(self.on_note_change)
btn_note_save.clicked.connect(self.on_save_click)
self.details_input = details_input
self.note_input = note_input
self.btn_note_save = btn_note_save
self._orig_note = ""
self._workfile_doc = None
def on_note_change(self):
text = self.note_input.toPlainText()
self.btn_note_save.setEnabled(self._orig_note != text)
def on_save_click(self):
self._orig_note = self.note_input.toPlainText()
self.on_note_change()
self.save_clicked.emit()
def set_context(self, asset_id, task_name, filepath, workfile_doc):
# Check if asset, task and file are selected
# NOTE workfile document is not requirement
enabled = bool(asset_id) and bool(task_name) and bool(filepath)
self.details_input.setEnabled(enabled)
self.note_input.setEnabled(enabled)
self.btn_note_save.setEnabled(enabled)
# Make sure workfile doc is overridden
self._workfile_doc = workfile_doc
# Disable inputs and remove texts if any required arguments are missing
if not enabled:
self._orig_note = ""
self.details_input.setPlainText("")
self.note_input.setPlainText("")
return
orig_note = ""
if workfile_doc:
orig_note = workfile_doc["data"].get("note") or orig_note
self._orig_note = orig_note
self.note_input.setPlainText(orig_note)
# Set as empty string
self.details_input.setPlainText("")
filestat = os.stat(filepath)
size_ending_mapping = {
"KB": 1024 ** 1,
"MB": 1024 ** 2,
"GB": 1024 ** 3
}
size = filestat.st_size
ending = "B"
for _ending, _size in size_ending_mapping.items():
if filestat.st_size < _size:
break
size = filestat.st_size / _size
ending = _ending
# Append html string
datetime_format = "%b %d %Y %H:%M:%S"
creation_time = datetime.datetime.fromtimestamp(filestat.st_ctime)
modification_time = datetime.datetime.fromtimestamp(filestat.st_mtime)
lines = (
"<b>Size:</b>",
"{:.2f} {}".format(size, ending),
"<b>Created:</b>",
creation_time.strftime(datetime_format),
"<b>Modified:</b>",
modification_time.strftime(datetime_format)
)
self.details_input.appendHtml("<br>".join(lines))
def get_workfile_data(self):
data = {
"note": self.note_input.toPlainText()
}
return self._workfile_doc, data
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)
window_flags = QtCore.Qt.Window | QtCore.Qt.WindowCloseButtonHint
if not parent:
window_flags |= QtCore.Qt.WindowStaysOnTopHint
self.setWindowFlags(window_flags)
# Create pages widget and set it as central widget
pages_widget = QtWidgets.QStackedWidget(self)
self.setCentralWidget(pages_widget)
home_page_widget = QtWidgets.QWidget(pages_widget)
home_body_widget = QtWidgets.QWidget(home_page_widget)
assets_widget = SingleSelectAssetsWidget(io, parent=home_body_widget)
assets_widget.set_current_asset_btn_visibility(True)
tasks_widget = TasksWidget(io, home_body_widget)
files_widget = FilesWidget(home_body_widget)
side_panel = SidePanelWidget(home_body_widget)
pages_widget.addWidget(home_page_widget)
# Build home
home_page_layout = QtWidgets.QVBoxLayout(home_page_widget)
home_page_layout.addWidget(home_body_widget)
# Build home - body
body_layout = QtWidgets.QVBoxLayout(home_body_widget)
split_widget = QtWidgets.QSplitter(home_body_widget)
split_widget.addWidget(assets_widget)
split_widget.addWidget(tasks_widget)
split_widget.addWidget(files_widget)
split_widget.addWidget(side_panel)
split_widget.setSizes([255, 160, 455, 175])
body_layout.addWidget(split_widget)
# Add top margin for tasks to align it visually with files as
# the files widget has a filter field which tasks does not.
tasks_widget.setContentsMargins(0, 32, 0, 0)
# Set context after asset widget is refreshed
# - to do so it is necessary to wait until refresh is done
set_context_timer = QtCore.QTimer()
set_context_timer.setInterval(100)
# Connect signals
set_context_timer.timeout.connect(self._on_context_set_timeout)
assets_widget.selection_changed.connect(self._on_asset_changed)
tasks_widget.task_changed.connect(self._on_task_changed)
files_widget.file_selected.connect(self.on_file_select)
files_widget.workfile_created.connect(self.on_workfile_create)
files_widget.file_opened.connect(self._on_file_opened)
side_panel.save_clicked.connect(self.on_side_panel_save)
self._set_context_timer = set_context_timer
self.home_page_widget = home_page_widget
self.pages_widget = pages_widget
self.home_body_widget = home_body_widget
self.split_widget = split_widget
self.assets_widget = assets_widget
self.tasks_widget = tasks_widget
self.files_widget = files_widget
self.side_panel = side_panel
# Force focus on the open button by default, required for Houdini.
files_widget.btn_open.setFocus()
self.resize(1200, 600)
self._first_show = True
self._context_to_set = None
def showEvent(self, event):
super(Window, self).showEvent(event)
if self._first_show:
self._first_show = False
self.refresh()
self.setStyleSheet(style.load_stylesheet())
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 accidentally perform Maya commands
whilst trying to name an instance.
"""
def set_save_enabled(self, enabled):
self.files_widget.btn_save.setEnabled(enabled)
def on_file_select(self, filepath):
asset_id = self.assets_widget.get_selected_asset_id()
task_name = self.tasks_widget.get_selected_task_name()
workfile_doc = None
if asset_id and task_name and filepath:
filename = os.path.split(filepath)[1]
workfile_doc = get_workfile_doc(
asset_id, task_name, filename, io
)
self.side_panel.set_context(
asset_id, task_name, filepath, workfile_doc
)
def on_workfile_create(self, filepath):
self._create_workfile_doc(filepath)
def _on_file_opened(self):
self.close()
def on_side_panel_save(self):
workfile_doc, data = self.side_panel.get_workfile_data()
if not workfile_doc:
filepath = self.files_widget._get_selected_filepath()
self._create_workfile_doc(filepath, force=True)
workfile_doc = self._get_current_workfile_doc()
save_workfile_data_to_doc(workfile_doc, data, io)
def _get_current_workfile_doc(self, filepath=None):
if filepath is None:
filepath = self.files_widget._get_selected_filepath()
task_name = self.tasks_widget.get_selected_task_name()
asset_id = self.assets_widget.get_selected_asset_id()
if not task_name or not asset_id or not filepath:
return
filename = os.path.split(filepath)[1]
return get_workfile_doc(
asset_id, task_name, filename, io
)
def _create_workfile_doc(self, filepath, force=False):
workfile_doc = None
if not force:
workfile_doc = self._get_current_workfile_doc(filepath)
if not workfile_doc:
workdir, filename = os.path.split(filepath)
asset_id = self.assets_widget.get_selected_asset_id()
asset_doc = io.find_one({"_id": asset_id})
task_name = self.tasks_widget.get_selected_task_name()
create_workfile_doc(asset_doc, task_name, filename, workdir, io)
def refresh(self):
# Refresh asset widget
self.assets_widget.refresh()
self._on_task_changed()
def set_context(self, context):
self._context_to_set = context
self._set_context_timer.start()
def _on_context_set_timeout(self):
if self._context_to_set is None:
self._set_context_timer.stop()
return
if self.assets_widget.refreshing:
return
self._context_to_set, context = None, self._context_to_set
if "asset" in context:
asset_doc = io.find_one(
{
"name": context["asset"],
"type": "asset"
},
{"_id": 1}
) or {}
asset_id = asset_doc.get("_id")
# Select the asset
self.assets_widget.select_asset(asset_id)
self.tasks_widget.set_asset_id(asset_id)
if "task" in context:
self.tasks_widget.select_task_name(context["task"])
self._on_task_changed()
def _on_asset_changed(self):
asset_id = self.assets_widget.get_selected_asset_id()
if asset_id:
self.tasks_widget.setEnabled(True)
else:
# Force disable the other widgets if no
# active selection
self.tasks_widget.setEnabled(False)
self.files_widget.setEnabled(False)
self.tasks_widget.set_asset_id(asset_id)
def _on_task_changed(self):
asset_id = self.assets_widget.get_selected_asset_id()
task_name = self.tasks_widget.get_selected_task_name()
task_type = self.tasks_widget.get_selected_task_type()
asset_is_valid = asset_id is not None
self.tasks_widget.setEnabled(asset_is_valid)
self.files_widget.setEnabled(bool(task_name) and asset_is_valid)
self.files_widget.set_asset_task(asset_id, task_name, task_type)
self.files_widget.refresh()