merged origin/develop

This commit is contained in:
Ondrej Samohel 2021-05-05 17:29:24 +02:00
commit 54c72435e6
97 changed files with 1695 additions and 435 deletions

7
.gitignore vendored
View file

@ -91,4 +91,9 @@ website/i18n/*
website/debug.log
website/.docusaurus
website/.docusaurus
# Poetry
########
.poetry/

View file

@ -1,5 +1,61 @@
# Changelog
## [2.17.1](https://github.com/pypeclub/openpype/tree/2.17.1) (2021-04-30)
[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.0...2.17.1)
**Enhancements:**
- TVPaint frame range definition [\#1424](https://github.com/pypeclub/OpenPype/pull/1424)
- PS - group all published instances [\#1415](https://github.com/pypeclub/OpenPype/pull/1415)
- Nuke: deadline submission with gpu [\#1414](https://github.com/pypeclub/OpenPype/pull/1414)
- Add task name to context pop up. [\#1383](https://github.com/pypeclub/OpenPype/pull/1383)
- AE add duration validation [\#1363](https://github.com/pypeclub/OpenPype/pull/1363)
- Maya: Support for Redshift proxies [\#1360](https://github.com/pypeclub/OpenPype/pull/1360)
**Fixed bugs:**
- Nuke: fixing undo for loaded mov and sequence [\#1433](https://github.com/pypeclub/OpenPype/pull/1433)
- AE - validation for duration was 1 frame shorter [\#1426](https://github.com/pypeclub/OpenPype/pull/1426)
- Houdini menu filename [\#1417](https://github.com/pypeclub/OpenPype/pull/1417)
- Maya: Vray - problem getting all file nodes for look publishing [\#1399](https://github.com/pypeclub/OpenPype/pull/1399)
## [2.17.0](https://github.com/pypeclub/openpype/tree/2.17.0) (2021-04-20)
[Full Changelog](https://github.com/pypeclub/openpype/compare/3.0.0-beta2...2.17.0)
**Enhancements:**
- Forward compatible ftrack group [\#1243](https://github.com/pypeclub/OpenPype/pull/1243)
- Maya: Make tx option configurable with presets [\#1328](https://github.com/pypeclub/OpenPype/pull/1328)
- TVPaint asset name validation [\#1302](https://github.com/pypeclub/OpenPype/pull/1302)
- TV Paint: Set initial project settings. [\#1299](https://github.com/pypeclub/OpenPype/pull/1299)
- TV Paint: Validate mark in and out. [\#1298](https://github.com/pypeclub/OpenPype/pull/1298)
- Validate project settings [\#1297](https://github.com/pypeclub/OpenPype/pull/1297)
- After Effects: added SubsetManager [\#1234](https://github.com/pypeclub/OpenPype/pull/1234)
- Show error message in pyblish UI [\#1206](https://github.com/pypeclub/OpenPype/pull/1206)
**Fixed bugs:**
- Hiero: fixing source frame from correct object [\#1362](https://github.com/pypeclub/OpenPype/pull/1362)
- Nuke: fix colourspace, prerenders and nuke panes opening [\#1308](https://github.com/pypeclub/OpenPype/pull/1308)
- AE remove orphaned instance from workfile - fix self.stub [\#1282](https://github.com/pypeclub/OpenPype/pull/1282)
- Nuke: deadline submission with search replaced env values from preset [\#1194](https://github.com/pypeclub/OpenPype/pull/1194)
- Ftrack custom attributes in bulks [\#1312](https://github.com/pypeclub/OpenPype/pull/1312)
- Ftrack optional pypclub role [\#1303](https://github.com/pypeclub/OpenPype/pull/1303)
- After Effects: remove orphaned instances [\#1275](https://github.com/pypeclub/OpenPype/pull/1275)
- Avalon schema names [\#1242](https://github.com/pypeclub/OpenPype/pull/1242)
- Handle duplication of Task name [\#1226](https://github.com/pypeclub/OpenPype/pull/1226)
- Modified path of plugin loads for Harmony and TVPaint [\#1217](https://github.com/pypeclub/OpenPype/pull/1217)
- Regex checks in profiles filtering [\#1214](https://github.com/pypeclub/OpenPype/pull/1214)
- Bulk mov strict task [\#1204](https://github.com/pypeclub/OpenPype/pull/1204)
- Update custom ftrack session attributes [\#1202](https://github.com/pypeclub/OpenPype/pull/1202)
- Nuke: write node colorspace ignore `default\(\)` label [\#1199](https://github.com/pypeclub/OpenPype/pull/1199)
- Nuke: reverse search to make it more versatile [\#1178](https://github.com/pypeclub/OpenPype/pull/1178)
## [2.16.1](https://github.com/pypeclub/pype/tree/2.16.1) (2021-04-13)
[Full Changelog](https://github.com/pypeclub/pype/compare/2.16.0...2.16.1)

View file

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
"""Definition of Igniter version."""
__version__ = "1.0.0-beta"
__version__ = "1.0.0-rc1"

View file

@ -2,14 +2,9 @@ import pyblish.api
class CollectRemoveMarked(pyblish.api.ContextPlugin):
"""Collect model data
"""Remove marked data
Ensures always only a single frame is extracted (current frame).
Note:
This is a workaround so that the `pype.model` family can use the
same pointcache extractor implementation as animation and pointcaches.
This always enforces the "current" frame to be published.
Remove instances that have 'remove' in their instance.data
"""

View file

@ -1,21 +0,0 @@
"""
Optional:
instance.data["remove"] -> mareker for removing
"""
import pyblish.api
class CollectClearInstances(pyblish.api.InstancePlugin):
"""Clear all marked instances"""
order = pyblish.api.CollectorOrder + 0.4999
label = "Clear Instances"
hosts = ["standalonepublisher"]
def process(self, instance):
self.log.debug(
f"Instance: `{instance}` | "
f"families: `{instance.data['families']}`")
if instance.data.get("remove"):
self.log.info(f"Removing: {instance}")
instance.context.remove(instance)

View file

@ -127,10 +127,28 @@ class FtrackModule(
self, old_value, new_value, changes, new_value_metadata
):
"""Implementation of ISettingsChangeListener interface."""
if not self.ftrack_url:
raise SaveWarningExc((
"Ftrack URL is not set."
" Can't propagate changes to Ftrack server."
))
ftrack_changes = changes.get("modules", {}).get("ftrack", {})
url_change_msg = None
if "ftrack_server" in ftrack_changes:
url_change_msg = (
"Ftrack URL was changed."
" This change may need to restart OpenPype to take affect."
)
try:
session = self.create_ftrack_session()
except Exception:
self.log.warning("Couldn't create ftrack session.", exc_info=True)
if url_change_msg:
raise SaveWarningExc(url_change_msg)
raise SaveWarningExc((
"Saving of attributes to ftrack wasn't successful,"
" try running Create/Update Avalon Attributes in ftrack."
@ -204,6 +222,9 @@ class FtrackModule(
" Try running Create/Update Avalon Attributes in ftrack."
).format(", ".join(missing_attributes)))
if url_change_msg:
raise SaveWarningExc(url_change_msg)
def on_project_settings_save(self, *_args, **_kwargs):
"""Implementation of ISettingsChangeListener interface."""
# Ignore

View file

@ -2,14 +2,9 @@ import pyblish.api
class CollectFtrackFamilies(pyblish.api.InstancePlugin):
"""Collect model data
Ensures always only a single frame is extracted (current frame).
Note:
This is a workaround so that the `pype.model` family can use the
same pointcache extractor implementation as animation and pointcaches.
This always enforces the "current" frame to be published.
"""Collect family for ftrack publishing
Add ftrack family to those instance that should be published to ftrack
"""
@ -23,6 +18,7 @@ class CollectFtrackFamilies(pyblish.api.InstancePlugin):
"rig",
"camera"
]
hosts = ["maya"]
def process(self, instance):

View file

@ -80,16 +80,20 @@ class SettingsAction(PypeModule, ITrayAction):
# Store if was visible
was_visible = self.settings_window.isVisible()
was_minimized = self.settings_window.isMinimized()
# Show settings gui
self.settings_window.show()
if was_minimized:
self.settings_window.showNormal()
# Pull window to the front.
self.settings_window.raise_()
self.settings_window.activateWindow()
# Reset content if was not visible
if not was_visible:
if not was_visible and not was_minimized:
self.settings_window.reset()

View file

@ -83,6 +83,7 @@ class SyncServerModule(PypeModule, ITrayModule):
DEFAULT_SITE = 'studio'
LOCAL_SITE = 'local'
LOG_PROGRESS_SEC = 5 # how often log progress to DB
DEFAULT_PRIORITY = 50 # higher is better, allowed range 1 - 1000
name = "sync_server"
label = "Sync Queue"
@ -472,6 +473,7 @@ class SyncServerModule(PypeModule, ITrayModule):
try:
self.sync_server_thread = SyncServerThread(self)
from .tray.app import SyncServerWindow
self.widget = SyncServerWindow(self)
except ValueError:
@ -662,7 +664,7 @@ class SyncServerModule(PypeModule, ITrayModule):
self.connection.Session["AVALON_PROJECT"] = collection
# retry_cnt - number of attempts to sync specific file before giving up
retries_arr = self._get_retries_arr(collection)
query = {
match = {
"type": "representation",
"$or": [
{"$and": [
@ -700,10 +702,47 @@ class SyncServerModule(PypeModule, ITrayModule):
]}
]
}
aggr = [
{"$match": match},
{'$unwind': '$files'},
{'$addFields': {
'order_remote': {
'$filter': {'input': '$files.sites', 'as': 'p',
'cond': {'$eq': ['$$p.name', remote_site]}
}},
'order_local': {
'$filter': {'input': '$files.sites', 'as': 'p',
'cond': {'$eq': ['$$p.name', active_site]}
}},
}},
{'$addFields': {
'priority': {
'$cond': [
{'$size': '$order_local.priority'},
{'$first': '$order_local.priority'},
{'$cond': [
{'$size': '$order_remote.priority'},
{'$first': '$order_remote.priority'},
self.DEFAULT_PRIORITY]}
]
},
}},
{'$group': {
'_id': '$_id',
# pass through context - same for representation
'context': {'$addToSet': '$context'},
'data': {'$addToSet': '$data'},
# pass through files as a list
'files': {'$addToSet': '$files'},
'priority': {'$max': "$priority"},
}},
{"$sort": {'priority': -1, '_id': 1}},
]
log.debug("active_site:{} - remote_site:{}".format(active_site,
remote_site))
log.debug("query: {}".format(query))
representations = self.connection.find(query)
log.debug("query: {}".format(aggr))
representations = self.connection.aggregate(aggr)
return representations
@ -749,7 +788,7 @@ class SyncServerModule(PypeModule, ITrayModule):
return SyncStatus.DO_NOTHING
def update_db(self, collection, new_file_id, file, representation,
site, error=None, progress=None):
site, error=None, progress=None, priority=None):
"""
Update 'provider' portion of records in DB with success (file_id)
or error (exception)
@ -763,12 +802,16 @@ class SyncServerModule(PypeModule, ITrayModule):
site (string): label ('gdrive', 'S3')
error (string): exception message
progress (float): 0-1 of progress of upload/download
priority (int): 0-100 set priority
Returns:
None
"""
representation_id = representation.get("_id")
file_id = file.get("_id")
file_id = None
if file:
file_id = file.get("_id")
query = {
"_id": representation_id
}
@ -780,6 +823,8 @@ class SyncServerModule(PypeModule, ITrayModule):
update["$unset"] = self._get_error_dict("", "", "")
elif progress is not None:
update["$set"] = self._get_progress_dict(progress)
elif priority is not None:
update["$set"] = self._get_priority_dict(priority, file_id)
else:
tries = self._get_tries_count(file, site)
tries += 1
@ -787,9 +832,10 @@ class SyncServerModule(PypeModule, ITrayModule):
update["$set"] = self._get_error_dict(error, tries)
arr_filter = [
{'s.name': site},
{'f._id': ObjectId(file_id)}
{'s.name': site}
]
if file_id:
arr_filter.append({'f._id': ObjectId(file_id)})
self.connection.database[collection].update_one(
query,
@ -798,7 +844,7 @@ class SyncServerModule(PypeModule, ITrayModule):
array_filters=arr_filter
)
if progress is not None:
if progress is not None or priority is not None:
return
status = 'failed'
@ -1192,6 +1238,21 @@ class SyncServerModule(PypeModule, ITrayModule):
val = {"files.$[f].sites.$[s].progress": progress}
return val
def _get_priority_dict(self, priority, file_id):
"""
Provide priority metadata to be stored in Db.
Used during upload/download for GUI to show.
Args:
priority: (int) - priority for file(s)
Returns:
(dictionary)
"""
if file_id:
str_key = "files.$[f].sites.$[s].priority"
else:
str_key = "files.$[].sites.$[s].priority"
return {str_key: int(priority)}
def _get_retries_arr(self, project_name):
"""
Returns array with allowed values in 'tries' field. If repre

View file

@ -85,8 +85,26 @@ class SyncServerWindow(QtWidgets.QDialog):
self.projects.current_project))
self.pause_btn.clicked.connect(self._pause)
self.pause_btn.setAutoDefault(False)
self.pause_btn.setDefault(False)
repres.message_generated.connect(self._update_message)
self.representationWidget = repres
def showEvent(self, event):
self.representationWidget.model.set_project(
self.projects.current_project)
self._set_running(True)
super().showEvent(event)
def closeEvent(self, event):
self._set_running(False)
super().closeEvent(event)
def _set_running(self, running):
self.representationWidget.model.is_running = running
self.representationWidget.model.timer.setInterval(0)
def _pause(self):
if self.sync_server.is_paused():
self.sync_server.unpause_server()

View file

@ -0,0 +1,116 @@
import os
from Qt import QtCore, QtWidgets, QtGui
from openpype.lib import PypeLogger
from openpype.modules.sync_server.tray import lib
log = PypeLogger().get_logger("SyncServer")
class PriorityDelegate(QtWidgets.QStyledItemDelegate):
"""Creates editable line edit to set priority on representation"""
def paint(self, painter, option, index):
super(PriorityDelegate, self).paint(painter, option, index)
if option.widget.selectionModel().isSelected(index) or \
option.state & QtWidgets.QStyle.State_MouseOver:
edit_icon = index.data(lib.EditIconRole)
if not edit_icon:
return
state = QtGui.QIcon.On
mode = QtGui.QIcon.Selected
icon_side = 16
icon_rect = QtCore.QRect(
option.rect.left() + option.rect.width() - icon_side - 4,
option.rect.top() + ((option.rect.height() - icon_side) / 2),
icon_side,
icon_side
)
edit_icon.paint(
painter, icon_rect,
QtCore.Qt.AlignRight, mode, state
)
def createEditor(self, parent, option, index):
editor = PriorityLineEdit(
parent,
option.widget.selectionModel().selectedRows())
editor.setFocus(True)
return editor
def setModelData(self, editor, model, index):
for index in editor.selected_idxs:
try:
val = int(editor.text())
except ValueError:
val = model.sync_server.DEFAULT_PRIORITY
model.set_priority_data(index, val)
class PriorityLineEdit(QtWidgets.QLineEdit):
"""Special LineEdit to consume Enter and store selected indexes"""
def __init__(self, parent=None, selected_idxs=None):
self.selected_idxs = selected_idxs
super(PriorityLineEdit, self).__init__(parent)
def keyPressEvent(self, event):
result = super(PriorityLineEdit, self).keyPressEvent(event)
if (
event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter)
):
return event.accept()
return result
class ImageDelegate(QtWidgets.QStyledItemDelegate):
"""
Prints icon of site and progress of synchronization
"""
def __init__(self, parent=None):
super(ImageDelegate, self).__init__(parent)
self.icons = {}
def paint(self, painter, option, index):
super(ImageDelegate, self).paint(painter, option, index)
option = QtWidgets.QStyleOptionViewItem(option)
option.showDecorationSelected = True
provider = index.data(lib.ProviderRole)
value = index.data(lib.ProgressRole)
date_value = index.data(lib.DateRole)
is_failed = index.data(lib.FailedRole)
if not self.icons.get(provider):
resource_path = os.path.dirname(__file__)
resource_path = os.path.join(resource_path, "..",
"providers", "resources")
pix_url = "{}/{}.png".format(resource_path, provider)
pixmap = QtGui.QPixmap(pix_url)
self.icons[provider] = pixmap
else:
pixmap = self.icons[provider]
padding = 10
point = QtCore.QPoint(option.rect.x() + padding,
option.rect.y() +
(option.rect.height() - pixmap.height()) / 2)
painter.drawPixmap(point, pixmap)
overlay_rect = option.rect.translated(0, 0)
overlay_rect.setHeight(overlay_rect.height() * (1.0 - float(value)))
painter.fillRect(overlay_rect,
QtGui.QBrush(QtGui.QColor(0, 0, 0, 100)))
text_rect = option.rect.translated(10, 0)
painter.drawText(text_rect,
QtCore.Qt.AlignCenter,
date_value)
if is_failed:
overlay_rect = option.rect.translated(0, 0)
painter.fillRect(overlay_rect,
QtGui.QBrush(QtGui.QColor(255, 0, 0, 35)))

View file

@ -25,6 +25,7 @@ DateRole = QtCore.Qt.UserRole + 6
FailedRole = QtCore.Qt.UserRole + 8
HeaderNameRole = QtCore.Qt.UserRole + 10
FullItemRole = QtCore.Qt.UserRole + 12
EditIconRole = QtCore.Qt.UserRole + 14
@six.add_metaclass(abc.ABCMeta)

View file

@ -6,8 +6,10 @@ from Qt import QtCore
from Qt.QtCore import Qt
from avalon.tools.delegates import pretty_timestamp
from avalon.vendor import qtawesome
from openpype.lib import PypeLogger
from openpype.api import get_local_site_id
from openpype.modules.sync_server.tray import lib
@ -41,6 +43,9 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
PAGE_SIZE = 20 # default page size to query for
REFRESH_SEC = 5000 # in seconds, requery DB for new status
refresh_started = QtCore.Signal()
refresh_finished = QtCore.Signal()
@property
def dbcon(self):
"""
@ -60,6 +65,14 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
def column_filtering(self):
return self._column_filtering
@property
def is_running(self):
return self._is_running
@is_running.setter
def is_running(self, state):
self._is_running = state
def rowCount(self, _index):
return len(self._data)
@ -78,7 +91,20 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
if orientation == Qt.Horizontal:
return self.COLUMN_LABELS[section][0] # return name
@property
def can_edit(self):
"""Returns true if some site is user local site, eg. could edit"""
return get_local_site_id() in (self.active_site, self.remote_site)
def get_column(self, index):
"""
Returns info about column
Args:
index (QModelIndex)
Returns:
(tuple): (COLUMN_NAME: COLUMN_LABEL)
"""
return self.COLUMN_LABELS[index]
def get_header_index(self, value):
@ -108,8 +134,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
actually queried (scrolled a couple of times to list more
than single page of records)
"""
if self.sync_server.is_paused() or \
self.sync_server.is_project_paused(self.project):
if self.is_editing or not self.is_running:
return
self.refresh_started.emit()
self.beginResetModel()
@ -191,7 +216,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
self.sort = {self.SORT_BY_COLUMN[index]: order} # reset
# add last one
for key, val in backup_sort.items():
if key != '_id':
if key != '_id' and key != self.SORT_BY_COLUMN[index]:
self.sort[key] = val
break
# add default one
@ -363,7 +388,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
"updated_dt_remote", # remote created_dt
"files_count", # count of files
"files_size", # file size of all files
"context.asset", # priority TODO
"priority", # priority
"status" # status
]
@ -374,6 +399,8 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
'representation': lib.MultiSelectFilter('representation')
}
EDITABLE_COLUMNS = ["priority"]
refresh_started = QtCore.Signal()
refresh_finished = QtCore.Signal()
@ -403,8 +430,8 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
status = attr.ib(default=None)
path = attr.ib(default=None)
def __init__(self, sync_server, header, project=None):
super(SyncRepresentationSummaryModel, self).__init__()
def __init__(self, sync_server, header, project=None, parent=None):
super(SyncRepresentationSummaryModel, self).__init__(parent=parent)
self._header = header
self._data = []
self._project = project
@ -412,10 +439,13 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
self._total_records = 0 # how many documents query actually found
self._word_filter = None
self._column_filtering = {}
self._is_running = False
self.edit_icon = qtawesome.icon("fa.edit", color="white")
self.is_editing = False
self._word_filter = None
self._initialized = False
if not self._project or self._project == lib.DUMMY_PROJECT:
return
@ -472,12 +502,17 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
return item.status == lib.STATUS[2] and \
item.remote_progress < 1
if role == Qt.DisplayRole:
if role in (Qt.DisplayRole, Qt.EditRole):
# because of ImageDelegate
if header_value in ['remote_site', 'local_site']:
return ""
return attr.asdict(item)[self._header[index.column()]]
if role == lib.EditIconRole:
if self.can_edit and header_value in self.EDITABLE_COLUMNS:
return self.edit_icon
if role == Qt.UserRole:
return item._id
@ -549,7 +584,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
avg_progress_remote,
repre.get("files_count", 1),
lib.pretty_size(repre.get("files_size", 0)),
1,
repre.get("priority"),
lib.STATUS[repre.get("status", -1)],
files[0].get('path')
)
@ -668,6 +703,16 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
'$cond': [{'$size': "$order_local.paused"},
1,
0]},
'priority': {
'$cond': [
{'$size': '$order_local.priority'},
{'$first': '$order_local.priority'},
{'$cond': [
{'$size': '$order_remote.priority'},
{'$first': '$order_remote.priority'},
self.sync_server.DEFAULT_PRIORITY]}
]
},
}},
{'$group': {
'_id': '$_id',
@ -690,7 +735,8 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
'failed_local_tries': {'$sum': '$failed_local_tries'},
'paused_remote': {'$sum': '$paused_remote'},
'paused_local': {'$sum': '$paused_local'},
'updated_dt_local': {'$max': "$updated_dt_local"}
'updated_dt_local': {'$max': "$updated_dt_local"},
'priority': {'$max': "$priority"},
}},
{"$project": self.projection}
]
@ -772,6 +818,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
'updated_dt_local': 1,
'paused_remote': 1,
'paused_local': 1,
'priority': 1,
'status': {
'$switch': {
'branches': [
@ -818,6 +865,35 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
}
}
def set_priority_data(self, index, value):
"""
Sets 'priority' flag and value on local site for selected reprs.
Args:
index (QItemIndex): selected index from View
value (int): priority value
Updates DB.
Potentially should allow set priority to any site when user
management is implemented.
"""
if not self.can_edit:
return
repre_id = self.data(index, Qt.UserRole)
representation = list(self.dbcon.find({"type": "representation",
"_id": repre_id}))
if representation:
self.sync_server.update_db(self.project, None, None,
representation.pop(),
get_local_site_id(),
priority=value)
self.is_editing = False
# all other approaches messed up selection to 0th index
self.timer.setInterval(0)
class SyncRepresentationDetailModel(_SyncRepresentationModel):
"""
@ -852,7 +928,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
"updated_dt_local", # local created_dt
"updated_dt_remote", # remote created_dt
"size", # remote progress
"size", # priority TODO
"priority", # priority
"status" # status
]
@ -861,8 +937,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
'file': lib.RegexTextFilter('file'),
}
refresh_started = QtCore.Signal()
refresh_finished = QtCore.Signal()
EDITABLE_COLUMNS = ["priority"]
@attr.s
class SyncRepresentationDetail:
@ -898,8 +973,11 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
self._total_records = 0 # how many documents query actually found
self._word_filter = None
self._id = _id
self._initialized = False
self._column_filtering = {}
self._is_running = False
self.is_editing = False
self.edit_icon = qtawesome.icon("fa.edit", color="white")
self.sync_server = sync_server
# TODO think about admin mode
@ -952,11 +1030,17 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
return item.status == lib.STATUS[2] and \
item.remote_progress < 1
if role == Qt.DisplayRole:
if role in (Qt.DisplayRole, Qt.EditRole):
# because of ImageDelegate
if header_value in ['remote_site', 'local_site']:
return ""
return attr.asdict(item)[self._header[index.column()]]
if role == lib.EditIconRole:
if self.can_edit and header_value in self.EDITABLE_COLUMNS:
return self.edit_icon
if role == Qt.UserRole:
return item._id
@ -1026,7 +1110,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
local_progress,
remote_progress,
lib.pretty_size(file.get('size', 0)),
1,
repre.get("priority"),
lib.STATUS[repre.get("status", -1)],
repre.get("tries"),
'\n'.join(errors),
@ -1144,7 +1228,17 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
"$order_remote.tries",
[]
]}
]}}
]}},
'priority': {
'$cond': [
{'$size': '$order_local.priority'},
{'$first': '$order_local.priority'},
{'$cond': [
{'$size': '$order_remote.priority'},
{'$first': '$order_remote.priority'},
self.sync_server.DEFAULT_PRIORITY]}
]
},
}},
{"$project": self.projection}
]
@ -1210,6 +1304,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
'failed_remote_error': 1,
'failed_local_error': 1,
'tries': 1,
'priority': 1,
'status': {
'$switch': {
'branches': [
@ -1261,3 +1356,37 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
},
'data.path': 1
}
def set_priority_data(self, index, value):
"""
Sets 'priority' flag and value on local site for selected reprs.
Args:
index (QItemIndex): selected index from View
value (int): priority value
Updates DB
"""
if not self.can_edit:
return
file_id = self.data(index, Qt.UserRole)
updated_file = None
# conversion from cursor to list
representations = list(self.dbcon.find({"type": "representation",
"_id": self._id}))
representation = representations.pop()
for repre_file in representation["files"]:
if repre_file["_id"] == file_id:
updated_file = repre_file
break
if representation and updated_file:
self.sync_server.update_db(self.project, None, updated_file,
representation, get_local_site_id(),
priority=value)
self.is_editing = False
# all other approaches messed up selection to 0th index
self.timer.setInterval(0)

View file

@ -23,6 +23,7 @@ from openpype.modules.sync_server.tray.models import (
)
from openpype.modules.sync_server.tray import lib
from openpype.modules.sync_server.tray import delegates
log = PypeLogger().get_logger("SyncServer")
@ -94,16 +95,19 @@ class SyncProjectListWidget(ProjectListWidget):
self.project_name = point_index.data(QtCore.Qt.DisplayRole)
menu = QtWidgets.QMenu()
menu = QtWidgets.QMenu(self)
actions_mapping = {}
if self.sync_server.is_project_paused(self.project_name):
action = QtWidgets.QAction("Unpause")
actions_mapping[action] = self._unpause
else:
action = QtWidgets.QAction("Pause")
actions_mapping[action] = self._pause
menu.addAction(action)
can_edit = self.model.can_edit
if can_edit:
if self.sync_server.is_project_paused(self.project_name):
action = QtWidgets.QAction("Unpause")
actions_mapping[action] = self._unpause
else:
action = QtWidgets.QAction("Pause")
actions_mapping[action] = self._pause
menu.addAction(action)
if self.local_site == get_local_site_id():
action = QtWidgets.QAction("Clear local project")
@ -145,10 +149,10 @@ class _SyncRepresentationWidget(QtWidgets.QWidget):
def _selection_changed(self, _new_selected, _all_selected):
idxs = self.selection_model.selectedRows()
self._selected_ids = []
self._selected_ids = set()
for index in idxs:
self._selected_ids.append(self.model.data(index, Qt.UserRole))
self._selected_ids.add(self.model.data(index, Qt.UserRole))
def _set_selection(self):
"""
@ -156,14 +160,14 @@ class _SyncRepresentationWidget(QtWidgets.QWidget):
Keep selection during model refresh.
"""
existing_ids = []
existing_ids = set()
for selected_id in self._selected_ids:
index = self.model.get_index(selected_id)
if index and index.isValid():
mode = QtCore.QItemSelectionModel.Select | \
QtCore.QItemSelectionModel.Rows
self.selection_model.select(index, mode)
existing_ids.append(selected_id)
existing_ids.add(selected_id)
self._selected_ids = existing_ids
@ -171,9 +175,17 @@ class _SyncRepresentationWidget(QtWidgets.QWidget):
"""
Opens representation dialog with all files after doubleclick
"""
# priority editing
if self.model.can_edit:
column_name = self.model.get_column(index.column())
if column_name[0] in self.model.EDITABLE_COLUMNS:
self.model.is_editing = True
self.table_view.openPersistentEditor(index)
return
_id = self.model.data(index, Qt.UserRole)
detail_window = SyncServerDetailWindow(
self.sync_server, _id, self.model.project)
self.sync_server, _id, self.model.project, parent=self)
detail_window.exec()
def _on_context_menu(self, point):
@ -189,13 +201,15 @@ class _SyncRepresentationWidget(QtWidgets.QWidget):
return
if is_multi:
index = self.model.get_index(self._selected_ids[0])
index = self.model.get_index(list(self._selected_ids)[0])
item = self.model.data(index, lib.FullItemRole)
else:
item = self.model.data(point_index, lib.FullItemRole)
can_edit = self.model.can_edit
action_kwarg_map, actions_mapping, menu = self._prepare_menu(item,
is_multi)
is_multi,
can_edit)
result = menu.exec_(QtGui.QCursor.pos())
if result:
@ -206,8 +220,8 @@ class _SyncRepresentationWidget(QtWidgets.QWidget):
self.model.refresh()
def _prepare_menu(self, item, is_multi):
menu = QtWidgets.QMenu()
def _prepare_menu(self, item, is_multi, can_edit):
menu = QtWidgets.QMenu(self)
actions_mapping = {}
action_kwarg_map = {}
@ -235,24 +249,30 @@ class _SyncRepresentationWidget(QtWidgets.QWidget):
self._get_action_kwargs(site)
menu.addAction(action)
if remote_progress == 1.0 or is_multi:
if can_edit and (remote_progress == 1.0 or is_multi):
action = QtWidgets.QAction("Re-sync Active site")
action_kwarg_map[action] = self._get_action_kwargs(active_site)
actions_mapping[action] = self._reset_site
menu.addAction(action)
if local_progress == 1.0 or is_multi:
if can_edit and (local_progress == 1.0 or is_multi):
action = QtWidgets.QAction("Re-sync Remote site")
action_kwarg_map[action] = self._get_action_kwargs(remote_site)
actions_mapping[action] = self._reset_site
menu.addAction(action)
if active_site == get_local_site_id():
if can_edit and active_site == get_local_site_id():
action = QtWidgets.QAction("Completely remove from local")
action_kwarg_map[action] = self._get_action_kwargs(active_site)
actions_mapping[action] = self._remove_site
menu.addAction(action)
if can_edit:
action = QtWidgets.QAction("Change priority")
action_kwarg_map[action] = self._get_action_kwargs(active_site)
actions_mapping[action] = self._change_priority
menu.addAction(action)
# # temp for testing only !!!
# action = QtWidgets.QAction("Download")
# action_kwarg_map[action] = self._get_action_kwargs(active_site)
@ -397,6 +417,16 @@ class _SyncRepresentationWidget(QtWidgets.QWidget):
except OSError:
raise OSError('unsupported xdg-open call??')
def _change_priority(self, **kwargs):
"""Open editor to change priority on first selected row"""
if self._selected_ids:
# get_index returns dummy index with column equals to 0
index = self.model.get_index(list(self._selected_ids)[0])
column_no = self.model.get_header_index("priority") # real column
real_index = self.model.index(index.row(), column_no)
self.model.is_editing = True
self.table_view.openPersistentEditor(real_index)
def _get_progress(self, item, site_name, opposite=False):
"""Returns progress value according to site (side)"""
progress = {'local': item.local_progress,
@ -441,7 +471,7 @@ class SyncRepresentationSummaryWidget(_SyncRepresentationWidget):
self.sync_server = sync_server
self._selected_ids = [] # keep last selected _id
self._selected_ids = set() # keep last selected _id
txt_filter = QtWidgets.QLineEdit()
txt_filter.setPlaceholderText("Quick filter representations..")
@ -459,7 +489,8 @@ class SyncRepresentationSummaryWidget(_SyncRepresentationWidget):
table_view = QtWidgets.QTableView()
headers = [item[0] for item in self.default_widths]
model = SyncRepresentationSummaryModel(sync_server, headers, project)
model = SyncRepresentationSummaryModel(sync_server, headers, project,
parent=self)
table_view.setModel(model)
table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
table_view.setSelectionMode(
@ -470,15 +501,20 @@ class SyncRepresentationSummaryWidget(_SyncRepresentationWidget):
-1, Qt.AscendingOrder)
table_view.setAlternatingRowColors(True)
table_view.verticalHeader().hide()
table_view.viewport().setAttribute(QtCore.Qt.WA_Hover, True)
column = table_view.model().get_header_index("local_site")
delegate = ImageDelegate(self)
delegate = delegates.ImageDelegate(self)
table_view.setItemDelegateForColumn(column, delegate)
column = table_view.model().get_header_index("remote_site")
delegate = ImageDelegate(self)
delegate = delegates.ImageDelegate(self)
table_view.setItemDelegateForColumn(column, delegate)
column = table_view.model().get_header_index("priority")
priority_delegate = delegates.PriorityDelegate(self)
table_view.setItemDelegateForColumn(column, priority_delegate)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addLayout(top_bar_layout)
@ -508,18 +544,19 @@ class SyncRepresentationSummaryWidget(_SyncRepresentationWidget):
self.selection_model = self.table_view.selectionModel()
self.selection_model.selectionChanged.connect(self._selection_changed)
def _prepare_menu(self, item, is_multi):
def _prepare_menu(self, item, is_multi, can_edit):
action_kwarg_map, actions_mapping, menu = \
super()._prepare_menu(item, is_multi)
super()._prepare_menu(item, is_multi, can_edit)
if item.status in [lib.STATUS[0], lib.STATUS[1]] or is_multi:
if can_edit and (
item.status in [lib.STATUS[0], lib.STATUS[1]] or is_multi):
action = QtWidgets.QAction("Pause in queue")
actions_mapping[action] = self._pause
# pause handles which site_name it will pause itself
action_kwarg_map[action] = {"selected_ids": self._selected_ids}
menu.addAction(action)
if item.status == lib.STATUS[3] or is_multi:
if can_edit and (item.status == lib.STATUS[3] or is_multi):
action = QtWidgets.QAction("Unpause in queue")
actions_mapping[action] = self._unpause
action_kwarg_map[action] = {"selected_ids": self._selected_ids}
@ -598,7 +635,7 @@ class SyncRepresentationDetailWidget(_SyncRepresentationWidget):
self.sync_server = sync_server
self.representation_id = _id
self._selected_ids = []
self._selected_ids = set()
self.txt_filter = QtWidgets.QLineEdit()
self.txt_filter.setPlaceholderText("Quick filter representation..")
@ -616,6 +653,8 @@ class SyncRepresentationDetailWidget(_SyncRepresentationWidget):
model = SyncRepresentationDetailModel(sync_server, headers, _id,
project)
model.is_running = True
table_view.setModel(model)
table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
table_view.setSelectionMode(
@ -628,13 +667,18 @@ class SyncRepresentationDetailWidget(_SyncRepresentationWidget):
table_view.verticalHeader().hide()
column = model.get_header_index("local_site")
delegate = ImageDelegate(self)
delegate = delegates.ImageDelegate(self)
table_view.setItemDelegateForColumn(column, delegate)
column = model.get_header_index("remote_site")
delegate = ImageDelegate(self)
delegate = delegates.ImageDelegate(self)
table_view.setItemDelegateForColumn(column, delegate)
if model.can_edit:
column = table_view.model().get_header_index("priority")
priority_delegate = delegates.PriorityDelegate(self)
table_view.setItemDelegateForColumn(column, priority_delegate)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addLayout(top_bar_layout)
@ -658,12 +702,25 @@ class SyncRepresentationDetailWidget(_SyncRepresentationWidget):
self.txt_filter.textChanged.connect(lambda: model.set_word_filter(
self.txt_filter.text()))
table_view.doubleClicked.connect(self._double_clicked)
table_view.customContextMenuRequested.connect(self._on_context_menu)
model.refresh_started.connect(self._save_scrollbar)
model.refresh_finished.connect(self._set_scrollbar)
model.modelReset.connect(self._set_selection)
def _double_clicked(self, index):
"""
Opens representation dialog with all files after doubleclick
"""
# priority editing
if self.model.can_edit:
column_name = self.model.get_column(index.column())
if column_name[0] in self.model.EDITABLE_COLUMNS:
self.model.is_editing = True
self.table_view.openPersistentEditor(index)
return
def _show_detail(self, selected_ids=None):
"""
Shows windows with error message for failed sync of a file.
@ -672,10 +729,10 @@ class SyncRepresentationDetailWidget(_SyncRepresentationWidget):
detail_window.exec()
def _prepare_menu(self, item, is_multi):
def _prepare_menu(self, item, is_multi, can_edit):
"""Adds view (and model) dependent actions to default ones"""
action_kwarg_map, actions_mapping, menu = \
super()._prepare_menu(item, is_multi)
super()._prepare_menu(item, is_multi, can_edit)
if item.status == lib.STATUS[2] or is_multi:
action = QtWidgets.QAction("Open error detail")
@ -778,72 +835,6 @@ class SyncRepresentationErrorWidget(QtWidgets.QWidget):
layout.addWidget(text_area)
class ImageDelegate(QtWidgets.QStyledItemDelegate):
"""
Prints icon of site and progress of synchronization
"""
def __init__(self, parent=None):
super(ImageDelegate, self).__init__(parent)
self.icons = {}
def paint(self, painter, option, index):
super(ImageDelegate, self).paint(painter, option, index)
option = QtWidgets.QStyleOptionViewItem(option)
option.showDecorationSelected = True
provider = index.data(lib.ProviderRole)
value = index.data(lib.ProgressRole)
date_value = index.data(lib.DateRole)
is_failed = index.data(lib.FailedRole)
if not self.icons.get(provider):
resource_path = os.path.dirname(__file__)
resource_path = os.path.join(resource_path, "..",
"providers", "resources")
pix_url = "{}/{}.png".format(resource_path, provider)
pixmap = QtGui.QPixmap(pix_url)
self.icons[provider] = pixmap
else:
pixmap = self.icons[provider]
padding = 10
point = QtCore.QPoint(option.rect.x() + padding,
option.rect.y() +
(option.rect.height() - pixmap.height()) / 2)
painter.drawPixmap(point, pixmap)
overlay_rect = option.rect.translated(0, 0)
overlay_rect.setHeight(overlay_rect.height() * (1.0 - float(value)))
painter.fillRect(overlay_rect,
QtGui.QBrush(QtGui.QColor(0, 0, 0, 100)))
text_rect = option.rect.translated(10, 0)
painter.drawText(text_rect,
QtCore.Qt.AlignCenter,
date_value)
if is_failed:
overlay_rect = option.rect.translated(0, 0)
painter.fillRect(overlay_rect,
QtGui.QBrush(QtGui.QColor(255, 0, 0, 35)))
class TransparentWidget(QtWidgets.QWidget):
"""Used for header cell for resizing to work properly"""
clicked = QtCore.Signal(str)
def __init__(self, column_name, *args, **kwargs):
super(TransparentWidget, self).__init__(*args, **kwargs)
self.column_name = column_name
# self.setStyleSheet("background: red;")
def mouseReleaseEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.clicked.emit(self.column_name)
super(TransparentWidget, self).mouseReleaseEvent(event)
class HorizontalHeader(QtWidgets.QHeaderView):
"""Reiplemented QHeaderView to contain clickable changeable button"""
def __init__(self, parent=None):

View file

@ -14,4 +14,4 @@ class CollectCurrentUserPype(pyblish.api.ContextPlugin):
def process(self, context):
user = get_openpype_username()
context.data["user"] = user
self.log.debug("Colected user \"{}\"".format(user))
self.log.debug("Collected user \"{}\"".format(user))

View file

@ -19,7 +19,7 @@ class CollectSceneVersion(pyblish.api.ContextPlugin):
if "unreal" in pyblish.api.registered_hosts():
return
assert context.data.get('currentFile'), "Cannot get curren file"
assert context.data.get('currentFile'), "Cannot get current file"
filename = os.path.basename(context.data.get('currentFile'))
if '<shell>' in filename:

View file

@ -3,6 +3,7 @@ import re
import json
import copy
import tempfile
import clique
import openpype
import openpype.api
@ -114,8 +115,30 @@ class ExtractBurnin(openpype.api.Extractor):
# Prepare burnin options
profile_options = copy.deepcopy(self.default_options)
for key, value in (self.options or {}).items():
if value is not None:
profile_options[key] = value
if value is None:
continue
if key == "bg_color" and len(value) == 4:
bg_red, bg_green, bg_blue, bg_alpha = value
bg_color_hex = "#{0:0>2X}{1:0>2X}{2:0>2X}".format(
bg_red, bg_green, bg_blue
)
bg_color_alpha = float(bg_alpha) / 255
profile_options["bg_opacity"] = bg_color_alpha
profile_options["bg_color"] = bg_color_hex
continue
elif key == "font_color" and len(value) == 4:
fg_red, fg_green, fg_blue, fg_alpha = value
fg_color_hex = "#{0:0>2X}{1:0>2X}{2:0>2X}".format(
fg_red, fg_green, fg_blue
)
fg_color_alpha = float(fg_alpha) / 255
profile_options["opacity"] = fg_color_alpha
profile_options["font_color"] = fg_color_hex
continue
profile_options[key] = value
# Prepare global burnin values from presets
profile_burnins = {}
@ -247,7 +270,9 @@ class ExtractBurnin(openpype.api.Extractor):
"output": temp_data["full_output_path"],
"burnin_data": burnin_data,
"options": burnin_options,
"values": burnin_values
"values": burnin_values,
"full_input_path": temp_data["full_input_paths"][0],
"first_frame": temp_data["first_frame"]
}
self.log.debug(
@ -461,32 +486,47 @@ class ExtractBurnin(openpype.api.Extractor):
None: This is processing method.
"""
# TODO we should find better way to know if input is sequence
is_sequence = (
"sequence" in new_repre["tags"]
and isinstance(new_repre["files"], (tuple, list))
)
input_filenames = new_repre["files"]
is_sequence = False
if isinstance(input_filenames, (tuple, list)):
if len(input_filenames) > 1:
is_sequence = True
# Sequence must have defined first frame
# - not used if input is not a sequence
first_frame = None
if is_sequence:
input_filename = new_repre["sequence_file"]
else:
input_filename = new_repre["files"]
collections, _ = clique.assemble(input_filenames)
if not collections:
is_sequence = False
else:
input_filename = new_repre["sequence_file"]
collection = collections[0]
indexes = list(collection.indexes)
padding = len(str(max(indexes)))
head = collection.format("{head}")
tail = collection.format("{tail}")
output_filename = "{}%{:0>2}d{}{}".format(
head, padding, filename_suffix, tail
)
repre_files = []
for idx in indexes:
repre_files.append(output_filename % idx)
filepart_start, ext = os.path.splitext(input_filename)
dir_path, basename = os.path.split(filepart_start)
first_frame = min(indexes)
if is_sequence:
# NOTE modified to keep name when multiple dots are in name
basename_parts = basename.split(".")
frame_part = basename_parts.pop(-1)
if not is_sequence:
input_filename = input_filenames
if isinstance(input_filename, (tuple, list)):
input_filename = input_filename[0]
basename_start = ".".join(basename_parts) + filename_suffix
new_basename = ".".join((basename_start, frame_part))
output_filename = new_basename + ext
else:
filepart_start, ext = os.path.splitext(input_filename)
dir_path, basename = os.path.split(filepart_start)
output_filename = basename + filename_suffix + ext
if dir_path:
output_filename = os.path.join(dir_path, output_filename)
if dir_path:
output_filename = os.path.join(dir_path, output_filename)
repre_files = output_filename
stagingdir = new_repre["stagingDir"]
full_input_path = os.path.join(
@ -498,6 +538,9 @@ class ExtractBurnin(openpype.api.Extractor):
temp_data["full_input_path"] = full_input_path
temp_data["full_output_path"] = full_output_path
temp_data["first_frame"] = first_frame
new_repre["files"] = repre_files
self.log.debug("full_input_path: {}".format(full_input_path))
self.log.debug("full_output_path: {}".format(full_output_path))
@ -505,17 +548,16 @@ class ExtractBurnin(openpype.api.Extractor):
# Prepare full paths to input files and filenames for reprensetation
full_input_paths = []
if is_sequence:
repre_files = []
for frame_index in range(1, temp_data["duration"] + 1):
repre_files.append(output_filename % frame_index)
full_input_paths.append(full_input_path % frame_index)
for filename in input_filenames:
filepath = os.path.join(
os.path.normpath(stagingdir), filename
).replace("\\", "/")
full_input_paths.append(filepath)
else:
full_input_paths.append(full_input_path)
repre_files = output_filename
temp_data["full_input_paths"] = full_input_paths
new_repre["files"] = repre_files
def prepare_repre_data(self, instance, repre, burnin_data, temp_data):
"""Prepare data for representation.

View file

@ -14,7 +14,7 @@ ffprobe_path = openpype.lib.get_ffmpeg_tool_path("ffprobe")
FFMPEG = (
'"{}" -i "%(input)s" %(filters)s %(args)s%(output)s'
'"{}"%(input_args)s -i "%(input)s" %(filters)s %(args)s%(output)s'
).format(ffmpeg_path)
FFPROBE = (
@ -121,10 +121,18 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
'font_size': 42
}
def __init__(self, source, streams=None, options_init=None):
def __init__(
self, source, streams=None, options_init=None, first_frame=None
):
if not streams:
streams = _streams(source)
input_args = []
if first_frame:
input_args.append("-start_number {}".format(first_frame))
self.input_args = input_args
super().__init__(source, streams)
if options_init:
@ -289,7 +297,12 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
if self.filter_string:
filters = '-vf "{}"'.format(self.filter_string)
input_args = ""
if self.input_args:
input_args = " {}".format(" ".join(self.input_args))
return (FFMPEG % {
'input_args': input_args,
'input': self.source,
'output': output,
'args': '%s ' % args if args else '',
@ -370,7 +383,8 @@ def example(input_path, output_path):
def burnins_from_data(
input_path, output_path, data,
codec_data=None, options=None, burnin_values=None, overwrite=True
codec_data=None, options=None, burnin_values=None, overwrite=True,
full_input_path=None, first_frame=None
):
"""This method adds burnins to video/image file based on presets setting.
@ -427,8 +441,11 @@ def burnins_from_data(
"shot": "sh0010"
}
"""
streams = None
if full_input_path:
streams = _streams(full_input_path)
burnin = ModifiedBurnins(input_path, options_init=options)
burnin = ModifiedBurnins(input_path, streams, options, first_frame)
frame_start = data.get("frame_start")
frame_end = data.get("frame_end")
@ -591,6 +608,8 @@ if __name__ == "__main__":
in_data["burnin_data"],
codec_data=in_data.get("codec"),
options=in_data.get("options"),
burnin_values=in_data.get("values")
burnin_values=in_data.get("values"),
full_input_path=in_data.get("full_input_path"),
first_frame=in_data.get("first_frame")
)
print("* Burnin script has finished")

View file

@ -20,7 +20,7 @@
"harmony/20",
"photoshop/2021",
"aftereffects/2021",
"unreal/4-24"
"unreal/4-26"
],
"tools_env": []
}

View file

@ -21,8 +21,8 @@
"secondary_pool": "",
"group": "",
"department": "",
"limit_groups": {},
"use_gpu": true
"use_gpu": true,
"limit_groups": {}
},
"HarmonySubmitDeadline": {
"enabled": true,

View file

@ -73,8 +73,18 @@
"enabled": true,
"options": {
"font_size": 42,
"opacity": 1.0,
"bg_opacity": 0.5,
"font_color": [
255,
255,
255,
255
],
"bg_color": [
0,
0,
0,
127
],
"x_offset": 5,
"y_offset": 5,
"bg_padding": 5

View file

@ -21,10 +21,20 @@
"LoadClip": {
"enabled": true,
"families": [
"render2d", "source", "plate", "render", "review"
"render2d",
"source",
"plate",
"render",
"review"
],
"representations": [
"exr", "dpx", "jpg", "jpeg", "png", "h264", "mov"
"exr",
"dpx",
"jpg",
"jpeg",
"png",
"h264",
"mov"
],
"clip_name_template": "{asset}_{subset}_{representation}"
}

View file

@ -210,11 +210,11 @@
"environment": {}
},
"__dynamic_keys_labels__": {
"13-0": "13.0 (Testing only)",
"12-2": "12.2",
"12-0": "12.0",
"11-3": "11.3",
"11-2": "11.2",
"13-0": "13.0 (Testing only)"
"11-2": "11.2"
}
}
},
@ -354,11 +354,11 @@
"environment": {}
},
"__dynamic_keys_labels__": {
"13-0": "13.0 (Testing only)",
"12-2": "12.2",
"12-0": "12.0",
"11-3": "11.3",
"11-2": "11.2",
"13-0": "13.0 (Testing only)"
"11-2": "11.2"
}
}
},
@ -496,11 +496,11 @@
"environment": {}
},
"__dynamic_keys_labels__": {
"13-0": "13.0 (Testing only)",
"12-2": "12.2",
"12-0": "12.0",
"11-3": "11.3",
"11-2": "11.2",
"13-0": "13.0 (Testing only)"
"11-2": "11.2"
}
}
},
@ -640,11 +640,11 @@
"environment": {}
},
"__dynamic_keys_labels__": {
"13-0": "13.0 (Testing only)",
"12-2": "12.2",
"12-0": "12.0",
"11-3": "11.3",
"11-2": "11.2",
"13-0": "13.0 (Testing only)"
"11-2": "11.2"
}
}
},

View file

@ -1,12 +1,10 @@
{
"studio_name": "Studio name",
"studio_code": "stu",
"admin_password": "",
"environment": {
"OPENPYPE_OCIO_CONFIG": "{STUDIO_SOFT}/OpenColorIO-Configs",
"__environment_keys__": {
"global": [
"OPENPYPE_OCIO_CONFIG"
]
"global": []
}
},
"openpype_path": {

View file

@ -457,27 +457,18 @@ class BaseItemEntity(BaseEntity):
pass
@property
def can_discard_changes(self):
"""Result defines if `discard_changes` will be processed.
Also can be used as validation before the method is called.
"""
def _can_discard_changes(self):
"""Defines if `discard_changes` will be processed."""
return self.has_unsaved_changes
@property
def can_add_to_studio_default(self):
"""Result defines if `add_to_studio_default` will be processed.
Also can be used as validation before the method is called.
"""
def _can_add_to_studio_default(self):
"""Defines if `add_to_studio_default` will be processed."""
if self._override_state is not OverrideState.STUDIO:
return False
if self.is_dynamic_item or self.is_in_dynamic_item:
return False
# Skip if entity is under group
if self.group_item:
if self.group_item is not None:
return False
# Skip if is group and any children is already marked with studio
@ -487,36 +478,24 @@ class BaseItemEntity(BaseEntity):
return True
@property
def can_remove_from_studio_default(self):
"""Result defines if `remove_from_studio_default` can be triggered.
This can be also used as validation before the method is called.
"""
def _can_remove_from_studio_default(self):
"""Defines if `remove_from_studio_default` can be processed."""
if self._override_state is not OverrideState.STUDIO:
return False
if self.is_dynamic_item or self.is_in_dynamic_item:
return False
if not self.has_studio_override:
return False
return True
@property
def can_add_to_project_override(self):
"""Result defines if `add_to_project_override` can be triggered.
Also can be used as validation before the method is called.
"""
if self.is_dynamic_item or self.is_in_dynamic_item:
return False
def _can_add_to_project_override(self):
"""Defines if `add_to_project_override` can be processed."""
# Show only when project overrides are set
if self._override_state is not OverrideState.PROJECT:
return False
# Do not show on items under group item
if self.group_item:
if self.group_item is not None:
return False
# Skip if already is marked to save project overrides
@ -525,14 +504,8 @@ class BaseItemEntity(BaseEntity):
return True
@property
def can_remove_from_project_override(self):
"""Result defines if `remove_from_project_override` can be triggered.
This can be also used as validation before the method is called.
"""
if self.is_dynamic_item or self.is_in_dynamic_item:
return False
def _can_remove_from_project_override(self):
"""Defines if `remove_from_project_override` can be processed."""
if self._override_state is not OverrideState.PROJECT:
return False
@ -544,6 +517,54 @@ class BaseItemEntity(BaseEntity):
return False
return True
@property
def can_trigger_discard_changes(self):
"""Defines if can trigger `discard_changes`.
Also can be used as validation before the method is called.
"""
return self._can_discard_changes
@property
def can_trigger_add_to_studio_default(self):
"""Defines if can trigger `add_to_studio_default`.
Also can be used as validation before the method is called.
"""
if self.is_dynamic_item or self.is_in_dynamic_item:
return False
return self._can_add_to_studio_default
@property
def can_trigger_remove_from_studio_default(self):
"""Defines if can trigger `remove_from_studio_default`.
Also can be used as validation before the method is called.
"""
if self.is_dynamic_item or self.is_in_dynamic_item:
return False
return self._can_remove_from_studio_default
@property
def can_trigger_add_to_project_override(self):
"""Defines if can trigger `add_to_project_override`.
Also can be used as validation before the method is called.
"""
if self.is_dynamic_item or self.is_in_dynamic_item:
return False
return self._can_add_to_project_override
@property
def can_trigger_remove_from_project_override(self):
"""Defines if can trigger `remove_from_project_override`.
Also can be used as validation before the method is called.
"""
if self.is_dynamic_item or self.is_in_dynamic_item:
return False
return self._can_remove_from_project_override
def discard_changes(self, on_change_trigger=None):
"""Discard changes on entity and it's children.
@ -568,7 +589,7 @@ class BaseItemEntity(BaseEntity):
"""
initialized = False
if on_change_trigger is None:
if not self.can_discard_changes:
if not self.can_trigger_discard_changes:
return
initialized = True
@ -588,7 +609,7 @@ class BaseItemEntity(BaseEntity):
def add_to_studio_default(self, on_change_trigger=None):
initialized = False
if on_change_trigger is None:
if not self.can_add_to_studio_default:
if not self.can_trigger_add_to_studio_default:
return
initialized = True
@ -625,7 +646,7 @@ class BaseItemEntity(BaseEntity):
"""
initialized = False
if on_change_trigger is None:
if not self.can_remove_from_studio_default:
if not self.can_trigger_remove_from_studio_default:
return
initialized = True
@ -649,7 +670,7 @@ class BaseItemEntity(BaseEntity):
def add_to_project_override(self, on_change_trigger=None):
initialized = False
if on_change_trigger is None:
if not self.can_add_to_project_override:
if not self.can_trigger_add_to_project_override:
return
initialized = True
@ -689,7 +710,7 @@ class BaseItemEntity(BaseEntity):
initialized = False
if on_change_trigger is None:
if not self.can_remove_from_project_override:
if not self.can_trigger_remove_from_project_override:
return
initialized = True
on_change_trigger = []
@ -775,7 +796,8 @@ class ItemEntity(BaseItemEntity):
# Group item reference
if self.parent.is_group:
self.group_item = self.parent
elif self.parent.group_item:
elif self.parent.group_item is not None:
self.group_item = self.parent.group_item
self.key = self.schema_data.get("key")

View file

@ -353,6 +353,20 @@ class DictImmutableKeysEntity(ItemEntity):
for key in METADATA_KEYS:
if key in value:
metadata[key] = value.pop(key)
old_metadata = metadata.get(M_OVERRIDEN_KEY)
if old_metadata:
old_metadata_set = set(old_metadata)
new_metadata = []
for key in self.non_gui_children.keys():
if key in old_metadata:
new_metadata.append(key)
old_metadata_set.remove(key)
for key in old_metadata_set:
new_metadata.append(key)
metadata[M_OVERRIDEN_KEY] = new_metadata
return value, metadata
def update_default_value(self, value):
@ -458,6 +472,9 @@ class DictImmutableKeysEntity(ItemEntity):
for child_obj in self.non_gui_children.values():
child_obj.add_to_studio_default(on_change_trigger)
self._ignore_child_changes = False
self._update_current_metadata()
self.parent.on_child_change(self)
def _remove_from_studio_default(self, on_change_trigger):
@ -471,6 +488,9 @@ class DictImmutableKeysEntity(ItemEntity):
for child_obj in self.non_gui_children.values():
child_obj.add_to_project_override(_on_change_trigger)
self._ignore_child_changes = False
self._update_current_metadata()
self.parent.on_child_change(self)
def _remove_from_project_override(self, on_change_trigger):

View file

@ -222,7 +222,7 @@ class DictMutableKeysEntity(EndpointEntity):
if self.value_is_env_group:
self.item_schema["env_group_key"] = ""
if not self.group_item:
if self.group_item is None:
self.is_group = True
def schema_validations(self):
@ -251,8 +251,18 @@ class DictMutableKeysEntity(EndpointEntity):
)
raise EntitySchemaError(self, reason)
for child_obj in self.children_by_key.values():
child_obj.schema_validations()
# Validate object type schema
child_validated = False
for child_entity in self.children_by_key.values():
child_entity.schema_validations()
child_validated = True
break
if not child_validated:
key = "__tmp__"
tmp_child = self._add_key(key)
tmp_child.schema_validations()
self.children_by_key.pop(key)
def get_child_path(self, child_obj):
result_key = None
@ -522,7 +532,7 @@ class DictMutableKeysEntity(EndpointEntity):
self.had_project_override = value is not NOT_SET
def _discard_changes(self, on_change_trigger):
if not self.can_discard_changes:
if not self._can_discard_changes:
return
self.set_override_state(self._override_state)
@ -533,7 +543,7 @@ class DictMutableKeysEntity(EndpointEntity):
self.on_change()
def _remove_from_studio_default(self, on_change_trigger):
if not self.can_remove_from_studio_default:
if not self._can_remove_from_studio_default:
return
value = self._default_value
@ -574,7 +584,7 @@ class DictMutableKeysEntity(EndpointEntity):
self.on_change()
def _remove_from_project_override(self, on_change_trigger):
if not self.can_remove_from_project_override:
if not self._can_remove_from_project_override:
return
if self._has_studio_override:

View file

@ -32,7 +32,7 @@ class EndpointEntity(ItemEntity):
super(EndpointEntity, self).__init__(*args, **kwargs)
if (
not (self.group_item or self.is_group)
not (self.group_item is not None or self.is_group)
and not (self.is_dynamic_item or self.is_in_dynamic_item)
):
self.is_group = True
@ -251,7 +251,7 @@ class InputEntity(EndpointEntity):
self._current_value = copy.deepcopy(value)
def _discard_changes(self, on_change_trigger=None):
if not self.can_discard_changes:
if not self._can_discard_changes:
return
self._value_is_modified = False
@ -289,7 +289,7 @@ class InputEntity(EndpointEntity):
self.on_change()
def _remove_from_studio_default(self, on_change_trigger):
if not self.can_remove_from_studio_default:
if not self._can_remove_from_studio_default:
return
value = self._default_value
@ -307,7 +307,7 @@ class InputEntity(EndpointEntity):
self.on_change()
def _remove_from_project_override(self, on_change_trigger):
if not self.can_remove_from_project_override:
if not self._can_remove_from_project_override:
return
self._has_project_override = False

View file

@ -49,7 +49,7 @@ class PathEntity(ItemEntity):
return self.child_obj.items()
def _item_initalization(self):
if not self.group_item and not self.is_group:
if self.group_item is None and not self.is_group:
self.is_group = True
self.multiplatform = self.schema_data.get("multiplatform", False)
@ -199,7 +199,7 @@ class ListStrictEntity(ItemEntity):
# GUI attribute
self.is_horizontal = self.schema_data.get("horizontal", True)
if not self.group_item and not self.is_group:
if self.group_item is None and not self.is_group:
self.is_group = True
def schema_validations(self):
@ -453,4 +453,5 @@ class ListStrictEntity(ItemEntity):
def reset_callbacks(self):
super(ListStrictEntity, self).reset_callbacks()
self.child_obj.reset_callbacks()
for child_obj in self.children:
child_obj.reset_callbacks()

View file

@ -59,8 +59,7 @@ class ListEntity(EndpointEntity):
)
def append(self, item):
child_obj = self._add_new_item()
child_obj.set_override_state(self._override_state)
child_obj = self.add_new_item(trigger_change=False)
child_obj.set(item)
self.on_change()
@ -92,8 +91,7 @@ class ListEntity(EndpointEntity):
raise ValueError("ListEntity.remove(x): x not in ListEntity")
def insert(self, idx, item):
child_obj = self._add_new_item(idx)
child_obj.set_override_state(self._override_state)
child_obj = self.add_new_item(idx, trigger_change=False)
child_obj.set(item)
self.on_change()
@ -105,10 +103,16 @@ class ListEntity(EndpointEntity):
self.children.insert(idx, child_obj)
return child_obj
def add_new_item(self, idx=None):
def add_new_item(self, idx=None, trigger_change=True):
child_obj = self._add_new_item(idx)
child_obj.set_override_state(self._override_state)
self.on_change()
if self._override_state is OverrideState.STUDIO:
child_obj.add_to_studio_default([])
elif self._override_state is OverrideState.PROJECT:
child_obj.add_to_project_default([])
if trigger_change:
self.on_change()
return child_obj
def swap_items(self, item_1, item_2):
@ -144,7 +148,7 @@ class ListEntity(EndpointEntity):
item_schema = {"type": item_schema}
self.item_schema = item_schema
if not self.group_item:
if self.group_item is None:
self.is_group = True
# Value that was set on set_override_state
@ -167,8 +171,18 @@ class ListEntity(EndpointEntity):
)
raise EntitySchemaError(self, reason)
for child_obj in self.children:
child_obj.schema_validations()
# Validate object type schema
child_validated = False
for child_entity in self.children:
child_entity.schema_validations()
child_validated = True
break
if not child_validated:
idx = 0
tmp_child = self._add_new_item(idx)
tmp_child.schema_validations()
self.children.pop(idx)
def get_child_path(self, child_obj):
result_idx = None
@ -343,7 +357,7 @@ class ListEntity(EndpointEntity):
return output
def _discard_changes(self, on_change_trigger):
if not self.can_discard_changes:
if not self._can_discard_changes:
return
not_set = object()
@ -405,7 +419,7 @@ class ListEntity(EndpointEntity):
self.on_change()
def _remove_from_studio_default(self, on_change_trigger):
if not self.can_remove_from_studio_default:
if not self._can_remove_from_studio_default:
return
value = self._default_value
@ -433,7 +447,7 @@ class ListEntity(EndpointEntity):
self.on_change()
def _remove_from_project_override(self, on_change_trigger):
if not self.can_remove_from_project_override:
if not self._can_remove_from_project_override:
return
if self._has_studio_override:

View file

@ -247,8 +247,7 @@
"label": "Used in plugins",
"object_type": {
"type": "text",
"key": "pluginClass",
"label": "Plugin Class"
"key": "pluginClass"
}
},
{
@ -295,8 +294,7 @@
"label": "Used in plugins",
"object_type": {
"type": "text",
"key": "pluginClass",
"label": "Plugin Class"
"key": "pluginClass"
}
},
{

View file

@ -301,20 +301,24 @@
"minimum": 0
},
{
"type": "number",
"key": "opacity",
"label": "Font opacity",
"decimal": 2,
"maximum": 1,
"minimum": 0
"type": "schema_template",
"name": "template_rgba_color",
"template_data": [
{
"label": "Font Color",
"name": "font_color"
}
]
},
{
"type": "number",
"key": "bg_opacity",
"label": "Background opacity",
"decimal": 2,
"maximum": 1,
"minimum": 0
"type": "schema_template",
"name": "template_rgba_color",
"template_data": [
{
"label": "Background Color",
"name": "bg_color"
}
]
},
{
"type": "number",

View file

@ -4,7 +4,6 @@
"key": "filters",
"label": "Publish GUI Filters",
"object_type": {
"type": "raw-json",
"label": "Plugins"
"type": "raw-json"
}
}

View file

@ -28,7 +28,6 @@
"object_type": {
"type": "dict",
"collapsible": true,
"checkbox_key": "enabled",
"children": [
{
"type": "schema_template",

View file

@ -28,7 +28,6 @@
"object_type": {
"type": "dict",
"collapsible": true,
"checkbox_key": "enabled",
"children": [
{
"type": "schema_template",

View file

@ -28,7 +28,6 @@
"object_type": {
"type": "dict",
"collapsible": true,
"checkbox_key": "enabled",
"children": [
{
"type": "schema_template",

View file

@ -28,7 +28,6 @@
"object_type": {
"type": "dict",
"collapsible": true,
"checkbox_key": "enabled",
"children": [
{
"type": "schema_template",

View file

@ -24,7 +24,6 @@
"object_type": {
"type": "dict",
"collapsible": true,
"checkbox_key": "enabled",
"children": [
{
"type": "schema_template",

View file

@ -28,7 +28,6 @@
"object_type": {
"type": "dict",
"collapsible": true,
"checkbox_key": "enabled",
"children": [
{
"type": "schema_template",

View file

@ -28,7 +28,6 @@
"object_type": {
"type": "dict",
"collapsible": true,
"checkbox_key": "enabled",
"children": [
{
"type": "schema_template",

View file

@ -29,7 +29,6 @@
"object_type": {
"type": "dict",
"collapsible": true,
"checkbox_key": "enabled",
"children": [
{
"type": "schema_template",

View file

@ -18,6 +18,18 @@
{
"type": "splitter"
},
{
"type": "label",
"label": "This is <b>NOT a securely stored password!</b>. It only acts as a simple barrier to stop users from accessing studio wide settings."
},
{
"type": "text",
"key": "admin_password",
"label": "Admin password"
},
{
"type": "splitter"
},
{
"key": "environment",
"label": "Environment",

View file

@ -168,7 +168,7 @@ class CacheValues:
class MongoSettingsHandler(SettingsHandler):
"""Settings handler that use mongo for storing and loading of settings."""
global_general_keys = ("openpype_path", )
global_general_keys = ("openpype_path", "admin_password")
def __init__(self):
# Get mongo connection

View file

@ -1,5 +1,7 @@
import sys
from Qt import QtWidgets, QtGui
from .lib import is_password_required
from .widgets import PasswordDialog
from .local_settings import LocalSettingsWindow
from .settings import (
style,
@ -24,13 +26,14 @@ def main(user_role=None):
widget = MainWidget(user_role)
widget.show()
widget.reset()
sys.exit(app.exec_())
__all__ = (
"is_password_required",
"style",
"PasswordDialog",
"MainWidget",
"ProjectListWidget",
"LocalSettingsWindow",

View file

@ -0,0 +1,16 @@
def is_password_required():
from openpype.settings import (
get_system_settings,
get_local_settings
)
system_settings = get_system_settings()
password = system_settings["general"].get("admin_password")
if not password:
return False
local_settings = get_local_settings()
is_admin = local_settings.get("general", {}).get("is_admin", False)
if is_admin:
return False
return True

View file

@ -1,28 +1,77 @@
import getpass
from Qt import QtWidgets
from Qt import QtWidgets, QtCore
from openpype.tools.settings import (
is_password_required,
PasswordDialog
)
class LocalGeneralWidgets(QtWidgets.QWidget):
def __init__(self, parent):
super(LocalGeneralWidgets, self).__init__(parent)
self._loading_local_settings = False
username_input = QtWidgets.QLineEdit(self)
username_input.setPlaceholderText(getpass.getuser())
is_admin_input = QtWidgets.QCheckBox(self)
layout = QtWidgets.QFormLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addRow("OpenPype Username", username_input)
layout.addRow("Admin permissions", is_admin_input)
is_admin_input.stateChanged.connect(self._on_admin_check_change)
self.username_input = username_input
self.is_admin_input = is_admin_input
def update_local_settings(self, value):
self._loading_local_settings = True
username = ""
is_admin = False
if value:
username = value.get("username", username)
is_admin = value.get("is_admin", is_admin)
self.username_input.setText(username)
if self.is_admin_input.isChecked() != is_admin:
# Use state as `stateChanged` is connected to callbacks
if is_admin:
state = QtCore.Qt.Checked
else:
state = QtCore.Qt.Unchecked
self.is_admin_input.setCheckState(state)
self._loading_local_settings = False
def _on_admin_check_change(self):
if self._loading_local_settings:
return
if not self.is_admin_input.isChecked():
return
if not is_password_required():
return
dialog = PasswordDialog(self, False)
dialog.setModal(True)
dialog.exec_()
result = dialog.result()
if self.is_admin_input.isChecked() != result:
# Use state as `stateChanged` is connected to callbacks
if result:
state = QtCore.Qt.Checked
else:
state = QtCore.Qt.Unchecked
self.is_admin_input.setCheckState(state)
def settings_value(self):
# Add changed
# If these have changed then
@ -30,6 +79,8 @@ class LocalGeneralWidgets(QtWidgets.QWidget):
username = self.username_input.text()
if username:
output["username"] = username
# Do not return output yet since we don't have mechanism to save or
# load these data through api calls
is_admin = self.is_admin_input.isChecked()
if is_admin:
output["is_admin"] = is_admin
return output

View file

@ -156,6 +156,8 @@ class LocalSettingsWindow(QtWidgets.QWidget):
def __init__(self, parent=None):
super(LocalSettingsWindow, self).__init__(parent)
self._reset_on_show = True
self.resize(1000, 600)
self.setWindowTitle("OpenPype Local settings")
@ -193,9 +195,14 @@ class LocalSettingsWindow(QtWidgets.QWidget):
self.reset_btn = reset_btn
self.save_btn = save_btn
self.reset()
def showEvent(self, event):
super(LocalSettingsWindow, self).showEvent(event)
if self._reset_on_show:
self.reset()
def reset(self):
if self._reset_on_show:
self._reset_on_show = False
value = get_local_settings()
self.settings_widget.update_local_settings(value)

View file

@ -0,0 +1,8 @@
import os
RESOURCES_DIR = os.path.dirname(os.path.abspath(__file__))
def get_resource(*args):
return os.path.normpath(os.path.join(RESOURCES_DIR, *args))

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -71,7 +71,7 @@ class BaseWidget(QtWidgets.QWidget):
def _discard_changes_action(self, menu, actions_mapping):
# TODO use better condition as unsaved changes may be caused due to
# changes in schema.
if not self.entity.can_discard_changes:
if not self.entity.can_trigger_discard_changes:
return
def discard_changes():
@ -86,7 +86,7 @@ class BaseWidget(QtWidgets.QWidget):
def _add_to_studio_default(self, menu, actions_mapping):
"""Set values as studio overrides."""
# Skip if not in studio overrides
if not self.entity.can_add_to_studio_default:
if not self.entity.can_trigger_add_to_studio_default:
return
action = QtWidgets.QAction("Add to studio default")
@ -94,7 +94,7 @@ class BaseWidget(QtWidgets.QWidget):
menu.addAction(action)
def _remove_from_studio_default_action(self, menu, actions_mapping):
if not self.entity.can_remove_from_studio_default:
if not self.entity.can_trigger_remove_from_studio_default:
return
def remove_from_studio_default():
@ -106,7 +106,7 @@ class BaseWidget(QtWidgets.QWidget):
menu.addAction(action)
def _add_to_project_override_action(self, menu, actions_mapping):
if not self.entity.can_add_to_project_override:
if not self.entity.can_trigger_add_to_project_override:
return
action = QtWidgets.QAction("Add to project project override")
@ -114,7 +114,7 @@ class BaseWidget(QtWidgets.QWidget):
menu.addAction(action)
def _remove_from_project_override_action(self, menu, actions_mapping):
if not self.entity.can_remove_from_project_override:
if not self.entity.can_trigger_remove_from_project_override:
return
def remove_from_project_override():

View file

@ -318,9 +318,14 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
"`create_root_entity` method not implemented"
)
def _on_reset_start(self):
return
def reset(self):
self.set_state(CategoryState.Working)
self._on_reset_start()
self.input_fields = []
while self.content_layout.count() != 0:
@ -485,7 +490,6 @@ class ProjectWidget(SettingsCategoryWidget):
def ui_tweaks(self):
project_list_widget = ProjectListWidget(self)
project_list_widget.refresh()
self.main_layout.insertWidget(0, project_list_widget, 0)
@ -501,6 +505,9 @@ class ProjectWidget(SettingsCategoryWidget):
if self is saved_tab_widget:
return
def _on_reset_start(self):
self.project_list_widget.refresh()
def _on_reset_crash(self):
self.project_list_widget.setEnabled(False)
super(ProjectWidget, self)._on_reset_crash()

View file

@ -661,8 +661,14 @@ class ProjectListWidget(QtWidgets.QWidget):
self.current_project = None
if self.dbcon:
for project_name in self.dbcon.database.collection_names():
items.append(project_name)
database = self.dbcon.database
for project_name in database.collection_names():
project_doc = database[project_name].find_one(
{"type": "project"},
{"name": 1}
)
if project_doc:
items.append(project_doc["name"])
for item in items:
model.appendRow(QtGui.QStandardItem(item))

View file

@ -1,4 +1,4 @@
from Qt import QtWidgets, QtGui
from Qt import QtWidgets, QtGui, QtCore
from .categories import (
CategoryState,
SystemWidget,
@ -7,6 +7,11 @@ from .categories import (
from .widgets import ShadowWidget
from .. import style
from openpype.tools.settings import (
is_password_required,
PasswordDialog
)
class MainWidget(QtWidgets.QWidget):
widget_width = 1000
@ -14,6 +19,12 @@ class MainWidget(QtWidgets.QWidget):
def __init__(self, user_role, parent=None):
super(MainWidget, self).__init__(parent)
self._user_passed = False
self._reset_on_show = True
self._password_dialog = None
self.setObjectName("MainWidget")
self.setWindowTitle("OpenPype Settings")
@ -44,6 +55,7 @@ class MainWidget(QtWidgets.QWidget):
self.setLayout(layout)
self._shadow_widget = ShadowWidget("Working...", self)
self._shadow_widget.setVisible(False)
for tab_widget in tab_widgets:
tab_widget.saved.connect(self._on_tab_save)
@ -75,6 +87,48 @@ class MainWidget(QtWidgets.QWidget):
if app:
app.processEvents()
def showEvent(self, event):
super(MainWidget, self).showEvent(event)
if self._reset_on_show:
self.reset()
def _show_password_dialog(self):
if self._password_dialog:
self._password_dialog.open()
def _on_password_dialog_close(self, password_passed):
# Store result for future settings reset
self._user_passed = password_passed
# Remove reference to password dialog
self._password_dialog = None
if password_passed:
self.reset()
else:
self.close()
def reset(self):
if self._password_dialog:
return
if not self._user_passed:
self._user_passed = not is_password_required()
self._on_state_change()
if not self._user_passed:
# Avoid doubled dialog
dialog = PasswordDialog(self)
dialog.setModal(True)
dialog.finished.connect(self._on_password_dialog_close)
self._password_dialog = dialog
QtCore.QTimer.singleShot(100, self._show_password_dialog)
return
if self._reset_on_show:
self._reset_on_show = False
for tab_widget in self.tab_widgets:
tab_widget.reset()

View file

@ -0,0 +1,164 @@
from Qt import QtWidgets, QtCore, QtGui
from .resources import get_resource
from openpype.api import get_system_settings
from openpype.settings.lib import (
get_local_settings,
save_local_settings
)
class PressHoverButton(QtWidgets.QPushButton):
_mouse_pressed = False
_mouse_hovered = False
change_state = QtCore.Signal(bool)
def mousePressEvent(self, event):
self._mouse_pressed = True
self._mouse_hovered = True
self.change_state.emit(self._mouse_hovered)
super(PressHoverButton, self).mousePressEvent(event)
def mouseReleaseEvent(self, event):
self._mouse_pressed = False
self._mouse_hovered = False
self.change_state.emit(self._mouse_hovered)
super(PressHoverButton, self).mouseReleaseEvent(event)
def mouseMoveEvent(self, event):
mouse_pos = self.mapFromGlobal(QtGui.QCursor.pos())
under_mouse = self.rect().contains(mouse_pos)
if under_mouse != self._mouse_hovered:
self._mouse_hovered = under_mouse
self.change_state.emit(self._mouse_hovered)
super(PressHoverButton, self).mouseMoveEvent(event)
class PasswordDialog(QtWidgets.QDialog):
"""Stupidly simple dialog to compare password from general settings."""
finished = QtCore.Signal(bool)
def __init__(self, parent=None, allow_remember=True):
super(PasswordDialog, self).__init__(parent)
self.setWindowTitle("Settings Password")
self.resize(300, 120)
system_settings = get_system_settings()
self._expected_result = (
system_settings["general"].get("admin_password")
)
self._final_result = None
self._allow_remember = allow_remember
# Password input
password_widget = QtWidgets.QWidget(self)
password_label = QtWidgets.QLabel("Password:", password_widget)
password_input = QtWidgets.QLineEdit(password_widget)
password_input.setEchoMode(QtWidgets.QLineEdit.Password)
show_password_icon_path = get_resource("images", "eye.png")
show_password_icon = QtGui.QIcon(show_password_icon_path)
show_password_btn = PressHoverButton(password_widget)
show_password_btn.setIcon(show_password_icon)
show_password_btn.setStyleSheet((
"border: none;padding:0.1em;"
))
show_password_btn.setFocusPolicy(QtCore.Qt.ClickFocus)
password_layout = QtWidgets.QHBoxLayout(password_widget)
password_layout.setContentsMargins(0, 0, 0, 0)
password_layout.addWidget(password_label)
password_layout.addWidget(password_input)
password_layout.addWidget(show_password_btn)
message_label = QtWidgets.QLabel("", self)
# Buttons
buttons_widget = QtWidgets.QWidget(self)
remember_checkbox = QtWidgets.QCheckBox("Remember", buttons_widget)
remember_checkbox.setVisible(allow_remember)
remember_checkbox.setStyleSheet((
"spacing: 0.5em;"
))
ok_btn = QtWidgets.QPushButton("Ok", buttons_widget)
cancel_btn = QtWidgets.QPushButton("Cancel", buttons_widget)
buttons_layout = QtWidgets.QHBoxLayout(buttons_widget)
buttons_layout.setContentsMargins(0, 0, 0, 0)
buttons_layout.addWidget(remember_checkbox)
buttons_layout.addStretch(1)
buttons_layout.addWidget(ok_btn)
buttons_layout.addWidget(cancel_btn)
# Main layout
layout = QtWidgets.QVBoxLayout(self)
layout.addSpacing(10)
layout.addWidget(password_widget, 0)
layout.addWidget(message_label, 0)
layout.addStretch(1)
layout.addWidget(buttons_widget, 0)
ok_btn.clicked.connect(self._on_ok_click)
cancel_btn.clicked.connect(self._on_cancel_click)
show_password_btn.change_state.connect(self._on_show_password)
self.password_input = password_input
self.remember_checkbox = remember_checkbox
self.message_label = message_label
def remember_password(self):
if not self._allow_remember:
return False
return self.remember_checkbox.isChecked()
def result(self):
if self._final_result is None:
return False
return self._final_result == self._expected_result
def keyPressEvent(self, event):
if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
self._on_ok_click()
return event.accept()
super(PasswordDialog, self).keyPressEvent(event)
def closeEvent(self, event):
super(PasswordDialog, self).closeEvent(event)
self.finished.emit(self.result())
def _on_ok_click(self):
input_value = self.password_input.text()
if input_value != self._expected_result:
self.message_label.setText("Invalid password. Try it again...")
self.password_input.setFocus()
return
if self.remember_password():
local_settings = get_local_settings()
if "general" not in local_settings:
local_settings["general"] = {}
local_settings["general"]["is_admin"] = True
save_local_settings(local_settings)
self._final_result = input_value
self.close()
def _on_show_password(self, show_password):
if show_password:
echo_mode = QtWidgets.QLineEdit.Normal
else:
echo_mode = QtWidgets.QLineEdit.Password
self.password_input.setEchoMode(echo_mode)
def _on_cancel_click(self):
self.close()

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.0.0-beta2"
__version__ = "3.0.0-rc2"

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
version = "3.0.0-beta2"
version = "3.0.0-rc2"
description = "Open VFX and Animation pipeline with support."
authors = ["OpenPype Team <info@openpype.io>"]
license = "MIT License"

@ -1 +1 @@
Subproject commit 807e8577a0268580a2934ba38889911adad26eb1
Subproject commit cfd4191e364b47de7364096f45d9d9d9a901692a

View file

@ -70,8 +70,6 @@ function Install-Poetry() {
Write-Host ">>> " -NoNewline -ForegroundColor Green
Write-Host "Installing Poetry ... "
(Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python -
# add it to PATH
$env:PATH = "$($env:PATH);$($env:USERPROFILE)\.poetry\bin"
}
$art = @"
@ -101,6 +99,14 @@ $current_dir = Get-Location
$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$openpype_root = (Get-Item $script_dir).parent.FullName
$env:_INSIDE_OPENPYPE_TOOL = "1"
# make sure Poetry is in PATH
if (-not (Test-Path 'env:POETRY_HOME')) {
$env:POETRY_HOME = "$openpype_root\.poetry"
}
$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin"
Set-Location -Path $openpype_root
$version_file = Get-Content -Path "$($openpype_root)\openpype\version.py"
@ -134,47 +140,20 @@ Write-Host "Making sure submodules are up-to-date ..."
git submodule update --init --recursive
Write-Host ">>> " -NoNewline -ForegroundColor green
Write-Host "Building OpenPype [ " -NoNewline -ForegroundColor white
Write-Host "OpenPype [ " -NoNewline -ForegroundColor white
Write-host $openpype_version -NoNewline -ForegroundColor green
Write-Host " ] ..." -ForegroundColor white
Write-Host ">>> " -NoNewline -ForegroundColor green
Write-Host "Detecting host Python ... " -NoNewline
if (-not (Get-Command "python" -ErrorAction SilentlyContinue)) {
Write-Host "!!! Python not detected" -ForegroundColor red
Exit-WithCode 1
}
$version_command = @"
import sys
print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1]))
"@
$p = & python -c $version_command
$env:PYTHON_VERSION = $p
$m = $p -match '(\d+)\.(\d+)'
if(-not $m) {
Write-Host "!!! Cannot determine version" -ForegroundColor red
Exit-WithCode 1
}
# We are supporting python 3.6 and up
if(($matches[1] -lt 3) -or ($matches[2] -lt 7)) {
Write-Host "FAILED Version [ $p ] is old and unsupported" -ForegroundColor red
Exit-WithCode 1
}
Write-Host "OK [ $p ]" -ForegroundColor green
Write-Host " ]" -ForegroundColor white
Write-Host ">>> " -NoNewline -ForegroundColor Green
Write-Host "Reading Poetry ... " -NoNewline
if (-not (Test-Path -PathType Container -Path "$($env:USERPROFILE)\.poetry\bin")) {
if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) {
Write-Host "NOT FOUND" -ForegroundColor Yellow
Install-Poetry
Write-Host "INSTALLED" -ForegroundColor Cyan
Write-Host "*** " -NoNewline -ForegroundColor Yellow
Write-Host "We need to install Poetry create virtual env first ..."
& "$openpype_root\tools\create_env.ps1"
} else {
Write-Host "OK" -ForegroundColor Green
}
$env:PATH = "$($env:PATH);$($env:USERPROFILE)\.poetry\bin"
Write-Host ">>> " -NoNewline -ForegroundColor green
Write-Host "Cleaning cache files ... " -NoNewline

View file

@ -132,7 +132,6 @@ install_poetry () {
echo -e "${BIGreen}>>>${RST} Installing Poetry ..."
command -v curl >/dev/null 2>&1 || { echo -e "${BIRed}!!!${RST}${BIYellow} Missing ${RST}${BIBlue}curl${BIYellow} command.${RST}"; return 1; }
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3 -
export PATH="$PATH:$HOME/.poetry/bin"
}
# Main
@ -149,6 +148,14 @@ main () {
version_command="import os;exec(open(os.path.join('$openpype_root', 'openpype', 'version.py')).read());print(__version__);"
openpype_version="$(python3 <<< ${version_command})"
_inside_openpype_tool="1"
# make sure Poetry is in PATH
if [[ -z $POETRY_HOME ]]; then
export POETRY_HOME="$openpype_root/.poetry"
fi
export PATH="$POETRY_HOME/bin:$PATH"
echo -e "${BIYellow}---${RST} Cleaning build directory ..."
rm -rf "$openpype_root/build" && mkdir "$openpype_root/build" > /dev/null
@ -157,12 +164,12 @@ main () {
clean_pyc
echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c"
if [ -f "$HOME/.poetry/bin/poetry" ]; then
if [ -f "$POETRY_HOME/bin/poetry" ]; then
echo -e "${BIGreen}OK${RST}"
export PATH="$PATH:$HOME/.poetry/bin"
else
echo -e "${BIYellow}NOT FOUND${RST}"
install_poetry || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return; }
echo -e "${BIYellow}***${RST} We need to install Poetry and virtual env ..."
. "$openpype_root/tools/create_env.sh" || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return; }
fi
echo -e "${BIGreen}>>>${RST} Making sure submodules are up-to-date ..."

View file

@ -43,9 +43,10 @@ function Show-PSWarning() {
function Install-Poetry() {
Write-Host ">>> " -NoNewline -ForegroundColor Green
Write-Host "Installing Poetry ... "
$env:POETRY_HOME="$openpype_root\.poetry"
(Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python -
# add it to PATH
$env:PATH = "$($env:PATH);$($env:USERPROFILE)\.poetry\bin"
$env:PATH = "$($env:PATH);$openpype_root\.poetry\bin"
}
@ -84,6 +85,12 @@ $current_dir = Get-Location
$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$openpype_root = (Get-Item $script_dir).parent.FullName
# make sure Poetry is in PATH
if (-not (Test-Path 'env:POETRY_HOME')) {
$env:POETRY_HOME = "$openpype_root\.poetry"
}
$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin"
Set-Location -Path $openpype_root
$art = @"
@ -105,8 +112,9 @@ $art = @"
"@
Write-Host $art -ForegroundColor DarkGreen
if (-not (Test-Path 'env:_INSIDE_OPENPYPE_TOOL')) {
Write-Host $art -ForegroundColor DarkGreen
}
# Enable if PS 7.x is needed.
# Show-PSWarning
@ -128,7 +136,7 @@ Test-Python
Write-Host ">>> " -NoNewline -ForegroundColor Green
Write-Host "Reading Poetry ... " -NoNewline
if (-not (Test-Path -PathType Container -Path "$($env:USERPROFILE)\.poetry\bin")) {
if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) {
Write-Host "NOT FOUND" -ForegroundColor Yellow
Install-Poetry
Write-Host "INSTALLED" -ForegroundColor Cyan

View file

@ -144,19 +144,27 @@ realpath () {
main () {
# Main
echo -e "${BGreen}"
art
echo -e "${RST}"
if [[ -z $_inside_openpype_tool ]]; then
echo -e "${BGreen}"
art
echo -e "${RST}"
fi
detect_python || return 1
# Directories
openpype_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}")))
# make sure Poetry is in PATH
if [[ -z $POETRY_HOME ]]; then
export POETRY_HOME="$openpype_root/.poetry"
fi
export PATH="$POETRY_HOME/bin:$PATH"
pushd "$openpype_root" > /dev/null || return > /dev/null
echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c"
if [ -f "$HOME/.poetry/bin/poetry" ]; then
if [ -f "$POETRY_HOME/bin/poetry" ]; then
echo -e "${BIGreen}OK${RST}"
export PATH="$PATH:$HOME/.poetry/bin"
else
echo -e "${BIYellow}NOT FOUND${RST}"
install_poetry || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return; }

View file

@ -37,6 +37,15 @@ function Show-PSWarning() {
$current_dir = Get-Location
$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$openpype_root = (Get-Item $script_dir).parent.FullName
$env:_INSIDE_OPENPYPE_TOOL = "1"
# make sure Poetry is in PATH
if (-not (Test-Path 'env:POETRY_HOME')) {
$env:POETRY_HOME = "$openpype_root\.poetry"
}
$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin"
Set-Location -Path $openpype_root
$art = @"
@ -71,6 +80,17 @@ if (-not $openpype_version) {
Exit-WithCode 1
}
Write-Host ">>> " -NoNewline -ForegroundColor Green
Write-Host "Reading Poetry ... " -NoNewline
if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) {
Write-Host "NOT FOUND" -ForegroundColor Yellow
Write-Host "*** " -NoNewline -ForegroundColor Yellow
Write-Host "We need to install Poetry create virtual env first ..."
& "$openpype_root\tools\create_env.ps1"
} else {
Write-Host "OK" -ForegroundColor Green
}
Write-Host ">>> " -NoNewline -ForegroundColor green
Write-Host "Cleaning cache files ... " -NoNewline
Get-ChildItem $openpype_root -Filter "*.pyc" -Force -Recurse | Remove-Item -Force

View file

@ -128,8 +128,26 @@ main () {
# Directories
openpype_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}")))
_inside_openpype_tool="1"
# make sure Poetry is in PATH
if [[ -z $POETRY_HOME ]]; then
export POETRY_HOME="$openpype_root/.poetry"
fi
export PATH="$POETRY_HOME/bin:$PATH"
pushd "$openpype_root" > /dev/null || return > /dev/null
echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c"
if [ -f "$POETRY_HOME/bin/poetry" ]; then
echo -e "${BIGreen}OK${RST}"
else
echo -e "${BIYellow}NOT FOUND${RST}"
echo -e "${BIYellow}***${RST} We need to install Poetry and virtual env ..."
. "$openpype_root/tools/create_env.sh" || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return; }
fi
echo -e "${BIGreen}>>>${RST} Generating zip from current sources ..."
PYTHONPATH="$openpype_root:$PYTHONPATH"
OPENPYPE_ROOT="$openpype_root"

View file

@ -14,7 +14,28 @@ PS> .\fetch_thirdparty_libs.ps1
$current_dir = Get-Location
$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$openpype_root = (Get-Item $script_dir).parent.FullName
$env:_INSIDE_OPENPYPE_TOOL = "1"
# make sure Poetry is in PATH
if (-not (Test-Path 'env:POETRY_HOME')) {
$env:POETRY_HOME = "$openpype_root\.poetry"
}
$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin"
Set-Location -Path $openpype_root
Write-Host ">>> " -NoNewline -ForegroundColor Green
Write-Host "Reading Poetry ... " -NoNewline
if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) {
Write-Host "NOT FOUND" -ForegroundColor Yellow
Write-Host "*** " -NoNewline -ForegroundColor Yellow
Write-Host "We need to install Poetry create virtual env first ..."
& "$openpype_root\tools\create_env.ps1"
} else {
Write-Host "OK" -ForegroundColor Green
}
& poetry run python "$($openpype_root)\tools\fetch_thirdparty_libs.py"
Set-Location -Path $current_dir

View file

@ -116,14 +116,31 @@ main () {
echo -e "${BGreen}"
art
echo -e "${RST}"
detect_python || return 1
# Directories
pype_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}")))
pushd "$pype_root" > /dev/null || return > /dev/null
openpype_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}")))
_inside_openpype_tool="1"
# make sure Poetry is in PATH
if [[ -z $POETRY_HOME ]]; then
export POETRY_HOME="$openpype_root/.poetry"
fi
export PATH="$POETRY_HOME/bin:$PATH"
echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c"
if [ -f "$POETRY_HOME/bin/poetry" ]; then
echo -e "${BIGreen}OK${RST}"
else
echo -e "${BIYellow}NOT FOUND${RST}"
echo -e "${BIYellow}***${RST} We need to install Poetry and virtual env ..."
. "$openpype_root/tools/create_env.sh" || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return; }
fi
pushd "$openpype_root" > /dev/null || return > /dev/null
echo -e "${BIGreen}>>>${RST} Running Pype tool ..."
poetry run python3 "$pype_root/tools/fetch_thirdparty_libs.py"
poetry run python3 "$openpype_root/tools/fetch_thirdparty_libs.py"
}
main

View file

@ -16,6 +16,15 @@ PS> .\make_docs.ps1
$current_dir = Get-Location
$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$openpype_root = (Get-Item $script_dir).parent.FullName
$env:_INSIDE_OPENPYPE_TOOL = "1"
# make sure Poetry is in PATH
if (-not (Test-Path 'env:POETRY_HOME')) {
$env:POETRY_HOME = "$openpype_root\.poetry"
}
$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin"
Set-Location -Path $openpype_root
@ -39,6 +48,17 @@ $art = @"
Write-Host $art -ForegroundColor DarkGreen
Write-Host ">>> " -NoNewline -ForegroundColor Green
Write-Host "Reading Poetry ... " -NoNewline
if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) {
Write-Host "NOT FOUND" -ForegroundColor Yellow
Write-Host "*** " -NoNewline -ForegroundColor Yellow
Write-Host "We need to install Poetry create virtual env first ..."
& "$openpype_root\tools\create_env.ps1"
} else {
Write-Host "OK" -ForegroundColor Green
}
Write-Host "This will not overwrite existing source rst files, only scan and add new."
Set-Location -Path $openpype_root
Write-Host ">>> " -NoNewline -ForegroundColor green

View file

@ -80,6 +80,24 @@ main () {
# Directories
openpype_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}")))
_inside_openpype_tool="1"
# make sure Poetry is in PATH
if [[ -z $POETRY_HOME ]]; then
export POETRY_HOME="$openpype_root/.poetry"
fi
export PATH="$POETRY_HOME/bin:$PATH"
echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c"
if [ -f "$POETRY_HOME/bin/poetry" ]; then
echo -e "${BIGreen}OK${RST}"
else
echo -e "${BIYellow}NOT FOUND${RST}"
echo -e "${BIYellow}***${RST} We need to install Poetry and virtual env ..."
. "$openpype_root/tools/create_env.sh" || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return; }
fi
pushd "$openpype_root" > /dev/null || return > /dev/null
echo -e "${BIGreen}>>>${RST} Running apidoc ..."

View file

@ -0,0 +1,32 @@
<#
.SYNOPSIS
Helper script to run mongodb.
.DESCRIPTION
This script will detect mongodb, add it to the PATH and launch it on specified port and db location.
.EXAMPLE
PS> .\run_mongo.ps1
#>
$art = @"
.---= [ by Pype Club ] =---.
https://openpype.io
"@
Write-Host $art -ForegroundColor DarkGreen
$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$openpype_root = (Get-Item $script_dir).parent.FullName
cd $openpype_root/website
yarn run start

View file

@ -14,6 +14,27 @@ PS> .\run_settings.ps1
$current_dir = Get-Location
$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$openpype_root = (Get-Item $script_dir).parent.FullName
$env:_INSIDE_OPENPYPE_TOOL = "1"
# make sure Poetry is in PATH
if (-not (Test-Path 'env:POETRY_HOME')) {
$env:POETRY_HOME = "$openpype_root\.poetry"
}
$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin"
Set-Location -Path $openpype_root
Write-Host ">>> " -NoNewline -ForegroundColor Green
Write-Host "Reading Poetry ... " -NoNewline
if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) {
Write-Host "NOT FOUND" -ForegroundColor Yellow
Write-Host "*** " -NoNewline -ForegroundColor Yellow
Write-Host "We need to install Poetry create virtual env first ..."
& "$openpype_root\tools\create_env.ps1"
} else {
Write-Host "OK" -ForegroundColor Green
}
& poetry run python "$($openpype_root)\start.py" settings --dev
Set-Location -Path $current_dir

View file

@ -57,6 +57,7 @@ BIPurple='\033[1;95m' # Purple
BICyan='\033[1;96m' # Cyan
BIWhite='\033[1;97m' # White
##############################################################################
# Return absolute path
# Globals:
@ -72,10 +73,29 @@ realpath () {
# Main
main () {
# Directories
openpype_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}")))
_inside_openpype_tool="1"
# make sure Poetry is in PATH
if [[ -z $POETRY_HOME ]]; then
export POETRY_HOME="$openpype_root/.poetry"
fi
export PATH="$POETRY_HOME/bin:$PATH"
pushd "$openpype_root" > /dev/null || return > /dev/null
echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c"
if [ -f "$POETRY_HOME/bin/poetry" ]; then
echo -e "${BIGreen}OK${RST}"
else
echo -e "${BIYellow}NOT FOUND${RST}"
echo -e "${BIYellow}***${RST} We need to install Poetry and virtual env ..."
. "$openpype_root/tools/create_env.sh" || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return; }
fi
echo -e "${BIGreen}>>>${RST} Generating zip from current sources ..."
poetry run python3 "$openpype_root/start.py" settings --dev
}

View file

@ -57,6 +57,14 @@ $current_dir = Get-Location
$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$openpype_root = (Get-Item $script_dir).parent.FullName
$env:_INSIDE_OPENPYPE_TOOL = "1"
# make sure Poetry is in PATH
if (-not (Test-Path 'env:POETRY_HOME')) {
$env:POETRY_HOME = "$openpype_root\.poetry"
}
$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin"
Set-Location -Path $openpype_root
$version_file = Get-Content -Path "$($openpype_root)\openpype\version.py"
@ -69,34 +77,20 @@ if (-not $openpype_version) {
}
Write-Host ">>> " -NoNewline -ForegroundColor green
Write-Host "Building OpenPype [ " -NoNewline -ForegroundColor white
Write-Host "OpenPype [ " -NoNewline -ForegroundColor white
Write-host $openpype_version -NoNewline -ForegroundColor green
Write-Host " ] ..." -ForegroundColor white
Write-Host ">>> " -NoNewline -ForegroundColor green
Write-Host "Detecting host Python ... " -NoNewline
if (-not (Get-Command "python" -ErrorAction SilentlyContinue)) {
Write-Host "!!! Python not detected" -ForegroundColor red
Exit-WithCode 1
Write-Host ">>> " -NoNewline -ForegroundColor Green
Write-Host "Reading Poetry ... " -NoNewline
if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) {
Write-Host "NOT FOUND" -ForegroundColor Yellow
Write-Host "*** " -NoNewline -ForegroundColor Yellow
Write-Host "We need to install Poetry create virtual env first ..."
& "$openpype_root\tools\create_env.ps1"
} else {
Write-Host "OK" -ForegroundColor Green
}
$version_command = @"
import sys
print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1]))
"@
$p = & python -c $version_command
$env:PYTHON_VERSION = $p
$m = $p -match '(\d+)\.(\d+)'
if(-not $m) {
Write-Host "!!! Cannot determine version" -ForegroundColor red
Exit-WithCode 1
}
# We are supporting python 3.6 and up
if(($matches[1] -lt 3) -or ($matches[2] -lt 7)) {
Write-Host "FAILED Version [ $p ] is old and unsupported" -ForegroundColor red
Exit-WithCode 1
}
Write-Host "OK [ $p ]" -ForegroundColor green
Write-Host ">>> " -NoNewline -ForegroundColor green
Write-Host "Cleaning cache files ... " -NoNewline

View file

@ -57,32 +57,6 @@ BIPurple='\033[1;95m' # Purple
BICyan='\033[1;96m' # Cyan
BIWhite='\033[1;97m' # White
##############################################################################
# Detect required version of python
# Globals:
# colors
# PYTHON
# Arguments:
# None
# Returns:
# None
###############################################################################
detect_python () {
echo -e "${BIGreen}>>>${RST} Using python \c"
local version_command="import sys;print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1]))"
local python_version="$(python3 <<< ${version_command})"
oIFS="$IFS"
IFS=.
set -- $python_version
IFS="$oIFS"
if [ "$1" -ge "3" ] && [ "$2" -ge "6" ] ; then
echo -e "${BIWhite}[${RST} ${BIGreen}$1.$2${RST} ${BIWhite}]${RST}"
else
command -v python3 >/dev/null 2>&1 || { echo -e "${BIRed}FAILED${RST} ${BIYellow} Version [${RST}${BICyan}$1.$2${RST}]${BIYellow} is old and unsupported${RST}"; return 1; }
fi
}
##############################################################################
# Clean pyc files in specified directory
# Globals:
@ -118,10 +92,27 @@ main () {
echo -e "${BGreen}"
art
echo -e "${RST}"
detect_python || return 1
# Directories
openpype_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}")))
_inside_openpype_tool="1"
# make sure Poetry is in PATH
if [[ -z $POETRY_HOME ]]; then
export POETRY_HOME="$openpype_root/.poetry"
fi
export PATH="$POETRY_HOME/bin:$PATH"
echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c"
if [ -f "$POETRY_HOME/bin/poetry" ]; then
echo -e "${BIGreen}OK${RST}"
else
echo -e "${BIYellow}NOT FOUND${RST}"
echo -e "${BIYellow}***${RST} We need to install Poetry and virtual env ..."
. "$openpype_root/tools/create_env.sh" || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return; }
fi
pushd "$openpype_root" || return > /dev/null
echo -e "${BIGreen}>>>${RST} Testing OpenPype ..."

View file

@ -13,7 +13,27 @@ PS> .\run_tray.ps1
$current_dir = Get-Location
$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$openpype_root = (Get-Item $script_dir).parent.FullName
$env:_INSIDE_OPENPYPE_TOOL = "1"
# make sure Poetry is in PATH
if (-not (Test-Path 'env:POETRY_HOME')) {
$env:POETRY_HOME = "$openpype_root\.poetry"
}
$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin"
Set-Location -Path $openpype_root
Write-Host ">>> " -NoNewline -ForegroundColor Green
Write-Host "Reading Poetry ... " -NoNewline
if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) {
Write-Host "NOT FOUND" -ForegroundColor Yellow
Write-Host "*** " -NoNewline -ForegroundColor Yellow
Write-Host "We need to install Poetry create virtual env first ..."
& "$openpype_root\tools\create_env.ps1"
} else {
Write-Host "OK" -ForegroundColor Green
}
& poetry run python "$($openpype_root)\start.py" tray --debug
Set-Location -Path $current_dir

View file

@ -53,6 +53,24 @@ realpath () {
main () {
# Directories
openpype_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}")))
_inside_openpype_tool="1"
# make sure Poetry is in PATH
if [[ -z $POETRY_HOME ]]; then
export POETRY_HOME="$openpype_root/.poetry"
fi
export PATH="$POETRY_HOME/bin:$PATH"
echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c"
if [ -f "$POETRY_HOME/bin/poetry" ]; then
echo -e "${BIGreen}OK${RST}"
else
echo -e "${BIYellow}NOT FOUND${RST}"
echo -e "${BIYellow}***${RST} We need to install Poetry and virtual env ..."
. "$openpype_root/tools/create_env.sh" || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return; }
fi
pushd "$openpype_root" > /dev/null || return > /dev/null
echo -e "${BIGreen}>>>${RST} Running OpenPype Tray with debug option ..."

View file

@ -0,0 +1,208 @@
---
id: artist_hosts_tvpaint
title: TVPaint
sidebar_label: TVPaint
---
- [Work Files](artist_tools.md#workfiles)
- [Load](artist_tools.md#loader)
- [Create](artist_tools.md#creator)
- [Subset Manager](artist_tools.md#subset-manager)
- [Scene Inventory](artist_tools.md#scene-inventory)
- [Publish](artist_tools.md#publisher)
- [Library](artist_tools.md#library)
## Setup
When you launch TVPaint with OpenPype for the very first time it is necessary to do some additional steps. Right after the TVPaint launching a few system windows will pop up.
![permission](assets/tvp_permission.png)
Choose `Replace the file in the destination`. Then another window shows up.
![permission2](assets/tvp_permission2.png)
Click on `Continue`.
After opening TVPaint go to the menu bar: `Windows → Plugins → OpenPype`.
![pypewindow](assets/tvp_hidden_window.gif)
Another TVPaint window pop up. Please press `Yes`. This window will be presented in every single TVPaint launching. Unfortunately, there is no other way how to workaround it.
![writefile](assets/tvp_write_file.png)
Now OpenPype Tools menu is in your TVPaint work area.
![openpypetools](assets/tvp_openpype_menu.png)
You can start your work.
---
## Usage
In TVPaint you can find the Tools in OpenPype menu extension. The OpenPype Tools menu should be available in your work area. However, sometimes it happens that the Tools menu is hidden. You can display the extension panel by going to `Windows -> Plugins -> OpenPype`.
## Create
In TVPaint you can create and publish **[Reviews](#review)**, **[Render Passes](#render-pass)**, and **[Render Layers](#render-layer)**.
You have the possibility to organize your layers by using `Color group`.
On the bottom left corner of your timeline, you will note a `Color group` button.
![colorgroups](assets/tvp_color_groups.png)
It allows you to choose a group by checking one of the colors of the color list.
![colorgroups](assets/tvp_color_groups2.png)
The timeline's animation layer can be marked by the color you pick from your Color group. Layers in the timeline with the same color are gathered into a group represents one render layer.
![timeline](assets/tvp_timeline_color.png)
:::important
OpenPype specifically never tries to guess what you want to publish from the scene. Therefore, you have to tell OpenPype what you want to publish. There are three ways how to publish render from the scene.
:::
When you want to publish `review` or `render layer` or `render pass`, open the `Creator` through the Tools menu `Create` button.
### Review
<div class="row markdown">
<div class="col col--6 markdown">
`Review` renders the whole file as is and sends the resulting QuickTime to Ftrack.
To create reviewable quicktime of your animation:
- select `Review` in the `Creator`
- press `Create`
- When you run [publish](#publish), file will be rendered and converted to quicktime.`
</div>
<div class="col col--6 markdown">
![createreview](assets/tvp_create_review.png)
</div>
</div>
### Render Layer
<div class="row markdown">
<div class="col col--6 markdown">
Render Layer bakes all the animation layers of one particular color group together.
- Choose any amount of animation layers that need to be rendered together and assign them a color group.
- Select any layer of a particular color
- Go to `Creator` and choose `RenderLayer`.
- In the `Subset`, type in the name that the final published RenderLayer should have according to the naming convention in your studio. *(L10, BG, Hero, etc.)*
- Press `Create`
- When you run [publish](#publish), the whole color group will be rendered together and published as a single `RenderLayer`
</div>
<div class="col col--6 markdown">
![createlayer](assets/tvp_create_layer.png)
</div>
</div>
### Render Pass
Render Passes are smaller individual elements of a Render Layer. A `character` render layer might
consist of multiple render passes such as `Line`, `Color` and `Shadow`.
<div class="row markdown">
<div class="col col--6 markdown">
Render Passes are specific because they have to belong to a particular layer. If you try to create a render pass and did not create any render layers before, an error message will pop up.
When you want to create `RenderPass`
- choose one or several animation layers within one color group that you want to publish
- In the Creator, pick `RenderPass`
- Fill the `Subset` with the name of your pass, e.g. `Color`.
- Press `Create`
</div>
<div class="col col--6 markdown">
![createpass](assets/tvp_create_pass.png)
</div>
</div>
<br></br>
In this example, OpenPype will render selected animation layers within the given color group. E.i. the layers *L020_colour_fx*, *L020_colour_mouth*, and *L020_colour_eye* will be rendered as one pass belonging to the yellow RenderLayer.
![renderpass](assets/tvp_timeline_color2.png)
:::note
You can check your RendrePasses and RenderLayers in [Subset Manager](#subset-manager) or you can start publishing. The publisher will show you a collection of all instances on the left side.
:::
---
## Publish
<div class="row markdown">
<div class="col col--6 markdown">
Now that you have created the required instances, you can publish them via `Publish` tool.
- Click on `Publish` in OpenPype Tools menu.
- wait until all instances are collected.
- You can check on the left side whether all your instances have been created and are ready for publishing.
- Fill the comment on the bottom of the window.
- Press the `Play` button to publish
</div>
<div class="col col--6 markdown">
![pyblish](assets/tvp_pyblish_render.png)
</div>
</div>
Once the `Publisher` turns gets green your renders have been published.
---
## Subset Manager
All created instances (render layers, passes, and reviews) will be shown as a simple list. If you don't want to publish some, right click on the item in the list and select `Remove instance`.
![subsetmanager](assets/tvp_subset_manager.png)
---
## Load
When you want to load existing published work you can reach the `Loader` through the OpenPype Tools `Load` button.
The supported families for TVPaint are:
- `render`
- `image`
- `background`
- `plate`
To load a family item, right-click on the subset you want and import their representations, switch among the versions, delete older versions, copy files, etc.
![Loader](assets/tvp_loader.gif)
---
## Scene Inventory
Scene Inventory shows you everything that you have loaded into your scene using OpenPype. You can reach it through the extension's `Scene Inventory` button.
![sceneinventory](assets/tvp_scene_inventory.png)
You can switch to a previous version of the file or update it to the latest or delete items.

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View file

@ -23,6 +23,7 @@ module.exports = {
"artist_hosts_harmony",
"artist_hosts_aftereffects",
"artist_hosts_photoshop",
"artist_hosts_tvpaint",
"artist_hosts_unreal",
{
type: "category",