diff --git a/pype/ftrack/actions/action_asset_delete.py b/pype/ftrack/actions/action_asset_delete.py
deleted file mode 100644
index 654c78049b..0000000000
--- a/pype/ftrack/actions/action_asset_delete.py
+++ /dev/null
@@ -1,129 +0,0 @@
-import sys
-import argparse
-import logging
-from pype.vendor import ftrack_api
-from pype.ftrack import BaseAction
-
-
-class AssetDelete(BaseAction):
- '''Custom action.'''
-
- #: Action identifier.
- identifier = 'asset.delete'
- #: Action label.
- label = 'Asset Delete'
-
- def discover(self, session, entities, event):
- ''' Validation '''
-
- if (
- len(entities) != 1 or
- entities[0].entity_type not in ['Shot', 'Asset Build']
- ):
- return False
-
- return True
-
- def interface(self, session, entities, event):
-
- if not event['data'].get('values', {}):
- entity = entities[0]
-
- items = []
- for asset in entity['assets']:
- # get asset name for label
- label = 'None'
- if asset['name']:
- label = asset['name']
-
- items.append({
- 'label': label,
- 'name': label,
- 'value': False,
- 'type': 'boolean'
- })
-
- if len(items) < 1:
- return {
- 'success': False,
- 'message': 'There are no assets to delete'
- }
-
- return items
-
- def launch(self, session, entities, event):
-
- entity = entities[0]
- # if values were set remove those items
- if 'values' in event['data']:
- values = event['data']['values']
- # get list of assets to delete from form
- to_delete = []
- for key in values:
- if values[key]:
- to_delete.append(key)
- # delete them by name
- for asset in entity['assets']:
- if asset['name'] in to_delete:
- session.delete(asset)
- try:
- session.commit()
- except Exception:
- session.rollback()
- raise
-
- return {
- 'success': True,
- 'message': 'Asset deleted.'
- }
-
-
-def register(session, plugins_presets={}):
- '''Register action. Called when used as an event plugin.'''
-
- # Validate that session is an instance of ftrack_api.Session. If not,
- # assume that register is being called from an old or incompatible API and
- # return without doing anything.
- if not isinstance(session, ftrack_api.session.Session):
- return
-
- AssetDelete(session, plugins_presets).register()
-
-
-def main(arguments=None):
- '''Set up logging and register action.'''
- if arguments is None:
- arguments = []
-
- parser = argparse.ArgumentParser()
- # Allow setting of logging level from arguments.
- loggingLevels = {}
- for level in (
- logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING,
- logging.ERROR, logging.CRITICAL
- ):
- loggingLevels[logging.getLevelName(level).lower()] = level
-
- parser.add_argument(
- '-v', '--verbosity',
- help='Set the logging output verbosity.',
- choices=loggingLevels.keys(),
- default='info'
- )
- namespace = parser.parse_args(arguments)
-
- # Set up basic logging
- logging.basicConfig(level=loggingLevels[namespace.verbosity])
-
- session = ftrack_api.Session()
- register(session)
-
- # Wait for events
- logging.info(
- 'Registered actions and listening for events. Use Ctrl-C to abort.'
- )
- session.event_hub.wait()
-
-
-if __name__ == '__main__':
- raise SystemExit(main(sys.argv[1:]))
diff --git a/pype/ftrack/actions/action_attributes_remapper.py b/pype/ftrack/actions/action_attributes_remapper.py
index 2c4899410d..759b5765e5 100644
--- a/pype/ftrack/actions/action_attributes_remapper.py
+++ b/pype/ftrack/actions/action_attributes_remapper.py
@@ -8,6 +8,7 @@ from pype.ftrack.lib.io_nonsingleton import DbConnector
class AttributesRemapper(BaseAction):
'''Edit meta data action.'''
+ ignore_me = True
#: Action identifier.
identifier = 'attributes.remapper'
#: Action label.
diff --git a/pype/ftrack/actions/action_cust_attr_doctor.py b/pype/ftrack/actions/action_cust_attr_doctor.py
index b875f52ab8..af5fe2dc4a 100644
--- a/pype/ftrack/actions/action_cust_attr_doctor.py
+++ b/pype/ftrack/actions/action_cust_attr_doctor.py
@@ -9,6 +9,8 @@ from pype.ftrack import BaseAction
class CustomAttributeDoctor(BaseAction):
+
+ ignore_me = True
#: Action identifier.
identifier = 'custom.attributes.doctor'
#: Action label.
@@ -26,7 +28,9 @@ class CustomAttributeDoctor(BaseAction):
hierarchical_ca = ['handleStart', 'handleEnd', 'frameStart', 'frameEnd']
hierarchical_alternatives = {
'handleStart': 'handles',
- 'handleEnd': 'handles'
+ 'handleEnd': 'handles',
+ "frameStart": "fstart",
+ "frameEnd": "fend"
}
# Roles for new custom attributes
diff --git a/pype/ftrack/actions/action_delete_unpublished.py b/pype/ftrack/actions/action_delete_unpublished.py
deleted file mode 100644
index 5e7f783ba7..0000000000
--- a/pype/ftrack/actions/action_delete_unpublished.py
+++ /dev/null
@@ -1,93 +0,0 @@
-import sys
-import argparse
-import logging
-from pype.vendor import ftrack_api
-from pype.ftrack import BaseAction
-
-
-class VersionsCleanup(BaseAction):
- '''Custom action.'''
-
- # Action identifier
- identifier = 'versions.cleanup'
- # Action label
- label = 'Versions cleanup'
-
- def discover(self, session, entities, event):
- ''' Validation '''
-
- # Only 1 AssetVersion is allowed
- if len(entities) != 1 or entities[0].entity_type != 'AssetVersion':
- return False
-
- return True
-
- def launch(self, session, entities, event):
-
- entity = entities[0]
-
- # Go through all versions in asset
- for version in entity['asset']['versions']:
- if not version['is_published']:
- session.delete(version)
- try:
- session.commit()
- except Exception:
- session.rollback()
- raise
-
- return {
- 'success': True,
- 'message': 'Hidden versions were removed'
- }
-
-
-def register(session, plugins_presets={}):
- '''Register action. Called when used as an event plugin.'''
-
- # Validate that session is an instance of ftrack_api.Session. If not,
- # assume that register is being called from an old or incompatible API and
- # return without doing anything.
- if not isinstance(session, ftrack_api.session.Session):
- return
-
- VersionsCleanup(session, plugins_presets).register()
-
-
-def main(arguments=None):
- '''Set up logging and register action.'''
- if arguments is None:
- arguments = []
-
- parser = argparse.ArgumentParser()
- # Allow setting of logging level from arguments.
- loggingLevels = {}
- for level in (
- logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING,
- logging.ERROR, logging.CRITICAL
- ):
- loggingLevels[logging.getLevelName(level).lower()] = level
-
- parser.add_argument(
- '-v', '--verbosity',
- help='Set the logging output verbosity.',
- choices=loggingLevels.keys(),
- default='info'
- )
- namespace = parser.parse_args(arguments)
-
- # Set up basic logging
- logging.basicConfig(level=loggingLevels[namespace.verbosity])
-
- session = ftrack_api.Session()
- register(session)
-
- # Wait for events
- logging.info(
- 'Registered actions and listening for events. Use Ctrl-C to abort.'
- )
- session.event_hub.wait()
-
-
-if __name__ == '__main__':
- raise SystemExit(main(sys.argv[1:]))
diff --git a/pype/ftrack/actions/action_job_killer.py b/pype/ftrack/actions/action_job_killer.py
index 8584b26aa4..717f87e879 100644
--- a/pype/ftrack/actions/action_job_killer.py
+++ b/pype/ftrack/actions/action_job_killer.py
@@ -107,7 +107,7 @@ class JobKiller(BaseAction):
'Changing Job ({}) status: {} -> failed'
).format(job['id'], job['status']))
except Exception:
- self.warning.debug((
+ self.log.warning.debug((
'Changing Job ({}) has failed'
).format(job['id']))
diff --git a/pype/ftrack/actions/action_set_version.py b/pype/ftrack/actions/action_set_version.py
deleted file mode 100644
index 5bf965e3ef..0000000000
--- a/pype/ftrack/actions/action_set_version.py
+++ /dev/null
@@ -1,122 +0,0 @@
-import sys
-import argparse
-import logging
-from pype.vendor import ftrack_api
-from pype.ftrack import BaseAction
-
-
-class SetVersion(BaseAction):
- '''Custom action.'''
-
- #: Action identifier.
- identifier = 'version.set'
- #: Action label.
- label = 'Version Set'
-
- def discover(self, session, entities, event):
- ''' Validation '''
-
- # Only 1 AssetVersion is allowed
- if len(entities) != 1 or entities[0].entity_type != 'AssetVersion':
- return False
-
- return True
-
- def interface(self, session, entities, event):
-
- if not event['data'].get('values', {}):
- entity = entities[0]
-
- # Get actual version of asset
- act_ver = entity['version']
- # Set form
- items = [{
- 'label': 'Version number',
- 'type': 'number',
- 'name': 'version_number',
- 'value': act_ver
- }]
-
- return items
-
- def launch(self, session, entities, event):
-
- entity = entities[0]
-
- # Do something with the values or return a new form.
- values = event['data'].get('values', {})
- # Default is action True
- scs = False
-
- if not values['version_number']:
- msg = 'You didn\'t enter any version.'
- elif int(values['version_number']) <= 0:
- msg = 'Negative or zero version is not valid.'
- else:
- try:
- entity['version'] = values['version_number']
- session.commit()
- msg = 'Version was changed to v{0}'.format(
- values['version_number']
- )
- scs = True
- except Exception as e:
- msg = 'Unexpected error occurs during version set ({})'.format(
- str(e)
- )
-
- return {
- 'success': scs,
- 'message': msg
- }
-
-
-def register(session, plugins_presets={}):
- '''Register action. Called when used as an event plugin.'''
-
- # Validate that session is an instance of ftrack_api.Session. If not,
- # assume that register is being called from an old or incompatible API and
- # return without doing anything.
- if not isinstance(session, ftrack_api.session.Session):
- return
-
- SetVersion(session, plugins_presets).register()
-
-
-def main(arguments=None):
- '''Set up logging and register action.'''
- if arguments is None:
- arguments = []
-
- parser = argparse.ArgumentParser()
- # Allow setting of logging level from arguments.
- loggingLevels = {}
- for level in (
- logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING,
- logging.ERROR, logging.CRITICAL
- ):
- loggingLevels[logging.getLevelName(level).lower()] = level
-
- parser.add_argument(
- '-v', '--verbosity',
- help='Set the logging output verbosity.',
- choices=loggingLevels.keys(),
- default='info'
- )
- namespace = parser.parse_args(arguments)
-
- # Set up basic logging
- logging.basicConfig(level=loggingLevels[namespace.verbosity])
-
- session = ftrack_api.Session()
- register(session)
-
- # Wait for events
- logging.info(
- 'Registered actions and listening for events. Use Ctrl-C to abort.'
- )
- session.event_hub.wait()
-
-
-if __name__ == '__main__':
- raise SystemExit(main(sys.argv[1:]))
diff --git a/pype/ftrack/events/action_sync_hier_attrs.py b/pype/ftrack/events/action_sync_hier_attrs.py
index f8ecb9e3cd..c9d968ee5d 100644
--- a/pype/ftrack/events/action_sync_hier_attrs.py
+++ b/pype/ftrack/events/action_sync_hier_attrs.py
@@ -21,7 +21,7 @@ class SyncHierarchicalAttrs(BaseAction):
identifier = 'sync.hierarchical.attrs'
#: Action label.
label = "Pype Admin"
- variant = '- Sync Hier Attrs (server)'
+ variant = '- Sync Hier Attrs (Server)'
#: Action description.
description = 'Synchronize hierarchical attributes'
#: Icon
diff --git a/pype/logging/gui/__init__.py b/pype/logging/gui/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/pype/logging/gui/app.py b/pype/logging/gui/app.py
new file mode 100644
index 0000000000..7cee280158
--- /dev/null
+++ b/pype/logging/gui/app.py
@@ -0,0 +1,37 @@
+from Qt import QtWidgets, QtCore
+from .widgets import LogsWidget, LogDetailWidget
+from pypeapp import style
+
+
+class LogsWindow(QtWidgets.QWidget):
+ def __init__(self, parent=None):
+ super(LogsWindow, self).__init__(parent)
+
+ self.setStyleSheet(style.load_stylesheet())
+ self.resize(1200, 800)
+ logs_widget = LogsWidget(parent=self)
+ log_detail = LogDetailWidget(parent=self)
+
+ main_layout = QtWidgets.QHBoxLayout()
+
+ log_splitter = QtWidgets.QSplitter()
+ log_splitter.setOrientation(QtCore.Qt.Horizontal)
+ log_splitter.addWidget(logs_widget)
+ log_splitter.addWidget(log_detail)
+ log_splitter.setStretchFactor(0, 65)
+ log_splitter.setStretchFactor(1, 35)
+
+ main_layout.addWidget(log_splitter)
+
+ self.logs_widget = logs_widget
+ self.log_detail = log_detail
+
+ self.setLayout(main_layout)
+ self.setWindowTitle("Logs")
+
+ self.logs_widget.active_changed.connect(self.on_selection_changed)
+
+ def on_selection_changed(self):
+ index = self.logs_widget.selected_log()
+ node = index.data(self.logs_widget.model.NodeRole)
+ self.log_detail.set_detail(node)
diff --git a/pype/logging/gui/lib.py b/pype/logging/gui/lib.py
new file mode 100644
index 0000000000..85782e071e
--- /dev/null
+++ b/pype/logging/gui/lib.py
@@ -0,0 +1,94 @@
+import contextlib
+from Qt import QtCore
+
+
+def _iter_model_rows(
+ model, column, include_root=False
+):
+ """Iterate over all row indices in a model"""
+ indices = [QtCore.QModelIndex()] # start iteration at root
+
+ for index in indices:
+ # Add children to the iterations
+ child_rows = model.rowCount(index)
+ for child_row in range(child_rows):
+ child_index = model.index(child_row, column, index)
+ indices.append(child_index)
+
+ if not include_root and not index.isValid():
+ continue
+
+ yield index
+
+
+@contextlib.contextmanager
+def preserve_states(
+ tree_view, column=0, role=None,
+ preserve_expanded=True, preserve_selection=True,
+ expanded_role=QtCore.Qt.DisplayRole, selection_role=QtCore.Qt.DisplayRole
+
+):
+ """Preserves row selection in QTreeView by column's data role.
+
+ This function is created to maintain the selection status of
+ the model items. When refresh is triggered the items which are expanded
+ will stay expanded and vise versa.
+
+ tree_view (QWidgets.QTreeView): the tree view nested in the application
+ column (int): the column to retrieve the data from
+ role (int): the role which dictates what will be returned
+
+ Returns:
+ None
+
+ """
+ # When `role` is set then override both expanded and selection roles
+ if role:
+ expanded_role = role
+ selection_role = role
+
+ model = tree_view.model()
+ selection_model = tree_view.selectionModel()
+ flags = selection_model.Select | selection_model.Rows
+
+ expanded = set()
+
+ if preserve_expanded:
+ for index in _iter_model_rows(
+ model, column=column, include_root=False
+ ):
+ if tree_view.isExpanded(index):
+ value = index.data(expanded_role)
+ expanded.add(value)
+
+ selected = None
+
+ if preserve_selection:
+ selected_rows = selection_model.selectedRows()
+ if selected_rows:
+ selected = set(row.data(selection_role) for row in selected_rows)
+
+ try:
+ yield
+ finally:
+ if expanded:
+ for index in _iter_model_rows(
+ model, column=0, include_root=False
+ ):
+ value = index.data(expanded_role)
+ is_expanded = value in expanded
+ # skip if new index was created meanwhile
+ if is_expanded is None:
+ continue
+ tree_view.setExpanded(index, is_expanded)
+
+ if selected:
+ # Go through all indices, select the ones with similar data
+ for index in _iter_model_rows(
+ model, column=column, include_root=False
+ ):
+ value = index.data(selection_role)
+ state = value in selected
+ if state:
+ tree_view.scrollTo(index) # Ensure item is visible
+ selection_model.select(index, flags)
diff --git a/pype/logging/gui/models.py b/pype/logging/gui/models.py
new file mode 100644
index 0000000000..6722ed0fe0
--- /dev/null
+++ b/pype/logging/gui/models.py
@@ -0,0 +1,169 @@
+import os
+from Qt import QtCore
+from pypeapp import Logger
+from pypeapp.lib.log import _bootstrap_mongo_log
+
+log = Logger().get_logger("LogModel", "LoggingModule")
+
+
+class LogModel(QtCore.QAbstractItemModel):
+ COLUMNS = [
+ "user",
+ "host",
+ "lineNumber",
+ "method",
+ "module",
+ "fileName",
+ "loggerName",
+ "message",
+ "level",
+ "timestamp",
+ ]
+
+ colums_mapping = {
+ "user": "User",
+ "host": "Host",
+ "lineNumber": "Line n.",
+ "method": "Method",
+ "module": "Module",
+ "fileName": "File name",
+ "loggerName": "Logger name",
+ "message": "Message",
+ "level": "Level",
+ "timestamp": "Timestamp",
+ }
+
+ NodeRole = QtCore.Qt.UserRole + 1
+
+ def __init__(self, parent=None):
+ super(LogModel, self).__init__(parent)
+ self._root_node = Node()
+
+ collection = os.environ.get('PYPE_LOG_MONGO_COL')
+ database = _bootstrap_mongo_log()
+ self.dbcon = None
+ if collection in database.list_collection_names():
+ self.dbcon = database[collection]
+
+ def add_log(self, log):
+ node = Node(log)
+ self._root_node.add_child(node)
+
+ def refresh(self):
+ self.clear()
+ self.beginResetModel()
+ if self.dbcon:
+ result = self.dbcon.find({})
+ for item in result:
+ self.add_log(item)
+ self.endResetModel()
+
+
+ def data(self, index, role):
+ if not index.isValid():
+ return None
+
+ if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
+ node = index.internalPointer()
+ column = index.column()
+
+ key = self.COLUMNS[column]
+ if key == "timestamp":
+ return str(node.get(key, None))
+ return node.get(key, None)
+
+ if role == self.NodeRole:
+ return index.internalPointer()
+
+ def index(self, row, column, parent):
+ """Return index for row/column under parent"""
+
+ if not parent.isValid():
+ parent_node = self._root_node
+ else:
+ parent_node = parent.internalPointer()
+
+ child_item = parent_node.child(row)
+ if child_item:
+ return self.createIndex(row, column, child_item)
+ else:
+ return QtCore.QModelIndex()
+
+ def rowCount(self, parent):
+ node = self._root_node
+ if parent.isValid():
+ node = parent.internalPointer()
+ return node.childCount()
+
+ def columnCount(self, parent):
+ return len(self.COLUMNS)
+
+ def parent(self, index):
+ return QtCore.QModelIndex()
+
+ def headerData(self, section, orientation, role):
+ if role == QtCore.Qt.DisplayRole:
+ if section < len(self.COLUMNS):
+ key = self.COLUMNS[section]
+ return self.colums_mapping.get(key, key)
+
+ super(LogModel, self).headerData(section, orientation, role)
+
+ def flags(self, index):
+ return (QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable)
+
+ def clear(self):
+ self.beginResetModel()
+ self._root_node = Node()
+ self.endResetModel()
+
+
+class Node(dict):
+ """A node that can be represented in a tree view.
+
+ The node can store data just like a dictionary.
+
+ >>> data = {"name": "John", "score": 10}
+ >>> node = Node(data)
+ >>> assert node["name"] == "John"
+
+ """
+
+ def __init__(self, data=None):
+ super(Node, self).__init__()
+
+ self._children = list()
+ self._parent = None
+
+ if data is not None:
+ assert isinstance(data, dict)
+ self.update(data)
+
+ def childCount(self):
+ return len(self._children)
+
+ def child(self, row):
+ if row >= len(self._children):
+ log.warning("Invalid row as child: {0}".format(row))
+ return
+
+ return self._children[row]
+
+ def children(self):
+ return self._children
+
+ def parent(self):
+ return self._parent
+
+ def row(self):
+ """
+ Returns:
+ int: Index of this node under parent"""
+ if self._parent is not None:
+ siblings = self.parent().children()
+ return siblings.index(self)
+
+ def add_child(self, child):
+ """Add a child to this node"""
+ child._parent = self
+ self._children.append(child)
diff --git a/pype/logging/gui/widgets.py b/pype/logging/gui/widgets.py
new file mode 100644
index 0000000000..66692c2c65
--- /dev/null
+++ b/pype/logging/gui/widgets.py
@@ -0,0 +1,426 @@
+import datetime
+import inspect
+from Qt import QtCore, QtWidgets, QtGui
+from PyQt5.QtCore import QVariant
+from .models import LogModel
+
+from .lib import preserve_states
+
+
+class SearchComboBox(QtWidgets.QComboBox):
+ """Searchable ComboBox with empty placeholder value as first value"""
+
+ def __init__(self, parent=None, placeholder=""):
+ super(SearchComboBox, self).__init__(parent)
+
+ self.setEditable(True)
+ self.setInsertPolicy(self.NoInsert)
+ self.lineEdit().setPlaceholderText(placeholder)
+
+ # Apply completer settings
+ completer = self.completer()
+ completer.setCompletionMode(completer.PopupCompletion)
+ completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
+
+ # Force style sheet on popup menu
+ # It won't take the parent stylesheet for some reason
+ # todo: better fix for completer popup stylesheet
+ if parent:
+ popup = completer.popup()
+ popup.setStyleSheet(parent.styleSheet())
+
+ self.currentIndexChanged.connect(self.onIndexChange)
+
+ def onIndexChange(self, index):
+ print(index)
+
+ def populate(self, items):
+ self.clear()
+ self.addItems([""]) # ensure first item is placeholder
+ self.addItems(items)
+
+ def get_valid_value(self):
+ """Return the current text if it's a valid value else None
+
+ Note: The empty placeholder value is valid and returns as ""
+
+ """
+
+ text = self.currentText()
+ lookup = set(self.itemText(i) for i in range(self.count()))
+ if text not in lookup:
+ return None
+
+ return text
+
+class CheckableComboBox2(QtWidgets.QComboBox):
+ def __init__(self, parent=None):
+ super(CheckableComboBox, self).__init__(parent)
+ self.view().pressed.connect(self.handleItemPressed)
+ self._changed = False
+
+ def handleItemPressed(self, index):
+ item = self.model().itemFromIndex(index)
+ if item.checkState() == QtCore.Qt.Checked:
+ item.setCheckState(QtCore.Qt.Unchecked)
+ else:
+ item.setCheckState(QtCore.Qt.Checked)
+ self._changed = True
+
+ def hidePopup(self):
+ if not self._changed:
+ super(CheckableComboBox, self).hidePopup()
+ self._changed = False
+
+ def itemChecked(self, index):
+ item = self.model().item(index, self.modelColumn())
+ return item.checkState() == QtCore.Qt.Checked
+
+ def setItemChecked(self, index, checked=True):
+ item = self.model().item(index, self.modelColumn())
+ if checked:
+ item.setCheckState(QtCore.Qt.Checked)
+ else:
+ item.setCheckState(QtCore.Qt.Unchecked)
+
+
+class SelectableMenu(QtWidgets.QMenu):
+
+ selection_changed = QtCore.Signal()
+
+ def mouseReleaseEvent(self, event):
+ action = self.activeAction()
+ if action and action.isEnabled():
+ action.trigger()
+ self.selection_changed.emit()
+ else:
+ super(SelectableMenu, self).mouseReleaseEvent(event)
+
+class CustomCombo(QtWidgets.QWidget):
+
+ selection_changed = QtCore.Signal()
+
+ def __init__(self, title, parent=None):
+ super(CustomCombo, self).__init__(parent)
+ toolbutton = QtWidgets.QToolButton(self)
+ toolbutton.setText(title)
+
+ toolmenu = SelectableMenu(self)
+
+ toolbutton.setMenu(toolmenu)
+ toolbutton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
+
+ layout = QtWidgets.QHBoxLayout()
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.addWidget(toolbutton)
+
+ self.setLayout(layout)
+
+ # toolmenu.selection_changed.connect(self.on_selection_changed)
+ toolmenu.selection_changed.connect(self.selection_changed)
+
+ self.toolbutton = toolbutton
+ self.toolmenu = toolmenu
+ self.main_layout = layout
+
+ def populate(self, items):
+ self.toolmenu.clear()
+ self.addItems(items)
+
+ def addItems(self, items):
+ for item in items:
+ action = self.toolmenu.addAction(item)
+ action.setCheckable(True)
+ action.setChecked(True)
+ self.toolmenu.addAction(action)
+
+ def items(self):
+ for action in self.toolmenu.actions():
+ yield action
+
+
+class CheckableComboBox(QtWidgets.QComboBox):
+ def __init__(self, parent=None):
+ super(CheckableComboBox, self).__init__(parent)
+
+ view = QtWidgets.QTreeView()
+ view.header().hide()
+ view.setRootIsDecorated(False)
+
+ model = QtGui.QStandardItemModel()
+
+ view.pressed.connect(self.handleItemPressed)
+ self._changed = False
+
+ self.setView(view)
+ self.setModel(model)
+
+ self.view = view
+ self.model = model
+
+ def handleItemPressed(self, index):
+ item = self.model.itemFromIndex(index)
+ if item.checkState() == QtCore.Qt.Checked:
+ item.setCheckState(QtCore.Qt.Unchecked)
+ else:
+ item.setCheckState(QtCore.Qt.Checked)
+ self._changed = True
+
+ def hidePopup(self):
+ if not self._changed:
+ super(CheckableComboBox, self).hidePopup()
+ self._changed = False
+
+ def itemChecked(self, index):
+ item = self.model.item(index, self.modelColumn())
+ return item.checkState() == QtCore.Qt.Checked
+
+ def setItemChecked(self, index, checked=True):
+ item = self.model.item(index, self.modelColumn())
+ if checked:
+ item.setCheckState(QtCore.Qt.Checked)
+ else:
+ item.setCheckState(QtCore.Qt.Unchecked)
+
+ def addItems(self, items):
+ for text, checked in items:
+ text_item = QtGui.QStandardItem(text)
+ checked_item = QtGui.QStandardItem()
+ checked_item.setData(QVariant(checked), QtCore.Qt.CheckStateRole)
+ self.model.appendRow([text_item, checked_item])
+
+
+class LogsWidget(QtWidgets.QWidget):
+ """A widget that lists the published subsets for an asset"""
+
+ active_changed = QtCore.Signal()
+
+ def __init__(self, parent=None):
+ super(LogsWidget, self).__init__(parent=parent)
+
+ model = LogModel()
+
+ filter_layout = QtWidgets.QHBoxLayout()
+
+ # user_filter = SearchComboBox(self, "Users")
+ user_filter = CustomCombo("Users", self)
+ users = model.dbcon.distinct("user")
+ user_filter.populate(users)
+ user_filter.selection_changed.connect(self.user_changed)
+
+ level_filter = CustomCombo("Levels", self)
+ # levels = [(level, True) for level in model.dbcon.distinct("level")]
+ levels = model.dbcon.distinct("level")
+ level_filter.addItems(levels)
+
+ date_from_label = QtWidgets.QLabel("From:")
+ date_filter_from = QtWidgets.QDateTimeEdit()
+
+ date_from_layout = QtWidgets.QVBoxLayout()
+ date_from_layout.addWidget(date_from_label)
+ date_from_layout.addWidget(date_filter_from)
+
+ # now = datetime.datetime.now()
+ # QtCore.QDateTime(now.year, now.month, now.day, now.hour, now.minute, second = 0, msec = 0, timeSpec = 0)
+ date_to_label = QtWidgets.QLabel("To:")
+ date_filter_to = QtWidgets.QDateTimeEdit()
+
+ date_to_layout = QtWidgets.QVBoxLayout()
+ date_to_layout.addWidget(date_to_label)
+ date_to_layout.addWidget(date_filter_to)
+
+ filter_layout.addWidget(user_filter)
+ filter_layout.addWidget(level_filter)
+
+ filter_layout.addLayout(date_from_layout)
+ filter_layout.addLayout(date_to_layout)
+
+ view = QtWidgets.QTreeView(self)
+ view.setAllColumnsShowFocus(True)
+
+ # # Set view delegates
+ # time_delegate = PrettyTimeDelegate()
+ # column = model.COLUMNS.index("time")
+ # view.setItemDelegateForColumn(column, time_delegate)
+
+ layout = QtWidgets.QVBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.addLayout(filter_layout)
+ layout.addWidget(view)
+
+ view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+ view.setSortingEnabled(True)
+ view.sortByColumn(
+ model.COLUMNS.index("timestamp"),
+ QtCore.Qt.AscendingOrder
+ )
+
+ view.setModel(model)
+
+ view.customContextMenuRequested.connect(self.on_context_menu)
+ view.selectionModel().selectionChanged.connect(self.active_changed)
+ # user_filter.connect()
+
+ # TODO remove if nothing will affect...
+ # header = self.view.header()
+ # # Enforce the columns to fit the data (purely cosmetic)
+ # if Qt.__binding__ in ("PySide2", "PyQt5"):
+ # header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
+ # else:
+ # header.setResizeMode(QtWidgets.QHeaderView.ResizeToContents)
+
+ # Set signals
+
+ # prepare
+ model.refresh()
+
+ # Store to memory
+ self.model = model
+ self.view = view
+
+ self.user_filter = user_filter
+
+ def user_changed(self):
+ for action in self.user_filter.items():
+ print(action)
+
+ def on_context_menu(self, point):
+ # TODO will be any actions? it's ready
+ return
+
+ point_index = self.view.indexAt(point)
+ if not point_index.isValid():
+ return
+
+ # Get selected subsets without groups
+ selection = self.view.selectionModel()
+ rows = selection.selectedRows(column=0)
+
+ def selected_log(self):
+ selection = self.view.selectionModel()
+ rows = selection.selectedRows(column=0)
+ if len(rows) == 1:
+ return rows[0]
+
+ return None
+
+
+class LogDetailWidget(QtWidgets.QWidget):
+ """A Widget that display information about a specific version"""
+ data_rows = [
+ "user",
+ "message",
+ "level",
+ "logname",
+ "method",
+ "module",
+ "fileName",
+ "lineNumber",
+ "host",
+ "timestamp"
+ ]
+
+ html_text = u"""
+
{user} - {timestamp}
+User
{user}
+
Level
{level}
+
Message
{message}
+
Log Name
{logname}
Method
{method}
+
File
{fileName}
+
Line
{lineNumber}
+
Host
{host}
+
Timestamp
{timestamp}
+"""
+
+ def __init__(self, parent=None):
+ super(LogDetailWidget, self).__init__(parent=parent)
+
+ layout = QtWidgets.QVBoxLayout(self)
+
+ label = QtWidgets.QLabel("Detail")
+ detail_widget = LogDetailTextEdit()
+ detail_widget.setReadOnly(True)
+ layout.addWidget(label)
+ layout.addWidget(detail_widget)
+
+ self.detail_widget = detail_widget
+
+ self.setEnabled(True)
+
+ self.set_detail(None)
+
+ def set_detail(self, detail_data):
+ if not detail_data:
+ self.detail_widget.setText("")
+ return
+
+ data = dict()
+ for row in self.data_rows:
+ value = detail_data.get(row) or "< Not set >"
+ data[row] = value
+
+
+ self.detail_widget.setHtml(self.html_text.format(**data))
+
+
+class LogDetailTextEdit(QtWidgets.QTextEdit):
+ """QTextEdit that displays version specific information.
+
+ This also overrides the context menu to add actions like copying
+ source path to clipboard or copying the raw data of the version
+ to clipboard.
+
+ """
+ def __init__(self, parent=None):
+ super(LogDetailTextEdit, self).__init__(parent=parent)
+
+ # self.data = {
+ # "source": None,
+ # "raw": None
+ # }
+ #
+ # def contextMenuEvent(self, event):
+ # """Context menu with additional actions"""
+ # menu = self.createStandardContextMenu()
+ #
+ # # Add additional actions when any text so we can assume
+ # # the version is set.
+ # if self.toPlainText().strip():
+ #
+ # menu.addSeparator()
+ # action = QtWidgets.QAction("Copy source path to clipboard",
+ # menu)
+ # action.triggered.connect(self.on_copy_source)
+ # menu.addAction(action)
+ #
+ # action = QtWidgets.QAction("Copy raw data to clipboard",
+ # menu)
+ # action.triggered.connect(self.on_copy_raw)
+ # menu.addAction(action)
+ #
+ # menu.exec_(event.globalPos())
+ # del menu
+ #
+ # def on_copy_source(self):
+ # """Copy formatted source path to clipboard"""
+ # source = self.data.get("source", None)
+ # if not source:
+ # return
+ #
+ # # path = source.format(root=api.registered_root())
+ # # clipboard = QtWidgets.QApplication.clipboard()
+ # # clipboard.setText(path)
+ #
+ # def on_copy_raw(self):
+ # """Copy raw version data to clipboard
+ #
+ # The data is string formatted with `pprint.pformat`.
+ #
+ # """
+ # raw = self.data.get("raw", None)
+ # if not raw:
+ # return
+ #
+ # raw_text = pprint.pformat(raw)
+ # clipboard = QtWidgets.QApplication.clipboard()
+ # clipboard.setText(raw_text)
diff --git a/pype/logging/tray/__init__.py b/pype/logging/tray/__init__.py
new file mode 100644
index 0000000000..a2586155e7
--- /dev/null
+++ b/pype/logging/tray/__init__.py
@@ -0,0 +1,5 @@
+from .logging_module import LoggingModule
+
+
+def tray_init(tray_widget, main_widget):
+ return LoggingModule(main_widget, tray_widget)
diff --git a/pype/logging/tray/logging_module.py b/pype/logging/tray/logging_module.py
new file mode 100644
index 0000000000..30f55cd680
--- /dev/null
+++ b/pype/logging/tray/logging_module.py
@@ -0,0 +1,36 @@
+import os
+from Qt import QtWidgets
+
+from pypeapp import Logger
+
+from ..gui.app import LogsWindow
+
+log = Logger().get_logger("LoggingModule", "logging")
+
+
+class LoggingModule:
+ def __init__(self, main_parent=None, parent=None):
+ self.parent = parent
+
+ self.window = LogsWindow()
+
+ # Definition of Tray menu
+ def tray_menu(self, parent_menu):
+ # Menu for Tray App
+ menu = QtWidgets.QMenu('Logging', parent_menu)
+ # menu.setProperty('submenu', 'on')
+
+ show_action = QtWidgets.QAction("Show Logs", menu)
+ show_action.triggered.connect(self.on_show_logs)
+ menu.addAction(show_action)
+
+ parent_menu.addMenu(menu)
+
+ def tray_start(self):
+ pass
+
+ def process_modules(self, modules):
+ return
+
+ def on_show_logs(self):
+ self.window.show()
diff --git a/pype/plugins/ftrack/publish/integrate_remove_components.py b/pype/plugins/ftrack/publish/integrate_remove_components.py
index a215ee1b97..bad50f7200 100644
--- a/pype/plugins/ftrack/publish/integrate_remove_components.py
+++ b/pype/plugins/ftrack/publish/integrate_remove_components.py
@@ -17,6 +17,9 @@ class IntegrateCleanComponentData(pyblish.api.InstancePlugin):
for comp in instance.data['representations']:
self.log.debug('component {}'.format(comp))
+
+ if "%" in comp['published_path'] or "#" in comp['published_path']:
+ continue
if comp.get('thumbnail') or ("thumbnail" in comp.get('tags', [])):
os.remove(comp['published_path'])
diff --git a/pype/plugins/global/publish/collect_anatomy.py b/pype/plugins/global/publish/collect_anatomy.py
new file mode 100644
index 0000000000..9412209850
--- /dev/null
+++ b/pype/plugins/global/publish/collect_anatomy.py
@@ -0,0 +1,20 @@
+"""
+Requires:
+ None
+Provides:
+ context -> anatomy (pypeapp.Anatomy)
+"""
+
+from pypeapp import Anatomy
+import pyblish.api
+
+
+class CollectAnatomy(pyblish.api.ContextPlugin):
+ """Collect Anatomy into Context"""
+
+ order = pyblish.api.CollectorOrder
+ label = "Collect Anatomy"
+
+ def process(self, context):
+ context.data['anatomy'] = Anatomy()
+ self.log.info("Anatomy templates collected...")
diff --git a/pype/plugins/global/publish/collect_comment.py b/pype/plugins/global/publish/collect_comment.py
index 5bbd1da2a1..22970665a1 100644
--- a/pype/plugins/global/publish/collect_comment.py
+++ b/pype/plugins/global/publish/collect_comment.py
@@ -1,3 +1,10 @@
+"""
+Requires:
+ None
+Provides:
+ context -> comment (str)
+"""
+
import pyblish.api
diff --git a/pype/plugins/global/publish/collect_context.py b/pype/plugins/global/publish/collect_context.py
index 31ab95259c..5f443ac5fb 100644
--- a/pype/plugins/global/publish/collect_context.py
+++ b/pype/plugins/global/publish/collect_context.py
@@ -1,3 +1,18 @@
+"""
+Requires:
+ environment -> SAPUBLISH_INPATH
+ environment -> SAPUBLISH_OUTPATH
+
+Provides:
+ context -> returnJsonPath (str)
+ context -> project
+ context -> asset
+ instance -> destination_list (list)
+ instance -> representations (list)
+ instance -> source (list)
+ instance -> representations
+"""
+
import os
import pyblish.api
from avalon import io
@@ -31,9 +46,25 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin):
in_data = json.load(f)
asset_name = in_data['asset']
+ family_preset_key = in_data.get('family_preset_key', '')
family = in_data['family']
subset = in_data['subset']
+ # Load presets
+ presets = context.data.get("presets")
+ if not presets:
+ from pypeapp import config
+ presets = config.get_presets()
+
+ # Get from presets anatomy key that will be used for getting template
+ # - default integrate new is used if not set
+ anatomy_key = presets.get(
+ "standalone_publish", {}).get(
+ "families", {}).get(
+ family_preset_key, {}).get(
+ "anatomy_template"
+ )
+
project = io.find_one({'type': 'project'})
asset = io.find_one({
'type': 'asset',
@@ -50,6 +81,8 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin):
"label": subset,
"name": subset,
"family": family,
+ "frameStart": in_data.get("representations", [None])[0].get("frameStart", None),
+ "frameEnd": in_data.get("representations", [None])[0].get("frameEnd", None),
"families": [family, 'ftrack'],
})
self.log.info("collected instance: {}".format(instance.data))
@@ -63,7 +96,9 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin):
component['destination'] = component['files']
component['stagingDir'] = component['stagingDir']
- component['anatomy_template'] = 'render'
+ # Do not set anatomy_template if not specified
+ if anatomy_key:
+ component['anatomy_template'] = anatomy_key
if isinstance(component['files'], list):
collections, remainder = clique.assemble(component['files'])
self.log.debug("collecting sequence: {}".format(collections))
diff --git a/pype/plugins/global/publish/collect_context_label.py b/pype/plugins/global/publish/collect_context_label.py
index ec8e0f7cdc..9c07d7de5b 100644
--- a/pype/plugins/global/publish/collect_context_label.py
+++ b/pype/plugins/global/publish/collect_context_label.py
@@ -1,3 +1,10 @@
+"""
+Requires:
+ context -> currentFile (str)
+Provides:
+ context -> label (str)
+"""
+
import os
import pyblish.api
diff --git a/pype/plugins/global/publish/collect_current_shell_file.py b/pype/plugins/global/publish/collect_current_shell_file.py
index a467459bc8..961cad86a1 100644
--- a/pype/plugins/global/publish/collect_current_shell_file.py
+++ b/pype/plugins/global/publish/collect_current_shell_file.py
@@ -1,3 +1,11 @@
+"""
+Requires:
+ None
+
+Provides:
+ context -> currentFile (str)
+"""
+
import os
import pyblish.api
diff --git a/pype/plugins/global/publish/collect_deadline_user.py b/pype/plugins/global/publish/collect_deadline_user.py
index 624e455251..125f9d0d26 100644
--- a/pype/plugins/global/publish/collect_deadline_user.py
+++ b/pype/plugins/global/publish/collect_deadline_user.py
@@ -1,3 +1,11 @@
+"""
+Requires:
+ environment -> DEADLINE_PATH
+
+Provides:
+ context -> deadlineUser (str)
+"""
+
import os
import subprocess
@@ -54,4 +62,3 @@ class CollectDeadlineUser(pyblish.api.ContextPlugin):
self.log.info("Found Deadline user: {}".format(user))
context.data['deadlineUser'] = user
-
diff --git a/pype/plugins/global/publish/collect_filesequences.py b/pype/plugins/global/publish/collect_filesequences.py
index 33531549cb..73f3a459c8 100644
--- a/pype/plugins/global/publish/collect_filesequences.py
+++ b/pype/plugins/global/publish/collect_filesequences.py
@@ -1,3 +1,13 @@
+"""
+Requires:
+ environment -> PYPE_PUBLISH_PATHS
+ context -> workspaceDir
+
+Provides:
+ context -> user (str)
+ instance -> new instance
+"""
+
import os
import re
import copy
diff --git a/pype/plugins/global/publish/collect_machine_name.py b/pype/plugins/global/publish/collect_machine_name.py
index 02360cff04..72ef68f8ed 100644
--- a/pype/plugins/global/publish/collect_machine_name.py
+++ b/pype/plugins/global/publish/collect_machine_name.py
@@ -1,3 +1,11 @@
+"""
+Requires:
+ none
+
+Provides:
+ context -> machine (str)
+"""
+
import pyblish.api
diff --git a/pype/plugins/global/publish/collect_output_repre_config.py b/pype/plugins/global/publish/collect_output_repre_config.py
index 5595e29cab..73ab050bcf 100644
--- a/pype/plugins/global/publish/collect_output_repre_config.py
+++ b/pype/plugins/global/publish/collect_output_repre_config.py
@@ -1,5 +1,11 @@
-import os
-import json
+"""
+Requires:
+ config_data -> ftrack.output_representation
+
+Provides:
+ context -> output_repre_config (str)
+"""
+
import pyblish.api
from pypeapp import config
@@ -9,7 +15,7 @@ class CollectOutputRepreConfig(pyblish.api.ContextPlugin):
order = pyblish.api.CollectorOrder
label = "Collect Config for representation"
- hosts = ["shell"]
+ hosts = ["shell", "standalonepublisher"]
def process(self, context):
config_data = config.get_presets()["ftrack"]["output_representation"]
diff --git a/pype/plugins/global/publish/collect_presets.py b/pype/plugins/global/publish/collect_presets.py
index 7e0d3e2f4b..5e79c555e2 100644
--- a/pype/plugins/global/publish/collect_presets.py
+++ b/pype/plugins/global/publish/collect_presets.py
@@ -1,3 +1,12 @@
+"""
+Requires:
+ config_data -> colorspace.default
+ config_data -> dataflow.default
+
+Provides:
+ context -> presets
+"""
+
from pyblish import api
from pypeapp import config
@@ -5,7 +14,7 @@ from pypeapp import config
class CollectPresets(api.ContextPlugin):
"""Collect Presets."""
- order = api.CollectorOrder
+ order = api.CollectorOrder - 0.491
label = "Collect Presets"
def process(self, context):
diff --git a/pype/plugins/global/publish/collect_project_data.py b/pype/plugins/global/publish/collect_project_data.py
index de51ad880c..acdbc2c41f 100644
--- a/pype/plugins/global/publish/collect_project_data.py
+++ b/pype/plugins/global/publish/collect_project_data.py
@@ -1,8 +1,15 @@
+"""
+Requires:
+ None
+
+Provides:
+ context -> projectData
+"""
+
import pyblish.api
import pype.api as pype
-
class CollectProjectData(pyblish.api.ContextPlugin):
"""Collecting project data from avalon db"""
diff --git a/pype/plugins/global/publish/collect_scene_version.py b/pype/plugins/global/publish/collect_scene_version.py
index 12075e2417..0d76015909 100644
--- a/pype/plugins/global/publish/collect_scene_version.py
+++ b/pype/plugins/global/publish/collect_scene_version.py
@@ -13,6 +13,8 @@ class CollectSceneVersion(pyblish.api.ContextPlugin):
label = 'Collect Version'
def process(self, context):
+ if "standalonepublisher" in context.data.get("host", []):
+ return
filename = os.path.basename(context.data.get('currentFile'))
diff --git a/pype/plugins/global/publish/collect_templates.py b/pype/plugins/global/publish/collect_templates.py
index fe48e97c03..f65433380b 100644
--- a/pype/plugins/global/publish/collect_templates.py
+++ b/pype/plugins/global/publish/collect_templates.py
@@ -1,16 +1,87 @@
+"""
+Requires:
+ session -> AVALON_PROJECT
+ context -> anatomy (pypeapp.Anatomy)
+ instance -> subset
+ instance -> asset
+ instance -> family
-import pype.api as pype
-from pypeapp import Anatomy
+Provides:
+ instance -> template
+ instance -> assumedTemplateData
+ instance -> assumedDestination
+"""
+import os
+
+from avalon import io, api
import pyblish.api
-class CollectTemplates(pyblish.api.ContextPlugin):
- """Inject the current working file into context"""
+class CollectTemplates(pyblish.api.InstancePlugin):
+ """Fill templates with data needed for publish"""
- order = pyblish.api.CollectorOrder
- label = "Collect Templates"
+ order = pyblish.api.CollectorOrder + 0.1
+ label = "Collect and fill Templates"
+ hosts = ["maya", "nuke", "standalonepublisher"]
- def process(self, context):
- context.data['anatomy'] = Anatomy()
- self.log.info("Anatomy templates collected...")
+ def process(self, instance):
+ # get all the stuff from the database
+ subset_name = instance.data["subset"]
+ asset_name = instance.data["asset"]
+ project_name = api.Session["AVALON_PROJECT"]
+
+ project = io.find_one({"type": "project",
+ "name": project_name},
+ projection={"config": True, "data": True})
+
+ template = project["config"]["template"]["publish"]
+ anatomy = instance.context.data['anatomy']
+
+ asset = io.find_one({"type": "asset",
+ "name": asset_name,
+ "parent": project["_id"]})
+
+ assert asset, ("No asset found by the name '{}' "
+ "in project '{}'".format(asset_name, project_name))
+ silo = asset['silo']
+
+ subset = io.find_one({"type": "subset",
+ "name": subset_name,
+ "parent": asset["_id"]})
+
+ # assume there is no version yet, we start at `1`
+ version = None
+ version_number = 1
+ if subset is not None:
+ version = io.find_one({"type": "version",
+ "parent": subset["_id"]},
+ sort=[("name", -1)])
+
+ # if there is a subset there ought to be version
+ if version is not None:
+ version_number += int(version["name"])
+
+ hierarchy = asset['data']['parents']
+ if hierarchy:
+ # hierarchy = os.path.sep.join(hierarchy)
+ hierarchy = os.path.join(*hierarchy)
+
+ template_data = {"root": api.Session["AVALON_PROJECTS"],
+ "project": {"name": project_name,
+ "code": project['data']['code']},
+ "silo": silo,
+ "family": instance.data['family'],
+ "asset": asset_name,
+ "subset": subset_name,
+ "version": version_number,
+ "hierarchy": hierarchy,
+ "representation": "TEMP"}
+
+ instance.data["template"] = template
+ instance.data["assumedTemplateData"] = template_data
+
+ # We take the parent folder of representation 'filepath'
+ instance.data["assumedDestination"] = os.path.dirname(
+ (anatomy.format(template_data))["publish"]["path"]
+ )
diff --git a/pype/plugins/global/publish/extract_thumbnail_sa.py b/pype/plugins/global/publish/extract_thumbnail_sa.py
new file mode 100644
index 0000000000..f42985b560
--- /dev/null
+++ b/pype/plugins/global/publish/extract_thumbnail_sa.py
@@ -0,0 +1,126 @@
+import os
+import tempfile
+import subprocess
+import pyblish.api
+import pype.api
+
+
+class ExtractThumbnail(pyblish.api.InstancePlugin):
+ """Extract jpeg thumbnail from component input from standalone publisher
+
+ Uses jpeg file from component if possible (when single or multiple jpegs
+ are loaded to component selected as thumbnail) otherwise extracts from
+ input file/s single jpeg to temp.
+ """
+
+ label = "Extract Thumbnail"
+ hosts = ["standalonepublisher"]
+ order = pyblish.api.ExtractorOrder
+
+ def process(self, instance):
+ repres = instance.data.get('representations')
+ if not repres:
+ return
+
+ thumbnail_repre = None
+ for repre in repres:
+ if repre.get("thumbnail"):
+ thumbnail_repre = repre
+ break
+
+ if not thumbnail_repre:
+ return
+
+ files = thumbnail_repre.get("files")
+ if not files:
+ return
+
+ if isinstance(files, list):
+ files_len = len(files)
+ file = str(files[0])
+ else:
+ files_len = 1
+ file = files
+
+ is_jpeg = False
+ if file.endswith(".jpeg") or file.endswith(".jpg"):
+ is_jpeg = True
+
+ if is_jpeg and files_len == 1:
+ # skip if already is single jpeg file
+ return
+
+ elif is_jpeg:
+ # use first frame as thumbnail if is sequence of jpegs
+ full_thumbnail_path = file
+ self.log.info(
+ "For thumbnail is used file: {}".format(full_thumbnail_path)
+ )
+
+ else:
+ # Convert to jpeg if not yet
+ full_input_path = os.path.join(thumbnail_repre["stagingDir"], file)
+ self.log.info("input {}".format(full_input_path))
+
+ full_thumbnail_path = tempfile.mkstemp(suffix=".jpg")[1]
+ self.log.info("output {}".format(full_thumbnail_path))
+
+ config_data = instance.context.data.get("output_repre_config", {})
+
+ proj_name = os.environ.get("AVALON_PROJECT", "__default__")
+ profile = config_data.get(
+ proj_name,
+ config_data.get("__default__", {})
+ )
+
+ ffmpeg_path = os.getenv("FFMPEG_PATH", "")
+ if ffmpeg_path:
+ ffmpeg_path += "/ffmpeg"
+ else:
+ ffmpeg_path = "ffmpeg"
+
+ jpeg_items = []
+ jpeg_items.append(ffmpeg_path)
+ # override file if already exists
+ jpeg_items.append("-y")
+ # add input filters from peresets
+ if profile:
+ jpeg_items.extend(profile.get('input', []))
+ # input file
+ jpeg_items.append("-i {}".format(full_input_path))
+ # extract only single file
+ jpeg_items.append("-vframes 1")
+ # output file
+ jpeg_items.append(full_thumbnail_path)
+
+ subprocess_jpeg = " ".join(jpeg_items)
+
+ # run subprocess
+ self.log.debug("Executing: {}".format(subprocess_jpeg))
+ subprocess.Popen(
+ subprocess_jpeg,
+ stdout=subprocess.PIPE,
+ shell=True
+ )
+
+ # remove thumbnail key from origin repre
+ thumbnail_repre.pop("thumbnail")
+
+ filename = os.path.basename(full_thumbnail_path)
+ staging_dir = os.path.dirname(full_thumbnail_path)
+
+ # create new thumbnail representation
+ representation = {
+ 'name': 'jpg',
+ 'ext': 'jpg',
+ 'files': filename,
+ "stagingDir": staging_dir,
+ "thumbnail": True,
+ "tags": []
+ }
+
+ # # add Delete tag when temp file was rendered
+ # if not is_jpeg:
+ # representation["tags"].append("delete")
+
+ instance.data["representations"].append(representation)
diff --git a/pype/plugins/global/publish/integrate_assumed_destination.py b/pype/plugins/global/publish/integrate_assumed_destination.py
index 6999ce6ab8..3bbd4cf33b 100644
--- a/pype/plugins/global/publish/integrate_assumed_destination.py
+++ b/pype/plugins/global/publish/integrate_assumed_destination.py
@@ -30,7 +30,8 @@ class IntegrateAssumedDestination(pyblish.api.InstancePlugin):
"resources")
# Clean the path
- mock_destination = os.path.abspath(os.path.normpath(mock_destination)).replace("\\", "/")
+ mock_destination = os.path.abspath(
+ os.path.normpath(mock_destination)).replace("\\", "/")
# Define resource destination and transfers
resources = instance.data.get("resources", list())
@@ -38,7 +39,8 @@ class IntegrateAssumedDestination(pyblish.api.InstancePlugin):
for resource in resources:
# Add destination to the resource
- source_filename = os.path.basename(resource["source"]).replace("\\", "/")
+ source_filename = os.path.basename(
+ resource["source"]).replace("\\", "/")
destination = os.path.join(mock_destination, source_filename)
# Force forward slashes to fix issue with software unable
@@ -53,7 +55,8 @@ class IntegrateAssumedDestination(pyblish.api.InstancePlugin):
files = resource['files']
for fsrc in files:
fname = os.path.basename(fsrc)
- fdest = os.path.join(mock_destination, fname).replace("\\", "/")
+ fdest = os.path.join(
+ mock_destination, fname).replace("\\", "/")
transfers.append([fsrc, fdest])
instance.data["resources"] = resources
diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py
index e5d8007d70..61881b2a34 100644
--- a/pype/plugins/global/publish/integrate_new.py
+++ b/pype/plugins/global/publish/integrate_new.py
@@ -307,7 +307,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
if repre.get("frameStart"):
frame_start_padding = len(str(
repre.get("frameEnd")))
- index_frame_start = repre.get("frameStart")
+ index_frame_start = int(repre.get("frameStart"))
dst_padding_exp = src_padding_exp
for i in src_collection.indexes:
@@ -322,7 +322,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
dst_padding = dst_padding_exp % index_frame_start
index_frame_start += 1
- dst = "{0}{1}{2}".format(dst_head, dst_padding, dst_tail)
+ dst = "{0}{1}{2}".format(dst_head, dst_padding, dst_tail).replace("..", ".")
self.log.debug("destination: `{}`".format(dst))
src = os.path.join(stagingdir, src_file_name)
self.log.debug("source: {}".format(src))
@@ -357,7 +357,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
src = os.path.join(stagingdir, fname)
anatomy_filled = anatomy.format(template_data)
dst = os.path.normpath(
- anatomy_filled[template_name]["path"])
+ anatomy_filled[template_name]["path"]).replace("..", ".")
instance.data["transfers"].append([src, dst])
@@ -440,6 +440,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
Returns:
None
"""
+ src = os.path.normpath(src)
+ dst = os.path.normpath(dst)
self.log.debug("Copying file .. {} -> {}".format(src, dst))
dirname = os.path.dirname(dst)
diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py
index 8d352b8872..b812fb16e6 100644
--- a/pype/plugins/global/publish/submit_publish_job.py
+++ b/pype/plugins/global/publish/submit_publish_job.py
@@ -1,7 +1,6 @@
import os
import json
import re
-from pprint import pprint
import logging
from avalon import api, io
@@ -147,7 +146,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
"PYPE_ROOT"
]
-
def _submit_deadline_post_job(self, instance, job):
"""
Deadline specific code separated from :meth:`process` for sake of
@@ -192,7 +190,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
# Transfer the environment from the original job to this dependent
# job so they use the same environment
-
environment = job["Props"].get("Env", {})
i = 0
for index, key in enumerate(environment):
@@ -295,7 +292,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
# Optional metadata (for debugging)
"metadata": {
"instance": data,
- "job": job,
+ "job": render_job,
"session": api.Session.copy()
}
}
diff --git a/pype/plugins/nuke/publish/collect_active_viewer.py b/pype/plugins/nuke/_publish_unused/collect_active_viewer.py
similarity index 83%
rename from pype/plugins/nuke/publish/collect_active_viewer.py
rename to pype/plugins/nuke/_publish_unused/collect_active_viewer.py
index 5dc17d8768..5a6cc02b88 100644
--- a/pype/plugins/nuke/publish/collect_active_viewer.py
+++ b/pype/plugins/nuke/_publish_unused/collect_active_viewer.py
@@ -11,5 +11,4 @@ class CollectActiveViewer(pyblish.api.ContextPlugin):
hosts = ["nuke"]
def process(self, context):
- context.data["ViewerProcess"] = nuke.ViewerProcess.node()
context.data["ActiveViewer"] = nuke.activeViewer()
diff --git a/pype/plugins/nuke/publish/validate_active_viewer.py b/pype/plugins/nuke/_publish_unused/validate_active_viewer.py
similarity index 65%
rename from pype/plugins/nuke/publish/validate_active_viewer.py
rename to pype/plugins/nuke/_publish_unused/validate_active_viewer.py
index bcf7cab6b3..618a7f1502 100644
--- a/pype/plugins/nuke/publish/validate_active_viewer.py
+++ b/pype/plugins/nuke/_publish_unused/validate_active_viewer.py
@@ -16,3 +16,9 @@ class ValidateActiveViewer(pyblish.api.ContextPlugin):
assert viewer_process_node, (
"Missing active viewer process! Please click on output write node and push key number 1-9"
)
+ active_viewer = context.data["ActiveViewer"]
+ active_input = active_viewer.activeInput()
+
+ assert active_input is not None, (
+ "Missing active viewer input! Please click on output write node and push key number 1-9"
+ )
diff --git a/pype/plugins/nuke/load/load_luts.py b/pype/plugins/nuke/load/load_luts.py
index 7e1302fffe..4f7c19a588 100644
--- a/pype/plugins/nuke/load/load_luts.py
+++ b/pype/plugins/nuke/load/load_luts.py
@@ -14,6 +14,7 @@ class LoadLuts(api.Loader):
order = 0
icon = "cc"
color = style.colors.light
+ ignore_attr = ["useLifetime"]
def load(self, context, name, namespace, data):
"""
@@ -83,6 +84,8 @@ class LoadLuts(api.Loader):
for ef_name, ef_val in nodes_order.items():
node = nuke.createNode(ef_val["class"])
for k, v in ef_val["node"].items():
+ if k in self.ignore_attr:
+ continue
if isinstance(v, list) and len(v) > 4:
node[k].setAnimated()
for i, value in enumerate(v):
@@ -194,6 +197,8 @@ class LoadLuts(api.Loader):
for ef_name, ef_val in nodes_order.items():
node = nuke.createNode(ef_val["class"])
for k, v in ef_val["node"].items():
+ if k in self.ignore_attr:
+ continue
if isinstance(v, list) and len(v) > 3:
node[k].setAnimated()
for i, value in enumerate(v):
diff --git a/pype/plugins/nuke/load/load_luts_ip.py b/pype/plugins/nuke/load/load_luts_ip.py
index b0a30d78e4..b30f84cc42 100644
--- a/pype/plugins/nuke/load/load_luts_ip.py
+++ b/pype/plugins/nuke/load/load_luts_ip.py
@@ -14,6 +14,7 @@ class LoadLutsInputProcess(api.Loader):
order = 0
icon = "eye"
color = style.colors.alert
+ ignore_attr = ["useLifetime"]
def load(self, context, name, namespace, data):
"""
@@ -83,6 +84,8 @@ class LoadLutsInputProcess(api.Loader):
for ef_name, ef_val in nodes_order.items():
node = nuke.createNode(ef_val["class"])
for k, v in ef_val["node"].items():
+ if k in self.ignore_attr:
+ continue
if isinstance(v, list) and len(v) > 4:
node[k].setAnimated()
for i, value in enumerate(v):
@@ -196,6 +199,8 @@ class LoadLutsInputProcess(api.Loader):
for ef_name, ef_val in nodes_order.items():
node = nuke.createNode(ef_val["class"])
for k, v in ef_val["node"].items():
+ if k in self.ignore_attr:
+ continue
if isinstance(v, list) and len(v) > 3:
node[k].setAnimated()
for i, value in enumerate(v):
diff --git a/pype/plugins/nuke/publish/extract_ouput_node.py b/pype/plugins/nuke/publish/extract_ouput_node.py
index 4d7533f010..a144761e5f 100644
--- a/pype/plugins/nuke/publish/extract_ouput_node.py
+++ b/pype/plugins/nuke/publish/extract_ouput_node.py
@@ -15,21 +15,17 @@ class CreateOutputNode(pyblish.api.ContextPlugin):
def process(self, context):
# capture selection state
with maintained_selection():
- # deselect all allNodes
- self.log.info(context.data["ActiveViewer"])
+ active_node = [node for inst in context[:]
+ for node in inst[:]
+ if "ak:family" in node.knobs()]
- active_viewer = context.data["ActiveViewer"]
- active_input = active_viewer.activeInput()
- active_node = active_viewer.node()
-
-
- last_viewer_node = active_node.input(active_input)
-
- name = last_viewer_node.name()
- self.log.info("Node name: {}".format(name))
+ if active_node:
+ self.log.info(active_node)
+ active_node = active_node[0]
+ self.log.info(active_node)
+ active_node['selected'].setValue(True)
# select only instance render node
- last_viewer_node['selected'].setValue(True)
output_node = nuke.createNode("Output")
# deselect all and select the original selection
diff --git a/pype/plugins/nuke/publish/extract_review_data.py b/pype/plugins/nuke/publish/extract_review_data.py
index 40c3e37434..791b9d7969 100644
--- a/pype/plugins/nuke/publish/extract_review_data.py
+++ b/pype/plugins/nuke/publish/extract_review_data.py
@@ -3,7 +3,6 @@ import nuke
import pyblish.api
import pype
-
class ExtractReviewData(pype.api.Extractor):
"""Extracts movie and thumbnail with baked in luts
@@ -48,9 +47,9 @@ class ExtractReviewData(pype.api.Extractor):
assert instance.data['representations'][0]['files'], "Instance data files should't be empty!"
- import nuke
temporary_nodes = []
- stagingDir = instance.data['representations'][0]["stagingDir"].replace("\\", "/")
+ stagingDir = instance.data[
+ 'representations'][0]["stagingDir"].replace("\\", "/")
self.log.debug("StagingDir `{0}`...".format(stagingDir))
collection = instance.data.get("collection", None)
@@ -70,16 +69,24 @@ class ExtractReviewData(pype.api.Extractor):
first_frame = instance.data.get("frameStart", None)
last_frame = instance.data.get("frameEnd", None)
- node = previous_node = nuke.createNode("Read")
+ rnode = nuke.createNode("Read")
- node["file"].setValue(
+ rnode["file"].setValue(
os.path.join(stagingDir, fname).replace("\\", "/"))
- node["first"].setValue(first_frame)
- node["origfirst"].setValue(first_frame)
- node["last"].setValue(last_frame)
- node["origlast"].setValue(last_frame)
- temporary_nodes.append(node)
+ rnode["first"].setValue(first_frame)
+ rnode["origfirst"].setValue(first_frame)
+ rnode["last"].setValue(last_frame)
+ rnode["origlast"].setValue(last_frame)
+ temporary_nodes.append(rnode)
+ previous_node = rnode
+
+ # get input process and connect it to baking
+ ipn = self.get_view_process_node()
+ if ipn is not None:
+ ipn.setInput(0, previous_node)
+ previous_node = ipn
+ temporary_nodes.append(ipn)
reformat_node = nuke.createNode("Reformat")
@@ -95,22 +102,10 @@ class ExtractReviewData(pype.api.Extractor):
previous_node = reformat_node
temporary_nodes.append(reformat_node)
- viewer_process_node = instance.context.data.get("ViewerProcess")
- dag_node = None
- if viewer_process_node:
- dag_node = nuke.createNode(viewer_process_node.Class())
- dag_node.setInput(0, previous_node)
- previous_node = dag_node
- temporary_nodes.append(dag_node)
- # Copy viewer process values
- excludedKnobs = ["name", "xpos", "ypos"]
- for item in viewer_process_node.knobs().keys():
- if item not in excludedKnobs and item in dag_node.knobs():
- x1 = viewer_process_node[item]
- x2 = dag_node[item]
- x2.fromScript(x1.toScript(False))
- else:
- self.log.warning("No viewer node found.")
+ dag_node = nuke.createNode("OCIODisplay")
+ dag_node.setInput(0, previous_node)
+ previous_node = dag_node
+ temporary_nodes.append(dag_node)
# create write node
write_node = nuke.createNode("Write")
@@ -164,3 +159,29 @@ class ExtractReviewData(pype.api.Extractor):
# Clean up
for node in temporary_nodes:
nuke.delete(node)
+
+ def get_view_process_node(self):
+
+ # Select only the target node
+ if nuke.selectedNodes():
+ [n.setSelected(False) for n in nuke.selectedNodes()]
+
+ ipn_orig = None
+ for v in [n for n in nuke.allNodes()
+ if "Viewer" in n.Class()]:
+ ip = v['input_process'].getValue()
+ ipn = v['input_process_node'].getValue()
+ if "VIEWER_INPUT" not in ipn and ip:
+ ipn_orig = nuke.toNode(ipn)
+ ipn_orig.setSelected(True)
+
+ if ipn_orig:
+ nuke.nodeCopy('%clipboard%')
+
+ [n.setSelected(False) for n in nuke.selectedNodes()] # Deselect all
+
+ nuke.nodePaste('%clipboard%')
+
+ ipn = nuke.selectedNode()
+
+ return ipn
diff --git a/pype/plugins/nuke/publish/validate_rendered_frames.py b/pype/plugins/nuke/publish/validate_rendered_frames.py
index 85cbe7b2c0..3887b5d5b7 100644
--- a/pype/plugins/nuke/publish/validate_rendered_frames.py
+++ b/pype/plugins/nuke/publish/validate_rendered_frames.py
@@ -81,3 +81,5 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin):
).format(__name__)
instance.data['collection'] = collection
+
+ return
diff --git a/pype/plugins/nuke/publish/validate_script.py b/pype/plugins/nuke/publish/validate_script.py
index 837e30dab7..307e3ade59 100644
--- a/pype/plugins/nuke/publish/validate_script.py
+++ b/pype/plugins/nuke/publish/validate_script.py
@@ -83,6 +83,8 @@ class ValidateScript(pyblish.api.InstancePlugin):
# Set frame range with handles
# asset_attributes["frameStart"] -= handle_start
# asset_attributes["frameEnd"] += handle_end
+ if len(str(asset_attributes["fps"])) > 4:
+ asset_attributes["fps"] = float("{0:.8f}".format(asset_attributes["fps"]))
# Get values from nukescript
script_attributes = {
diff --git a/pype/standalonepublish/publish.py b/pype/standalonepublish/publish.py
index 13b505666c..f199aaf84e 100644
--- a/pype/standalonepublish/publish.py
+++ b/pype/standalonepublish/publish.py
@@ -103,7 +103,7 @@ def avalon_api_publish(data, gui=True):
"-pp", os.pathsep.join(pyblish.api.registered_paths())
]
- os.environ["PYBLISH_HOSTS"] = "shell"
+ os.environ["PYBLISH_HOSTS"] = "standalonepublisher"
os.environ["SAPUBLISH_INPATH"] = json_data_path
if gui:
@@ -139,7 +139,7 @@ def cli_publish(data, gui=True):
if gui:
args += ["gui"]
- os.environ["PYBLISH_HOSTS"] = "shell"
+ os.environ["PYBLISH_HOSTS"] = "standalonepublisher"
os.environ["SAPUBLISH_INPATH"] = json_data_path
os.environ["SAPUBLISH_OUTPATH"] = return_data_path
diff --git a/pype/standalonepublish/widgets/__init__.py b/pype/standalonepublish/widgets/__init__.py
index 4c6a0e85a5..c6e0dd9a47 100644
--- a/pype/standalonepublish/widgets/__init__.py
+++ b/pype/standalonepublish/widgets/__init__.py
@@ -6,6 +6,7 @@ HelpRole = QtCore.Qt.UserRole + 2
FamilyRole = QtCore.Qt.UserRole + 3
ExistsRole = QtCore.Qt.UserRole + 4
PluginRole = QtCore.Qt.UserRole + 5
+PluginKeyRole = QtCore.Qt.UserRole + 6
from ..resources import get_resource
from .button_from_svgs import SvgResizable, SvgButton
diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py
index e60db892db..a5a686bae1 100644
--- a/pype/standalonepublish/widgets/widget_drop_frame.py
+++ b/pype/standalonepublish/widgets/widget_drop_frame.py
@@ -220,15 +220,21 @@ class DropDataFrame(QtWidgets.QFrame):
self._process_data(data)
def load_data_with_probe(self, filepath):
+ ffprobe_path = os.getenv("FFMPEG_PATH", "")
+ if ffprobe_path:
+ ffprobe_path += '/ffprobe'
+ else:
+ ffprobe_path = 'ffprobe'
+
args = [
- 'ffprobe',
+ ffprobe_path,
'-v', 'quiet',
'-print_format', 'json',
'-show_format',
'-show_streams', filepath
]
ffprobe_p = subprocess.Popen(
- args,
+ ' '.join(args),
stdout=subprocess.PIPE,
shell=True
)
diff --git a/pype/standalonepublish/widgets/widget_family.py b/pype/standalonepublish/widgets/widget_family.py
index 63776b1df3..26eb8077d9 100644
--- a/pype/standalonepublish/widgets/widget_family.py
+++ b/pype/standalonepublish/widgets/widget_family.py
@@ -5,7 +5,7 @@ import json
from collections import namedtuple
from . import QtWidgets, QtCore
-from . import HelpRole, FamilyRole, ExistsRole, PluginRole
+from . import HelpRole, FamilyRole, ExistsRole, PluginRole, PluginKeyRole
from . import FamilyDescriptionWidget
from pypeapp import config
@@ -116,8 +116,10 @@ class FamilyWidget(QtWidgets.QWidget):
def collect_data(self):
plugin = self.list_families.currentItem().data(PluginRole)
+ key = self.list_families.currentItem().data(PluginKeyRole)
family = plugin.family.rsplit(".", 1)[-1]
data = {
+ 'family_preset_key': key,
'family': family,
'subset': self.input_result.text(),
'version': self.version_spinbox.value()
@@ -318,7 +320,7 @@ class FamilyWidget(QtWidgets.QWidget):
has_families = False
presets = config.get_presets().get('standalone_publish', {})
- for creator in presets.get('families', {}).values():
+ for key, creator in presets.get('families', {}).items():
creator = namedtuple("Creator", creator.keys())(*creator.values())
label = creator.label or creator.family
@@ -327,6 +329,7 @@ class FamilyWidget(QtWidgets.QWidget):
item.setData(HelpRole, creator.help or "")
item.setData(FamilyRole, creator.family)
item.setData(PluginRole, creator)
+ item.setData(PluginKeyRole, key)
item.setData(ExistsRole, False)
self.list_families.addItem(item)