diff --git a/.gitignore b/.gitignore index ebb47e55d2..26bf7cf65f 100644 --- a/.gitignore +++ b/.gitignore @@ -91,4 +91,9 @@ website/i18n/* website/debug.log -website/.docusaurus \ No newline at end of file +website/.docusaurus + +# Poetry +######## + +.poetry/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 364555f8b2..b70f3f98f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/igniter/version.py b/igniter/version.py index 8c8ffdccb7..4f8f0907e9 100644 --- a/igniter/version.py +++ b/igniter/version.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- """Definition of Igniter version.""" -__version__ = "1.0.0-beta" +__version__ = "1.0.0-rc1" diff --git a/openpype/hosts/maya/plugins/publish/collect_remove_marked.py b/openpype/hosts/maya/plugins/publish/collect_remove_marked.py index a45c8e45a7..69e69f6630 100644 --- a/openpype/hosts/maya/plugins/publish/collect_remove_marked.py +++ b/openpype/hosts/maya/plugins/publish/collect_remove_marked.py @@ -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 """ diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_clear_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_clear_instances.py deleted file mode 100644 index 097e730251..0000000000 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_clear_instances.py +++ /dev/null @@ -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) diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index af578de86b..ee139a500e 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -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 diff --git a/openpype/hosts/maya/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py similarity index 64% rename from openpype/hosts/maya/plugins/publish/collect_ftrack_family.py rename to openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index b2b66b1875..e6daed9a33 100644 --- a/openpype/hosts/maya/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -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): diff --git a/openpype/modules/settings_action.py b/openpype/modules/settings_action.py index 3f7cb8c3ba..5651868f68 100644 --- a/openpype/modules/settings_action.py +++ b/openpype/modules/settings_action.py @@ -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() diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index a434af9fea..5645cdfbec 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -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 diff --git a/openpype/modules/sync_server/tray/app.py b/openpype/modules/sync_server/tray/app.py index 2538675c51..b3b6f0a6c3 100644 --- a/openpype/modules/sync_server/tray/app.py +++ b/openpype/modules/sync_server/tray/app.py @@ -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() diff --git a/openpype/modules/sync_server/tray/delegates.py b/openpype/modules/sync_server/tray/delegates.py new file mode 100644 index 0000000000..9316ec2c3e --- /dev/null +++ b/openpype/modules/sync_server/tray/delegates.py @@ -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))) diff --git a/openpype/modules/sync_server/tray/lib.py b/openpype/modules/sync_server/tray/lib.py index 04bd1f568e..c1f8eaf629 100644 --- a/openpype/modules/sync_server/tray/lib.py +++ b/openpype/modules/sync_server/tray/lib.py @@ -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) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 8fdd9487a4..efef039b8b 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -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) diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index 106fc4b8a8..e80f91e09f 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -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): diff --git a/openpype/plugins/publish/collect_current_pype_user.py b/openpype/plugins/publish/collect_current_pype_user.py index 003c779836..1a52a59012 100644 --- a/openpype/plugins/publish/collect_current_pype_user.py +++ b/openpype/plugins/publish/collect_current_pype_user.py @@ -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)) diff --git a/openpype/plugins/publish/collect_scene_version.py b/openpype/plugins/publish/collect_scene_version.py index f58bd0dd9d..669e6752f3 100644 --- a/openpype/plugins/publish/collect_scene_version.py +++ b/openpype/plugins/publish/collect_scene_version.py @@ -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 '' in filename: diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index e266c39714..1e805afba7 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -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. diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index 5e2a22f1b5..6dcf00e97c 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -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") diff --git a/openpype/settings/defaults/project_anatomy/attributes.json b/openpype/settings/defaults/project_anatomy/attributes.json index 3ad6761331..921d12e32a 100644 --- a/openpype/settings/defaults/project_anatomy/attributes.json +++ b/openpype/settings/defaults/project_anatomy/attributes.json @@ -20,7 +20,7 @@ "harmony/20", "photoshop/2021", "aftereffects/2021", - "unreal/4-24" + "unreal/4-26" ], "tools_env": [] } \ No newline at end of file diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 905ba68d60..03f3e19a64 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -21,8 +21,8 @@ "secondary_pool": "", "group": "", "department": "", - "limit_groups": {}, - "use_gpu": true + "use_gpu": true, + "limit_groups": {} }, "HarmonySubmitDeadline": { "enabled": true, diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 61db35ba79..712ec22cfc 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -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 diff --git a/openpype/settings/defaults/project_settings/hiero.json b/openpype/settings/defaults/project_settings/hiero.json index a8d6472c47..44b27fc16f 100644 --- a/openpype/settings/defaults/project_settings/hiero.json +++ b/openpype/settings/defaults/project_settings/hiero.json @@ -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}" } diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 56d63ecf09..e77b5d0ce5 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -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" } } }, diff --git a/openpype/settings/defaults/system_settings/general.json b/openpype/settings/defaults/system_settings/general.json index 2568e8b6a8..d03fedf3c9 100644 --- a/openpype/settings/defaults/system_settings/general.json +++ b/openpype/settings/defaults/system_settings/general.json @@ -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": { diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index b5c42e1da0..3e73fa8aa6 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -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") diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index d5563f80d6..052bbda4d0 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -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): diff --git a/openpype/settings/entities/dict_mutable_keys_entity.py b/openpype/settings/entities/dict_mutable_keys_entity.py index 19eb83072e..ef0124c0f0 100644 --- a/openpype/settings/entities/dict_mutable_keys_entity.py +++ b/openpype/settings/entities/dict_mutable_keys_entity.py @@ -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: diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 2ca20542ef..9278cfd9b0 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -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 diff --git a/openpype/settings/entities/item_entities.py b/openpype/settings/entities/item_entities.py index 56e7d1c7b2..fc92c28db9 100644 --- a/openpype/settings/entities/item_entities.py +++ b/openpype/settings/entities/item_entities.py @@ -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() diff --git a/openpype/settings/entities/list_entity.py b/openpype/settings/entities/list_entity.py index ee647264b7..a57468fff7 100644 --- a/openpype/settings/entities/list_entity.py +++ b/openpype/settings/entities/list_entity.py @@ -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: diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json index edd5c18f51..3c589f9492 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json @@ -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" } }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 1bd028ac79..62de311024 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -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", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_publish_gui_filter.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_publish_gui_filter.json index 1539bd0738..f27ca9586f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_publish_gui_filter.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_publish_gui_filter.json @@ -4,7 +4,6 @@ "key": "filters", "label": "Publish GUI Filters", "object_type": { - "type": "raw-json", - "label": "Plugins" + "type": "raw-json" } } diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_blender.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_blender.json index e6e7381e9f..e72b1fac5b 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_blender.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_blender.json @@ -28,7 +28,6 @@ "object_type": { "type": "dict", "collapsible": true, - "checkbox_key": "enabled", "children": [ { "type": "schema_template", diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_djv.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_djv.json index a95cedf7c3..229ca42b04 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_djv.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_djv.json @@ -28,7 +28,6 @@ "object_type": { "type": "dict", "collapsible": true, - "checkbox_key": "enabled", "children": [ { "type": "schema_template", diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_houdini.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_houdini.json index 22a5b2e737..ee88d90e8e 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_houdini.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_houdini.json @@ -28,7 +28,6 @@ "object_type": { "type": "dict", "collapsible": true, - "checkbox_key": "enabled", "children": [ { "type": "schema_template", diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_maya.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_maya.json index 7c33671fa7..9a81eda88d 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_maya.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_maya.json @@ -28,7 +28,6 @@ "object_type": { "type": "dict", "collapsible": true, - "checkbox_key": "enabled", "children": [ { "type": "schema_template", diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_shell.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_shell.json index e344f98594..fcbb415b12 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_shell.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_shell.json @@ -24,7 +24,6 @@ "object_type": { "type": "dict", "collapsible": true, - "checkbox_key": "enabled", "children": [ { "type": "schema_template", diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_tvpaint.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_tvpaint.json index eac09be113..3c13336eb1 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_tvpaint.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_tvpaint.json @@ -28,7 +28,6 @@ "object_type": { "type": "dict", "collapsible": true, - "checkbox_key": "enabled", "children": [ { "type": "schema_template", diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json index c5096197d6..1752899533 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json @@ -28,7 +28,6 @@ "object_type": { "type": "dict", "collapsible": true, - "checkbox_key": "enabled", "children": [ { "type": "schema_template", diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/template_nuke.json b/openpype/settings/entities/schemas/system_schema/host_settings/template_nuke.json index 3f25c7d72f..0055d0b191 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/template_nuke.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/template_nuke.json @@ -29,7 +29,6 @@ "object_type": { "type": "dict", "collapsible": true, - "checkbox_key": "enabled", "children": [ { "type": "schema_template", diff --git a/openpype/settings/entities/schemas/system_schema/schema_general.json b/openpype/settings/entities/schemas/system_schema/schema_general.json index fd650b4a1e..568ccad5b9 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_general.json +++ b/openpype/settings/entities/schemas/system_schema/schema_general.json @@ -18,6 +18,18 @@ { "type": "splitter" }, + { + "type": "label", + "label": "This is NOT a securely stored password!. 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", diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 44a0cc662d..65ec7291d3 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -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 diff --git a/openpype/tools/settings/__init__.py b/openpype/tools/settings/__init__.py index 3f47d1c2c3..547f4097a2 100644 --- a/openpype/tools/settings/__init__.py +++ b/openpype/tools/settings/__init__.py @@ -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", diff --git a/openpype/tools/settings/lib.py b/openpype/tools/settings/lib.py new file mode 100644 index 0000000000..0317941021 --- /dev/null +++ b/openpype/tools/settings/lib.py @@ -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 diff --git a/openpype/tools/settings/local_settings/general_widget.py b/openpype/tools/settings/local_settings/general_widget.py index 78bc53fdd2..d01c16ff82 100644 --- a/openpype/tools/settings/local_settings/general_widget.py +++ b/openpype/tools/settings/local_settings/general_widget.py @@ -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 diff --git a/openpype/tools/settings/local_settings/window.py b/openpype/tools/settings/local_settings/window.py index b6ca56d348..e117e7fa5d 100644 --- a/openpype/tools/settings/local_settings/window.py +++ b/openpype/tools/settings/local_settings/window.py @@ -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) diff --git a/openpype/tools/settings/resources/__init__.py b/openpype/tools/settings/resources/__init__.py new file mode 100644 index 0000000000..83ce1a286f --- /dev/null +++ b/openpype/tools/settings/resources/__init__.py @@ -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)) diff --git a/openpype/tools/settings/resources/images/eye.png b/openpype/tools/settings/resources/images/eye.png new file mode 100644 index 0000000000..5a683e2974 Binary files /dev/null and b/openpype/tools/settings/resources/images/eye.png differ diff --git a/openpype/tools/settings/settings/widgets/base.py b/openpype/tools/settings/settings/widgets/base.py index 4010b8ab20..3d633a100e 100644 --- a/openpype/tools/settings/settings/widgets/base.py +++ b/openpype/tools/settings/settings/widgets/base.py @@ -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(): diff --git a/openpype/tools/settings/settings/widgets/categories.py b/openpype/tools/settings/settings/widgets/categories.py index e4832c989a..ab28620fc5 100644 --- a/openpype/tools/settings/settings/widgets/categories.py +++ b/openpype/tools/settings/settings/widgets/categories.py @@ -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() diff --git a/openpype/tools/settings/settings/widgets/widgets.py b/openpype/tools/settings/settings/widgets/widgets.py index aa79cc4b62..249b4e305d 100644 --- a/openpype/tools/settings/settings/widgets/widgets.py +++ b/openpype/tools/settings/settings/widgets/widgets.py @@ -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)) diff --git a/openpype/tools/settings/settings/widgets/window.py b/openpype/tools/settings/settings/widgets/window.py index 96275facff..495f909e51 100644 --- a/openpype/tools/settings/settings/widgets/window.py +++ b/openpype/tools/settings/settings/widgets/window.py @@ -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() diff --git a/openpype/tools/settings/widgets.py b/openpype/tools/settings/widgets.py new file mode 100644 index 0000000000..e2662f350f --- /dev/null +++ b/openpype/tools/settings/widgets.py @@ -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() diff --git a/openpype/version.py b/openpype/version.py index dedf799055..25f9e08d80 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.0.0-beta2" +__version__ = "3.0.0-rc2" diff --git a/pyproject.toml b/pyproject.toml index 88c977cd99..c874db34f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 "] license = "MIT License" diff --git a/repos/avalon-core b/repos/avalon-core index 807e8577a0..cfd4191e36 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit 807e8577a0268580a2934ba38889911adad26eb1 +Subproject commit cfd4191e364b47de7364096f45d9d9d9a901692a diff --git a/tools/build.ps1 b/tools/build.ps1 index 0b55e0cdea..566e40cb55 100644 --- a/tools/build.ps1 +++ b/tools/build.ps1 @@ -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 diff --git a/tools/build.sh b/tools/build.sh index 6dfa505a11..0510447e07 100755 --- a/tools/build.sh +++ b/tools/build.sh @@ -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 ..." diff --git a/tools/create_env.ps1 b/tools/create_env.ps1 index 69ea85bfd3..5600ae71c7 100644 --- a/tools/create_env.ps1 +++ b/tools/create_env.ps1 @@ -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 diff --git a/tools/create_env.sh b/tools/create_env.sh index 9e7f1530b0..a29139d884 100755 --- a/tools/create_env.sh +++ b/tools/create_env.sh @@ -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; } diff --git a/tools/create_zip.ps1 b/tools/create_zip.ps1 index 29aaca2978..2fef4d216b 100644 --- a/tools/create_zip.ps1 +++ b/tools/create_zip.ps1 @@ -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 diff --git a/tools/create_zip.sh b/tools/create_zip.sh index c1faaa8592..adaf9431a7 100755 --- a/tools/create_zip.sh +++ b/tools/create_zip.sh @@ -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" diff --git a/tools/fetch_thirdparty_libs.ps1 b/tools/fetch_thirdparty_libs.ps1 index d1b914fac2..23f0b50c7a 100644 --- a/tools/fetch_thirdparty_libs.ps1 +++ b/tools/fetch_thirdparty_libs.ps1 @@ -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 diff --git a/tools/fetch_thirdparty_libs.sh b/tools/fetch_thirdparty_libs.sh index e305b4b3e4..3875541d57 100755 --- a/tools/fetch_thirdparty_libs.sh +++ b/tools/fetch_thirdparty_libs.sh @@ -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 \ No newline at end of file diff --git a/tools/make_docs.ps1 b/tools/make_docs.ps1 index d72005f230..01edaf9c58 100644 --- a/tools/make_docs.ps1 +++ b/tools/make_docs.ps1 @@ -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 diff --git a/tools/make_docs.sh b/tools/make_docs.sh index bb2bbbfb83..9dfab26a38 100755 --- a/tools/make_docs.sh +++ b/tools/make_docs.sh @@ -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 ..." diff --git a/tools/run_documentation.ps1 b/tools/run_documentation.ps1 new file mode 100644 index 0000000000..1be3709642 --- /dev/null +++ b/tools/run_documentation.ps1 @@ -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 + diff --git a/tools/run_settings.ps1 b/tools/run_settings.ps1 index 3f99de4b4e..7477e546b3 100644 --- a/tools/run_settings.ps1 +++ b/tools/run_settings.ps1 @@ -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 \ No newline at end of file diff --git a/tools/run_settings.sh b/tools/run_settings.sh index aae5afdd96..0287043bb6 100755 --- a/tools/run_settings.sh +++ b/tools/run_settings.sh @@ -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 } diff --git a/tools/run_tests.ps1 b/tools/run_tests.ps1 index 5a55f7acc7..7b9a5c841d 100644 --- a/tools/run_tests.ps1 +++ b/tools/run_tests.ps1 @@ -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 diff --git a/tools/run_tests.sh b/tools/run_tests.sh index 94deb1e065..5488be9430 100755 --- a/tools/run_tests.sh +++ b/tools/run_tests.sh @@ -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 ..." diff --git a/tools/run_tray.ps1 b/tools/run_tray.ps1 index 9485584c6f..533a791836 100644 --- a/tools/run_tray.ps1 +++ b/tools/run_tray.ps1 @@ -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 \ No newline at end of file diff --git a/tools/run_tray.sh b/tools/run_tray.sh index d39920efb8..339ff6f918 100755 --- a/tools/run_tray.sh +++ b/tools/run_tray.sh @@ -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 ..." diff --git a/website/docs/artist_hosts_tvpaint.md b/website/docs/artist_hosts_tvpaint.md new file mode 100644 index 0000000000..19cb615158 --- /dev/null +++ b/website/docs/artist_hosts_tvpaint.md @@ -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 + +
+
+ +`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.` + +
+
+ +![createreview](assets/tvp_create_review.png) + +
+
+ +### Render Layer + +
+
+ + +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` + +
+
+ +![createlayer](assets/tvp_create_layer.png) + +
+
+ + + + + +### 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`. + + +
+
+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` + +
+
+ +![createpass](assets/tvp_create_pass.png) + +
+
+ +

+ +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 + +
+
+ +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 + +
+
+ +![pyblish](assets/tvp_pyblish_render.png) + +
+
+ +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. diff --git a/website/docs/assets/tvp_asset_loader_actions.png b/website/docs/assets/tvp_asset_loader_actions.png new file mode 100644 index 0000000000..dbc3734e10 Binary files /dev/null and b/website/docs/assets/tvp_asset_loader_actions.png differ diff --git a/website/docs/assets/tvp_asset_loader_version.png b/website/docs/assets/tvp_asset_loader_version.png new file mode 100644 index 0000000000..b8052f3c84 Binary files /dev/null and b/website/docs/assets/tvp_asset_loader_version.png differ diff --git a/website/docs/assets/tvp_color_groups.png b/website/docs/assets/tvp_color_groups.png new file mode 100644 index 0000000000..558ff687c6 Binary files /dev/null and b/website/docs/assets/tvp_color_groups.png differ diff --git a/website/docs/assets/tvp_color_groups2.png b/website/docs/assets/tvp_color_groups2.png new file mode 100644 index 0000000000..7ab6775769 Binary files /dev/null and b/website/docs/assets/tvp_color_groups2.png differ diff --git a/website/docs/assets/tvp_create_layer.png b/website/docs/assets/tvp_create_layer.png new file mode 100644 index 0000000000..9d243da17a Binary files /dev/null and b/website/docs/assets/tvp_create_layer.png differ diff --git a/website/docs/assets/tvp_create_pass.png b/website/docs/assets/tvp_create_pass.png new file mode 100644 index 0000000000..7d226ea4b5 Binary files /dev/null and b/website/docs/assets/tvp_create_pass.png differ diff --git a/website/docs/assets/tvp_create_review.png b/website/docs/assets/tvp_create_review.png new file mode 100644 index 0000000000..d6e9f63428 Binary files /dev/null and b/website/docs/assets/tvp_create_review.png differ diff --git a/website/docs/assets/tvp_hidden_window.gif b/website/docs/assets/tvp_hidden_window.gif new file mode 100644 index 0000000000..b1adaa75b8 Binary files /dev/null and b/website/docs/assets/tvp_hidden_window.gif differ diff --git a/website/docs/assets/tvp_library.gif b/website/docs/assets/tvp_library.gif new file mode 100644 index 0000000000..14de86cdb5 Binary files /dev/null and b/website/docs/assets/tvp_library.gif differ diff --git a/website/docs/assets/tvp_loader.gif b/website/docs/assets/tvp_loader.gif new file mode 100644 index 0000000000..5775460372 Binary files /dev/null and b/website/docs/assets/tvp_loader.gif differ diff --git a/website/docs/assets/tvp_openpype_menu.png b/website/docs/assets/tvp_openpype_menu.png new file mode 100644 index 0000000000..cb5c2d4aac Binary files /dev/null and b/website/docs/assets/tvp_openpype_menu.png differ diff --git a/website/docs/assets/tvp_permission.png b/website/docs/assets/tvp_permission.png new file mode 100644 index 0000000000..e47d9841b6 Binary files /dev/null and b/website/docs/assets/tvp_permission.png differ diff --git a/website/docs/assets/tvp_permission2.png b/website/docs/assets/tvp_permission2.png new file mode 100644 index 0000000000..827e85db39 Binary files /dev/null and b/website/docs/assets/tvp_permission2.png differ diff --git a/website/docs/assets/tvp_pyblish.png b/website/docs/assets/tvp_pyblish.png new file mode 100644 index 0000000000..88423a3c75 Binary files /dev/null and b/website/docs/assets/tvp_pyblish.png differ diff --git a/website/docs/assets/tvp_pyblish_render.png b/website/docs/assets/tvp_pyblish_render.png new file mode 100644 index 0000000000..7d279c3aa0 Binary files /dev/null and b/website/docs/assets/tvp_pyblish_render.png differ diff --git a/website/docs/assets/tvp_render_pass.png b/website/docs/assets/tvp_render_pass.png new file mode 100644 index 0000000000..07790af7b2 Binary files /dev/null and b/website/docs/assets/tvp_render_pass.png differ diff --git a/website/docs/assets/tvp_scene_inventory.png b/website/docs/assets/tvp_scene_inventory.png new file mode 100644 index 0000000000..25c717b331 Binary files /dev/null and b/website/docs/assets/tvp_scene_inventory.png differ diff --git a/website/docs/assets/tvp_subset_manager.png b/website/docs/assets/tvp_subset_manager.png new file mode 100644 index 0000000000..1ffbead4ba Binary files /dev/null and b/website/docs/assets/tvp_subset_manager.png differ diff --git a/website/docs/assets/tvp_timeline_color.png b/website/docs/assets/tvp_timeline_color.png new file mode 100644 index 0000000000..6d00d4c8af Binary files /dev/null and b/website/docs/assets/tvp_timeline_color.png differ diff --git a/website/docs/assets/tvp_timeline_color2.png b/website/docs/assets/tvp_timeline_color2.png new file mode 100644 index 0000000000..e20e190471 Binary files /dev/null and b/website/docs/assets/tvp_timeline_color2.png differ diff --git a/website/docs/assets/tvp_write_file.png b/website/docs/assets/tvp_write_file.png new file mode 100644 index 0000000000..109e6badc9 Binary files /dev/null and b/website/docs/assets/tvp_write_file.png differ diff --git a/website/sidebars.js b/website/sidebars.js index 4f5b7d604d..41611190fd 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -23,6 +23,7 @@ module.exports = { "artist_hosts_harmony", "artist_hosts_aftereffects", "artist_hosts_photoshop", + "artist_hosts_tvpaint", "artist_hosts_unreal", { type: "category",