Merge branch 'develop' of github.com:pypeclub/OpenPype into chore/OP-2414_Move-harmony-to-openpype

This commit is contained in:
Petr Kalis 2022-02-11 12:24:28 +01:00
commit 28a745d54d
71 changed files with 2628 additions and 1165 deletions

View file

@ -1,8 +1,38 @@
# Changelog
## [3.8.3-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.2...HEAD)
### 📖 Documentation
- documentation: add example to `repack-version` command [\#2669](https://github.com/pypeclub/OpenPype/pull/2669)
- Update docusaurus [\#2639](https://github.com/pypeclub/OpenPype/pull/2639)
- Documentation: Fixed relative links [\#2621](https://github.com/pypeclub/OpenPype/pull/2621)
**🚀 Enhancements**
- Ftrack: Sync description to assets [\#2670](https://github.com/pypeclub/OpenPype/pull/2670)
- Houdini: Moved to OpenPype [\#2658](https://github.com/pypeclub/OpenPype/pull/2658)
- Maya: Move implementation to OpenPype [\#2649](https://github.com/pypeclub/OpenPype/pull/2649)
**🐛 Bug fixes**
- Maya: Fix menu callbacks [\#2671](https://github.com/pypeclub/OpenPype/pull/2671)
- hiero: removing obsolete unsupported plugin [\#2667](https://github.com/pypeclub/OpenPype/pull/2667)
**Merged pull requests:**
- Fix python install in docker for centos7 [\#2664](https://github.com/pypeclub/OpenPype/pull/2664)
- Deadline: Be able to pass Mongo url to job [\#2616](https://github.com/pypeclub/OpenPype/pull/2616)
## [3.8.2](https://github.com/pypeclub/OpenPype/tree/3.8.2) (2022-02-07)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.1...3.8.2)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.8.2-nightly.3...3.8.2)
### 📖 Documentation
- Cosmetics: Fix common typos in openpype/website [\#2617](https://github.com/pypeclub/OpenPype/pull/2617)
**🚀 Enhancements**
@ -11,21 +41,18 @@
- nuke: adding clear button to write nodes [\#2627](https://github.com/pypeclub/OpenPype/pull/2627)
- Ftrack: Family to Asset type mapping is in settings [\#2602](https://github.com/pypeclub/OpenPype/pull/2602)
- Nuke: load color space from representation data [\#2576](https://github.com/pypeclub/OpenPype/pull/2576)
- New Publisher: New features and preparations for new standalone publisher [\#2556](https://github.com/pypeclub/OpenPype/pull/2556)
**🐛 Bug fixes**
- Fix pulling of cx\_freeze 6.10 [\#2628](https://github.com/pypeclub/OpenPype/pull/2628)
### 📖 Documentation
- Cosmetics: Fix common typos in openpype/website [\#2617](https://github.com/pypeclub/OpenPype/pull/2617)
- Global: fix broken otio review extractor [\#2590](https://github.com/pypeclub/OpenPype/pull/2590)
**Merged pull requests:**
- Docker: enhance dockerfiles with metadata, fix pyenv initialization [\#2647](https://github.com/pypeclub/OpenPype/pull/2647)
- WebPublisher: fix instance duplicates [\#2641](https://github.com/pypeclub/OpenPype/pull/2641)
- Fix - safer pulling of task name for webpublishing from PS [\#2613](https://github.com/pypeclub/OpenPype/pull/2613)
- Webpublisher: Skip version collect [\#2591](https://github.com/pypeclub/OpenPype/pull/2591)
## [3.8.1](https://github.com/pypeclub/OpenPype/tree/3.8.1) (2022-02-01)
@ -34,7 +61,6 @@
**🚀 Enhancements**
- Webpublisher: Thumbnail extractor [\#2600](https://github.com/pypeclub/OpenPype/pull/2600)
- Webpublisher: Added endpoint to reprocess batch through UI [\#2555](https://github.com/pypeclub/OpenPype/pull/2555)
- Loader: Allow to toggle default family filters between "include" or "exclude" filtering [\#2541](https://github.com/pypeclub/OpenPype/pull/2541)
- Launcher: Added context menu to to skip opening last workfile [\#2536](https://github.com/pypeclub/OpenPype/pull/2536)
@ -44,34 +70,37 @@
- hotfix: OIIO tool path - add extension on windows [\#2618](https://github.com/pypeclub/OpenPype/pull/2618)
- Settings: Enum does not store empty string if has single item to select [\#2615](https://github.com/pypeclub/OpenPype/pull/2615)
- switch distutils to sysconfig for `get\_platform\(\)` [\#2594](https://github.com/pypeclub/OpenPype/pull/2594)
- Global: fix broken otio review extractor [\#2590](https://github.com/pypeclub/OpenPype/pull/2590)
- Fix poetry index and speedcopy update [\#2589](https://github.com/pypeclub/OpenPype/pull/2589)
- Webpublisher: Fix - subset names from processed .psd used wrong value for task [\#2586](https://github.com/pypeclub/OpenPype/pull/2586)
- `vrscene` creator Deadline webservice URL handling [\#2580](https://github.com/pypeclub/OpenPype/pull/2580)
- global: track name was failing if duplicated root word in name [\#2568](https://github.com/pypeclub/OpenPype/pull/2568)
- Validate Maya Rig produces no cycle errors [\#2484](https://github.com/pypeclub/OpenPype/pull/2484)
**Merged pull requests:**
- Bump pillow from 8.4.0 to 9.0.0 [\#2595](https://github.com/pypeclub/OpenPype/pull/2595)
- Webpublisher: Skip version collect [\#2591](https://github.com/pypeclub/OpenPype/pull/2591)
- build\(deps\): bump pillow from 8.4.0 to 9.0.0 [\#2523](https://github.com/pypeclub/OpenPype/pull/2523)
## [3.8.0](https://github.com/pypeclub/OpenPype/tree/3.8.0) (2022-01-24)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.8.0-nightly.7...3.8.0)
### 📖 Documentation
- Variable in docs renamed to proper name [\#2546](https://github.com/pypeclub/OpenPype/pull/2546)
**🆕 New features**
- Flame: extracting segments with trans-coding [\#2547](https://github.com/pypeclub/OpenPype/pull/2547)
- Maya : V-Ray Proxy - load all ABC files via proxy [\#2544](https://github.com/pypeclub/OpenPype/pull/2544)
- Maya to Unreal: Extended static mesh workflow [\#2537](https://github.com/pypeclub/OpenPype/pull/2537)
- Flame: collecting publishable instances [\#2519](https://github.com/pypeclub/OpenPype/pull/2519)
- Flame: create publishable clips [\#2495](https://github.com/pypeclub/OpenPype/pull/2495)
**🚀 Enhancements**
- Webpublisher: Moved error at the beginning of the log [\#2559](https://github.com/pypeclub/OpenPype/pull/2559)
- Ftrack: Use ApplicationManager to get DJV path [\#2558](https://github.com/pypeclub/OpenPype/pull/2558)
- Webpublisher: Added endpoint to reprocess batch through UI [\#2555](https://github.com/pypeclub/OpenPype/pull/2555)
- Settings: PathInput strip passed string [\#2550](https://github.com/pypeclub/OpenPype/pull/2550)
- Global: Exctract Review anatomy fill data with output name [\#2548](https://github.com/pypeclub/OpenPype/pull/2548)
- Cosmetics: Clean up some cosmetics / typos [\#2542](https://github.com/pypeclub/OpenPype/pull/2542)
@ -79,9 +108,6 @@
- General: Be able to use anatomy data in ffmpeg output arguments [\#2525](https://github.com/pypeclub/OpenPype/pull/2525)
- Expose toggle publish plug-in settings for Maya Look Shading Engine Naming [\#2521](https://github.com/pypeclub/OpenPype/pull/2521)
- Photoshop: Move implementation to OpenPype [\#2510](https://github.com/pypeclub/OpenPype/pull/2510)
- Slack: notifications are sent with Openpype logo and bot name [\#2499](https://github.com/pypeclub/OpenPype/pull/2499)
- Slack: Add review to notification message [\#2498](https://github.com/pypeclub/OpenPype/pull/2498)
- Maya: Collect 'fps' animation data only for "review" instances [\#2486](https://github.com/pypeclub/OpenPype/pull/2486)
**🐛 Bug fixes**
@ -103,10 +129,6 @@
- Maya: reset empty string attributes correctly to "" instead of "None" [\#2506](https://github.com/pypeclub/OpenPype/pull/2506)
- Improve FusionPreLaunch hook errors [\#2505](https://github.com/pypeclub/OpenPype/pull/2505)
### 📖 Documentation
- Variable in docs renamed to proper name [\#2546](https://github.com/pypeclub/OpenPype/pull/2546)
**Merged pull requests:**
- AfterEffects: Move implementation to OpenPype [\#2543](https://github.com/pypeclub/OpenPype/pull/2543)

View file

@ -1,352 +0,0 @@
# version_up_everywhere.py
# Adds action to enable a Clip/Shot to be Min/Max/Next/Prev versioned in all shots used in a Project.
#
# Usage:
# 1) Copy file to <HIERO_PLUGIN_PATH>/Python/Startup
# 2) Right-click on Clip(s) or Bins containing Clips in in the Bin View, or on Shots in the Timeline/Spreadsheet
# 3) Set Version for all Shots > OPTION to update the version in all shots where the Clip is used in the Project.
import hiero.core
try:
from PySide.QtGui import *
from PySide.QtCore import *
except:
from PySide2.QtGui import *
from PySide2.QtWidgets import *
from PySide2.QtCore import *
def whereAmI(self, searchType="TrackItem"):
"""returns a list of TrackItem or Sequnece objects in the Project which contain this Clip.
By default this will return a list of TrackItems where the Clip is used in its project.
You can also return a list of Sequences by specifying the searchType to be "Sequence".
Should consider putting this into hiero.core.Clip by default?
Example usage:
shotsForClip = clip.whereAmI("TrackItem")
sequencesForClip = clip.whereAmI("Sequence")
"""
proj = self.project()
if ("TrackItem" not in searchType) and ("Sequence" not in searchType):
print("searchType argument must be \"TrackItem\" or \"Sequence\"")
return None
# If user specifies a TrackItem, then it will return
searches = hiero.core.findItemsInProject(proj, searchType)
if len(searches) == 0:
print("Unable to find {} in any items of type: {}".format(
str(self), searchType))
return None
# Case 1: Looking for Shots (trackItems)
clipUsedIn = []
if isinstance(searches[0], hiero.core.TrackItem):
for shot in searches:
# We have to wrap this in a try/except because it's possible through the Python API for a Shot to exist without a Clip in the Bin
try:
# For versioning to work, we must look to the BinItem that a Clip is wrapped in.
if shot.source().binItem() == self.binItem():
clipUsedIn.append(shot)
# If we throw an exception here its because the Shot did not have a Source Clip in the Bin.
except RuntimeError:
hiero.core.log.info(
'Unable to find Parent Clip BinItem for Shot: %s, Source:%s'
% (shot, shot.source()))
pass
# Case 1: Looking for Shots (trackItems)
elif isinstance(searches[0], hiero.core.Sequence):
for seq in searches:
# Iterate tracks > shots...
tracks = seq.items()
for track in tracks:
shots = track.items()
for shot in shots:
if shot.source().binItem() == self.binItem():
clipUsedIn.append(seq)
return clipUsedIn
# Add whereAmI method to Clip object
hiero.core.Clip.whereAmI = whereAmI
#### MAIN VERSION EVERYWHERE GUBBINS #####
class VersionAllMenu(object):
# These are a set of action names we can use for operating on multiple Clip/TrackItems
eMaxVersion = "Max Version"
eMinVersion = "Min Version"
eNextVersion = "Next Version"
ePreviousVersion = "Previous Version"
# This is the title used for the Version Menu title. It's long isn't it?
actionTitle = "Set Version for all Shots"
def __init__(self):
self._versionEverywhereMenu = None
self._versionActions = []
hiero.core.events.registerInterest("kShowContextMenu/kBin",
self.binViewEventHandler)
hiero.core.events.registerInterest("kShowContextMenu/kTimeline",
self.binViewEventHandler)
hiero.core.events.registerInterest("kShowContextMenu/kSpreadsheet",
self.binViewEventHandler)
def showVersionUpdateReportFromShotManifest(self, sequenceShotManifest):
"""This just displays an info Message box, based on a Sequence[Shot] manifest dictionary"""
# Now present an info dialog, explaining where shots were updated
updateReportString = "The following Versions were updated:\n"
for seq in sequenceShotManifest.keys():
updateReportString += "%s:\n Shots:\n" % (seq.name())
for shot in sequenceShotManifest[seq]:
updateReportString += ' %s\n (New Version: %s)\n' % (
shot.name(), shot.currentVersion().name())
updateReportString += "\n"
infoBox = QMessageBox(hiero.ui.mainWindow())
infoBox.setIcon(QMessageBox.Information)
if len(sequenceShotManifest) <= 0:
infoBox.setText("No Shot Versions were updated")
infoBox.setInformativeText(
"Clip could not be found in any Shots in this Project")
else:
infoBox.setText(
"Versions were updated in %i Sequences of this Project." %
(len(sequenceShotManifest)))
infoBox.setInformativeText("Show Details for more info.")
infoBox.setDetailedText(updateReportString)
infoBox.exec_()
def makeVersionActionForSingleClip(self, version):
"""This is used to populate the QAction list of Versions when a single Clip is selected in the BinView.
It also triggers the Version Update action based on the version passed to it.
(Not sure if this is good design practice, but it's compact!)"""
action = QAction(version.name(), None)
action.setData(lambda: version)
def updateAllTrackItems():
currentClip = version.item()
trackItems = currentClip.whereAmI()
if not trackItems:
return
proj = currentClip.project()
# A Sequence-Shot manifest dictionary
sequenceShotManifest = {}
# Make this all undo-able in a single Group undo
with proj.beginUndo(
"Update All Versions for %s" % currentClip.name()):
for shot in trackItems:
seq = shot.parentSequence()
if seq not in sequenceShotManifest.keys():
sequenceShotManifest[seq] = [shot]
else:
sequenceShotManifest[seq] += [shot]
shot.setCurrentVersion(version)
# We also should update the current Version of the selected Clip for completeness...
currentClip.binItem().setActiveVersion(version)
# Now disaplay a Dialog which informs the user of where and what was changed
self.showVersionUpdateReportFromShotManifest(sequenceShotManifest)
action.triggered.connect(updateAllTrackItems)
return action
# This is just a convenience method for returning QActions with a title, triggered method and icon.
def makeAction(self, title, method, icon=None):
action = QAction(title, None)
action.setIcon(QIcon(icon))
# We do this magic, so that the title string from the action is used to trigger the version change
def methodWrapper():
method(title)
action.triggered.connect(methodWrapper)
return action
def clipSelectionFromView(self, view):
"""Helper method to return a list of Clips in the Active View"""
selection = hiero.ui.activeView().selection()
if len(selection) == 0:
return None
if isinstance(view, hiero.ui.BinView):
# We could have a mixture of Bins and Clips selected, so sort of the Clips and Clips inside Bins
clipItems = [
item.activeItem() for item in selection
if hasattr(item, "activeItem")
and isinstance(item.activeItem(), hiero.core.Clip)
]
# We'll also append Bins here, and see if can find Clips inside
bins = [
item for item in selection if isinstance(item, hiero.core.Bin)
]
# We search inside of a Bin for a Clip which is not already in clipBinItems
if len(bins) > 0:
# Grab the Clips inside of a Bin and append them to a list
for bin in bins:
clips = hiero.core.findItemsInBin(bin, "Clip")
for clip in clips:
if clip not in clipItems:
clipItems.append(clip)
elif isinstance(view,
(hiero.ui.TimelineEditor, hiero.ui.SpreadsheetView)):
# Here, we have shots. To get to the Clip froma TrackItem, just call source()
clipItems = [
item.source() for item in selection if hasattr(item, "source")
and isinstance(item, hiero.core.TrackItem)
]
return clipItems
# This generates the Version Up Everywhere menu
def createVersionEveryWhereMenuForView(self, view):
versionEverywhereMenu = QMenu(self.actionTitle)
self._versionActions = []
# We look to the activeView for a selection of Clips
clips = self.clipSelectionFromView(view)
# And bail if nothing is found
if len(clips) == 0:
return versionEverywhereMenu
# Now, if we have just one Clip selected, we'll form a special menu, which lists all versions
if len(clips) == 1:
# Get a reversed list of Versions, so that bigger ones appear at top
versions = list(reversed(clips[0].binItem().items()))
for version in versions:
self._versionActions += [
self.makeVersionActionForSingleClip(version)
]
elif len(clips) > 1:
# We will add Max/Min/Prev/Next options, which can be called on a TrackItem, without the need for a Version object
self._versionActions += [
self.makeAction(
self.eMaxVersion,
self.setTrackItemVersionForClipSelection,
icon=None)
]
self._versionActions += [
self.makeAction(
self.eMinVersion,
self.setTrackItemVersionForClipSelection,
icon=None)
]
self._versionActions += [
self.makeAction(
self.eNextVersion,
self.setTrackItemVersionForClipSelection,
icon=None)
]
self._versionActions += [
self.makeAction(
self.ePreviousVersion,
self.setTrackItemVersionForClipSelection,
icon=None)
]
for act in self._versionActions:
versionEverywhereMenu.addAction(act)
return versionEverywhereMenu
def setTrackItemVersionForClipSelection(self, versionOption):
view = hiero.ui.activeView()
if not view:
return
clipSelection = self.clipSelectionFromView(view)
if len(clipSelection) == 0:
return
proj = clipSelection[0].project()
# Create a Sequence-Shot Manifest, to report to users where a Shot was updated
sequenceShotManifest = {}
with proj.beginUndo("Update multiple Versions"):
for clip in clipSelection:
# Look to see if it exists in a TrackItem somewhere...
shotUsage = clip.whereAmI("TrackItem")
# Next, depending on the versionOption, make the appropriate update
# There's probably a more neat/compact way of doing this...
for shot in shotUsage:
# This step is done for reporting reasons
seq = shot.parentSequence()
if seq not in sequenceShotManifest.keys():
sequenceShotManifest[seq] = [shot]
else:
sequenceShotManifest[seq] += [shot]
if versionOption == self.eMaxVersion:
shot.maxVersion()
elif versionOption == self.eMinVersion:
shot.minVersion()
elif versionOption == self.eNextVersion:
shot.nextVersion()
elif versionOption == self.ePreviousVersion:
shot.prevVersion()
# Finally, for completeness, set the Max/Min version of the Clip too (if chosen)
# Note: It doesn't make sense to do Next/Prev on a Clip here because next/prev means different things for different Shots
if versionOption == self.eMaxVersion:
clip.binItem().maxVersion()
elif versionOption == self.eMinVersion:
clip.binItem().minVersion()
# Now disaplay a Dialog which informs the user of where and what was changed
self.showVersionUpdateReportFromShotManifest(sequenceShotManifest)
# This handles events from the Project Bin View
def binViewEventHandler(self, event):
if not hasattr(event.sender, "selection"):
# Something has gone wrong, we should only be here if raised
# by the Bin view which gives a selection.
return
selection = event.sender.selection()
# Return if there's no Selection. We won't add the Localise Menu.
if selection == None:
return
view = hiero.ui.activeView()
# Only add the Menu if Bins or Sequences are selected (this ensures menu isn't added in the Tags Pane)
if len(selection) > 0:
self._versionEverywhereMenu = self.createVersionEveryWhereMenuForView(
view)
hiero.ui.insertMenuAction(
self._versionEverywhereMenu.menuAction(),
event.menu,
after="foundry.menu.version")
return
# Instantiate the Menu to get it to register itself.
VersionAllMenu = VersionAllMenu()

View file

@ -2,11 +2,11 @@ import re
import pyblish.api
class PreCollectClipEffects(pyblish.api.InstancePlugin):
class CollectClipEffects(pyblish.api.InstancePlugin):
"""Collect soft effects instances."""
order = pyblish.api.CollectorOrder - 0.479
label = "Precollect Clip Effects Instances"
order = pyblish.api.CollectorOrder - 0.078
label = "Collect Clip Effects Instances"
families = ["clip"]
def process(self, instance):

View file

@ -2440,34 +2440,27 @@ def validate_fps():
# rounding, we have to round those numbers coming from Maya.
current_fps = float_round(mel.eval('currentTimeUnitToFPS()'), 2)
if current_fps != fps:
fps_match = current_fps == fps
if not fps_match and not IS_HEADLESS:
from openpype.widgets import popup
from Qt import QtWidgets
from ...widgets import popup
parent = get_main_window()
# Find maya main window
top_level_widgets = {w.objectName(): w for w in
QtWidgets.QApplication.topLevelWidgets()}
dialog = popup.Popup2(parent=parent)
dialog.setModal(True)
dialog.setWindowTitle("Maya scene not in line with project")
dialog.setMessage("The FPS is out of sync, please fix")
parent = top_level_widgets.get("MayaWindow", None)
if parent is None:
pass
else:
dialog = popup.Popup2(parent=parent)
dialog.setModal(True)
dialog.setWindowTitle("Maya scene not in line with project")
dialog.setMessage("The FPS is out of sync, please fix")
# Set new text for button (add optional argument for the popup?)
toggle = dialog.widgets["toggle"]
update = toggle.isChecked()
dialog.on_show.connect(lambda: set_scene_fps(fps, update))
# Set new text for button (add optional argument for the popup?)
toggle = dialog.widgets["toggle"]
update = toggle.isChecked()
dialog.on_show.connect(lambda: set_scene_fps(fps, update))
dialog.show()
dialog.show()
return False
return False
return True
return fps_match
def bake(nodes,

View file

@ -911,7 +911,7 @@ class RenderProductsRedshift(ARenderProducts):
"""
prefix = super(RenderProductsRedshift, self).get_renderer_prefix()
prefix = "{}.<aov>".format(prefix)
prefix = "{}{}<aov>".format(prefix, self.aov_separator)
return prefix
def get_render_products(self):

View file

@ -108,17 +108,17 @@ def install():
cmds.menuItem(
"Reset Frame Range",
command=reset_frame_range
command=lambda *args: reset_frame_range()
)
cmds.menuItem(
"Reset Resolution",
command=lib.reset_scene_resolution
command=lambda *args: lib.reset_scene_resolution()
)
cmds.menuItem(
"Set Colorspace",
command=lib.set_colorspace,
command=lambda *args: lib.set_colorspace(),
)
cmds.menuItem(divider=True, parent=MENU_NAME)
cmds.menuItem(

View file

@ -86,7 +86,7 @@ class CreateRender(plugin.Creator):
'vray': 'maya/<scene>/<Layer>/<Layer>',
'arnold': 'maya/<Scene>/<RenderLayer>/<RenderLayer>{aov_separator}<RenderPass>', # noqa
'renderman': 'maya/<Scene>/<layer>/<layer>{aov_separator}<aov>',
'redshift': 'maya/<Scene>/<RenderLayer>/<RenderLayer>{aov_separator}<RenderPass>' # noqa
'redshift': 'maya/<Scene>/<RenderLayer>/<RenderLayer>' # noqa
}
_aov_chars = {
@ -455,9 +455,7 @@ class CreateRender(plugin.Creator):
if renderer == "vray":
self._set_vray_settings(asset)
if renderer == "redshift":
_ = self._set_renderer_option(
"RedshiftOptions", "{}.imageFormat", 1
)
cmds.setAttr("redshiftOptions.imageFormat", 1)
# resolution
cmds.setAttr(

View file

@ -5,15 +5,16 @@ from maya import cmds
class ValidateFrameRange(pyblish.api.InstancePlugin):
"""Valides the frame ranges.
"""Validates the frame ranges.
This is optional validator checking if the frame range on instance
matches the one of asset. It also validate render frame range of render
layers
This is an optional validator checking if the frame range on instance
matches the frame range specified for the asset.
Repair action will change everything to match asset.
It also validates render frame ranges of render layers.
This can be turned off by artist to allow custom ranges.
Repair action will change everything to match the asset frame range.
This can be turned off by the artist to allow custom ranges.
"""
label = "Validate Frame Range"

View file

@ -172,8 +172,8 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
cls.log.error(("AOV ({}) image prefix is not set "
"correctly {} != {}").format(
cmds.getAttr("{}.name".format(aov)),
cmds.getAttr("{}.filePrefix".format(aov)),
aov_prefix
aov_prefix,
redshift_AOV_prefix
))
invalid = True
# get aov format
@ -329,7 +329,8 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
for aov in rs_aovs:
# fix AOV prefixes
cmds.setAttr(
"{}.filePrefix".format(aov), redshift_AOV_prefix)
"{}.filePrefix".format(aov),
redshift_AOV_prefix, type="string")
# fix AOV file format
default_ext = cmds.getAttr(
"redshiftOptions.imageFormat", asString=True)

View file

@ -59,6 +59,7 @@ class CollectBatchData(pyblish.api.ContextPlugin):
context.data["asset"] = asset_name
context.data["task"] = task_name
context.data["taskType"] = task_type
context.data["project_name"] = project_name
self._set_ctx_path(batch_data)

View file

@ -13,8 +13,10 @@ import tempfile
from avalon import io
import pyblish.api
from openpype.lib import prepare_template_data
from openpype.lib.plugin_tools import parse_json
from openpype.lib.plugin_tools import (
parse_json,
get_subset_name_with_asset_doc
)
class CollectPublishedFiles(pyblish.api.ContextPlugin):
"""
@ -34,7 +36,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
targets = ["filespublish"]
# from Settings
task_type_to_family = {}
task_type_to_family = []
def process(self, context):
batch_dir = context.data["batchDir"]
@ -47,8 +49,13 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
self.log.info("task_sub:: {}".format(task_subfolders))
asset_name = context.data["asset"]
asset_doc = io.find_one({
"type": "asset",
"name": asset_name
})
task_name = context.data["task"]
task_type = context.data["taskType"]
project_name = context.data["project_name"]
for task_dir in task_subfolders:
task_data = parse_json(os.path.join(task_dir,
"manifest.json"))
@ -57,20 +64,21 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
is_sequence = len(task_data["files"]) > 1
_, extension = os.path.splitext(task_data["files"][0])
family, families, subset_template, tags = self._get_family(
family, families, tags = self._get_family(
self.task_type_to_family,
task_type,
is_sequence,
extension.replace(".", ''))
subset = self._get_subset_name(
family, subset_template, task_name, task_data["variant"]
subset_name = get_subset_name_with_asset_doc(
family, task_data["variant"], task_name, asset_doc,
project_name=project_name, host_name="webpublisher"
)
version = self._get_last_version(asset_name, subset) + 1
version = self._get_last_version(asset_name, subset_name) + 1
instance = context.create_instance(subset)
instance = context.create_instance(subset_name)
instance.data["asset"] = asset_name
instance.data["subset"] = subset
instance.data["subset"] = subset_name
instance.data["family"] = family
instance.data["families"] = families
instance.data["version"] = version
@ -149,7 +157,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
extension (str): without '.'
Returns:
(family, [families], subset_template_name, tags) tuple
(family, [families], tags) tuple
AssertionError if not matching family found
"""
task_type = task_type.lower()
@ -160,12 +168,21 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
assert task_obj, "No family configuration for '{}'".format(task_type)
found_family = None
for family, content in task_obj.items():
if is_sequence != content["is_sequence"]:
families_config = []
# backward compatibility, should be removed pretty soon
if isinstance(task_obj, dict):
for family, config in task_obj:
config["result_family"] = family
families_config.append(config)
else:
families_config = task_obj
for config in families_config:
if is_sequence != config["is_sequence"]:
continue
if extension in content["extensions"] or \
'' in content["extensions"]: # all extensions setting
found_family = family
if (extension in config["extensions"] or
'' in config["extensions"]): # all extensions setting
found_family = config["result_family"]
break
msg = "No family found for combination of " +\
@ -173,10 +190,9 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
task_type, is_sequence, extension)
assert found_family, msg
return found_family, \
content["families"], \
content["subset_template_name"], \
content["tags"]
return (found_family,
config["families"],
config["tags"])
def _get_last_version(self, asset_name, subset_name):
"""Returns version number or 0 for 'asset' and 'subset'"""

View file

@ -359,12 +359,19 @@ class ConfiguredExtensionsEndpoint(_RestApiEndpoint):
"studio_exts": set(["psd", "psb", "tvpp", "tvp"])
}
collect_conf = sett["webpublisher"]["publish"]["CollectPublishedFiles"]
for _, mapping in collect_conf.get("task_type_to_family", {}).items():
for _family, config in mapping.items():
if config["is_sequence"]:
configured["sequence_exts"].update(config["extensions"])
else:
configured["file_exts"].update(config["extensions"])
configs = collect_conf.get("task_type_to_family", [])
mappings = []
for _, conf_mappings in configs.items():
if isinstance(conf_mappings, dict):
conf_mappings = conf_mappings.values()
for conf_mapping in conf_mappings:
mappings.append(conf_mapping)
for mapping in mappings:
if mapping["is_sequence"]:
configured["sequence_exts"].update(mapping["extensions"])
else:
configured["file_exts"].update(mapping["extensions"])
return Response(
status=200,

View file

@ -84,6 +84,7 @@ from .avalon_context import (
get_hierarchy,
get_linked_assets,
get_latest_version,
get_system_general_anatomy_data,
get_workfile_template_key,
get_workfile_template_key_from_context,
@ -222,6 +223,7 @@ __all__ = [
"get_hierarchy",
"get_linked_assets",
"get_latest_version",
"get_system_general_anatomy_data",
"get_workfile_template_key",
"get_workfile_template_key_from_context",

View file

@ -14,8 +14,7 @@ import six
from openpype.settings import (
get_system_settings,
get_project_settings,
get_environments
get_project_settings
)
from openpype.settings.constants import (
METADATA_KEYS,
@ -29,8 +28,7 @@ from .profiles_filtering import filter_profiles
from .local_settings import get_openpype_username
from .avalon_context import (
get_workdir_data,
get_workdir_with_workdir_data,
get_workfile_template_key
get_workdir_with_workdir_data
)
from .python_module_tools import (
@ -44,6 +42,9 @@ _logger = None
PLATFORM_NAMES = {"windows", "linux", "darwin"}
DEFAULT_ENV_SUBGROUP = "standard"
CUSTOM_LAUNCH_APP_GROUPS = {
"djvview"
}
def parse_environments(env_data, env_group=None, platform_name=None):
@ -405,11 +406,47 @@ class ApplicationManager:
clear_metadata=False, exclude_locals=False
)
all_app_defs = {}
# Prepare known applications
app_defs = settings["applications"]
additional_apps = {}
for group_name, variant_defs in app_defs.items():
if group_name in METADATA_KEYS:
continue
if group_name == "additional_apps":
additional_apps = variant_defs
else:
all_app_defs[group_name] = variant_defs
# Prepare additional applications
# - First find dynamic keys that can be used as labels of group
dynamic_keys = {}
for group_name, variant_defs in additional_apps.items():
if group_name == M_DYNAMIC_KEY_LABEL:
dynamic_keys = variant_defs
break
# Add additional apps to known applications
for group_name, variant_defs in additional_apps.items():
if group_name in METADATA_KEYS:
continue
# Determine group label
label = variant_defs.get("label")
if not label:
# Look for label set in dynamic labels
label = dynamic_keys.get(group_name)
if not label:
label = group_name
variant_defs["label"] = label
all_app_defs[group_name] = variant_defs
for group_name, variant_defs in all_app_defs.items():
if group_name in METADATA_KEYS:
continue
group = ApplicationGroup(group_name, variant_defs, self)
self.app_groups[group_name] = group
for app in group:
@ -892,7 +929,9 @@ class ApplicationLaunchContext:
# --- START: Backwards compatibility ---
hooks_dir = os.path.join(pype_dir, "hooks")
subfolder_names = ["global", self.host_name]
subfolder_names = ["global"]
if self.host_name:
subfolder_names.append(self.host_name)
for subfolder_name in subfolder_names:
path = os.path.join(hooks_dir, subfolder_name)
if (
@ -903,10 +942,12 @@ class ApplicationLaunchContext:
paths.append(path)
# --- END: Backwards compatibility ---
subfolders_list = (
["hooks"],
("hosts", self.host_name, "hooks")
)
subfolders_list = [
["hooks"]
]
if self.host_name:
subfolders_list.append(["hosts", self.host_name, "hooks"])
for subfolders in subfolders_list:
path = os.path.join(pype_dir, *subfolders)
if (

View file

@ -9,7 +9,10 @@ import collections
import functools
import getpass
from openpype.settings import get_project_settings
from openpype.settings import (
get_project_settings,
get_system_settings
)
from .anatomy import Anatomy
from .profiles_filtering import filter_profiles
@ -258,6 +261,18 @@ def get_hierarchy(asset_name=None):
return "/".join(hierarchy_items)
def get_system_general_anatomy_data():
system_settings = get_system_settings()
studio_name = system_settings["general"]["studio_name"]
studio_code = system_settings["general"]["studio_code"]
return {
"studio": {
"name": studio_name,
"code": studio_code
}
}
def get_linked_asset_ids(asset_doc):
"""Return linked asset ids for `asset_doc` from DB
@ -536,6 +551,10 @@ def get_workdir_data(project_doc, asset_doc, task_name, host_name):
"user": getpass.getuser(),
"hierarchy": hierarchy,
}
system_general_data = get_system_general_anatomy_data()
data.update(system_general_data)
return data
@ -1505,7 +1524,7 @@ def _get_task_context_data_for_anatomy(
"requested task type: `{}`".format(task_type)
)
return {
data = {
"project": {
"name": project_doc["name"],
"code": project_doc["data"].get("code")
@ -1518,6 +1537,11 @@ def _get_task_context_data_for_anatomy(
}
}
system_general_data = get_system_general_anatomy_data()
data.update(system_general_data)
return data
def get_custom_workfile_template_by_context(
template_profiles, project_doc, asset_doc, task_name, anatomy=None

View file

@ -4,12 +4,35 @@ import logging
import collections
import tempfile
import xml.etree.ElementTree
from .execute import run_subprocess
from .vendor_bin_utils import (
get_oiio_tools_path,
is_oiio_supported
)
# Max length of string that is supported by ffmpeg
MAX_FFMPEG_STRING_LEN = 8196
# OIIO known xml tags
STRING_TAGS = {
"format"
}
INT_TAGS = {
"x", "y", "z",
"width", "height", "depth",
"full_x", "full_y", "full_z",
"full_width", "full_height", "full_depth",
"tile_width", "tile_height", "tile_depth",
"nchannels",
"alpha_channel",
"z_channel",
"deep",
"subimages",
}
# Regex to parse array attributes
ARRAY_TYPE_REGEX = re.compile(r"^(int|float|string)\[\d+\]$")
def get_transcode_temp_directory():
"""Creates temporary folder for transcoding.
@ -24,87 +47,215 @@ def get_transcode_temp_directory():
def get_oiio_info_for_input(filepath, logger=None):
"""Call oiiotool to get information about input and return stdout."""
args = [
get_oiio_tools_path(), "--info", "-v", filepath
]
return run_subprocess(args, logger=logger)
"""Call oiiotool to get information about input and return stdout.
def parse_oiio_info(oiio_info):
"""Create an object based on output from oiiotool.
Removes quotation marks from compression value. Parse channels into
dictionary - key is channel name value is determined type of channel
(e.g. 'uint', 'float').
Args:
oiio_info (str): Output of calling "oiiotool --info -v <path>"
Returns:
dict: Loaded data from output.
Stdout should contain xml format string.
"""
lines = [
line.strip()
for line in oiio_info.split("\n")
args = [
get_oiio_tools_path(), "--info", "-v", "-i:infoformat=xml", filepath
]
# Each line should contain information about one key
# key - value are separated with ": "
oiio_sep = ": "
data_map = {}
for line in lines:
parts = line.split(oiio_sep)
if len(parts) < 2:
output = run_subprocess(args, logger=logger)
output = output.replace("\r\n", "\n")
xml_started = False
lines = []
for line in output.split("\n"):
if not xml_started:
if not line.startswith("<"):
continue
xml_started = True
if xml_started:
lines.append(line)
if not xml_started:
raise ValueError(
"Failed to read input file \"{}\".\nOutput:\n{}".format(
filepath, output
)
)
xml_text = "\n".join(lines)
return parse_oiio_xml_output(xml_text, logger=logger)
class RationalToInt:
"""Rational value stored as division of 2 integers using string."""
def __init__(self, string_value):
parts = string_value.split("/")
top = float(parts[0])
bottom = 1.0
if len(parts) != 1:
bottom = float(parts[1])
self._value = top / bottom
self._string_value = string_value
@property
def value(self):
return self._value
@property
def string_value(self):
return self._string_value
def __format__(self, *args, **kwargs):
return self._string_value.__format__(*args, **kwargs)
def __float__(self):
return self._value
def __str__(self):
return self._string_value
def __repr__(self):
return "<{}> {}".format(self.__class__.__name__, self._string_value)
def convert_value_by_type_name(value_type, value, logger=None):
"""Convert value to proper type based on type name.
In some cases value types have custom python class.
"""
if logger is None:
logger = logging.getLogger(__name__)
# Simple types
if value_type == "string":
return value
if value_type == "int":
return int(value)
if value_type == "float":
return float(value)
# Vectors will probably have more types
if value_type == "vec2f":
return [float(item) for item in value.split(",")]
# Matrix should be always have square size of element 3x3, 4x4
# - are returned as list of lists
if value_type == "matrix":
output = []
current_index = -1
parts = value.split(",")
parts_len = len(parts)
if parts_len == 1:
divisor = 1
elif parts_len == 4:
divisor = 2
elif parts_len == 9:
divisor == 3
elif parts_len == 16:
divisor = 4
else:
logger.info("Unknown matrix resolution {}. Value: \"{}\"".format(
parts_len, value
))
for part in parts:
output.append(float(part))
return output
for idx, item in enumerate(parts):
list_index = idx % divisor
if list_index > current_index:
current_index = list_index
output.append([])
output[list_index].append(float(item))
return output
if value_type == "rational2i":
return RationalToInt(value)
# Array of other types is converted to list
re_result = ARRAY_TYPE_REGEX.findall(value_type)
if re_result:
array_type = re_result[0]
output = []
for item in value.split(","):
output.append(
convert_value_by_type_name(array_type, item, logger=logger)
)
return output
logger.info((
"MISSING IMPLEMENTATION:"
" Unknown attrib type \"{}\". Value: {}"
).format(value_type, value))
return value
def parse_oiio_xml_output(xml_string, logger=None):
"""Parse xml output from OIIO info command."""
output = {}
if not xml_string:
return output
if logger is None:
logger = logging.getLogger("OIIO-xml-parse")
tree = xml.etree.ElementTree.fromstring(xml_string)
attribs = {}
output["attribs"] = attribs
for child in tree:
tag_name = child.tag
if tag_name == "attrib":
attrib_def = child.attrib
value = convert_value_by_type_name(
attrib_def["type"], child.text, logger=logger
)
attribs[attrib_def["name"]] = value
continue
key = parts.pop(0)
value = oiio_sep.join(parts)
data_map[key] = value
if "compression" in data_map:
value = data_map["compression"]
data_map["compression"] = value.replace("\"", "")
# Channels are stored as tex on each child
if tag_name == "channelnames":
value = []
for channel in child:
value.append(channel.text)
channels_info = {}
channels_value = data_map.get("channel list") or ""
if channels_value:
channels = channels_value.split(", ")
type_regex = re.compile(r"(?P<name>[^\(]+) \((?P<type>[^\)]+)\)")
for channel in channels:
match = type_regex.search(channel)
if not match:
channel_name = channel
channel_type = "uint"
else:
channel_name = match.group("name")
channel_type = match.group("type")
channels_info[channel_name] = channel_type
data_map["channels_info"] = channels_info
return data_map
# Convert known integer type tags to int
elif tag_name in INT_TAGS:
value = int(child.text)
# Keep value of known string tags
elif tag_name in STRING_TAGS:
value = child.text
# Keep value as text for unknown tags
# - feel free to add more tags
else:
value = child.text
logger.info((
"MISSING IMPLEMENTATION:"
" Unknown tag \"{}\". Value \"{}\""
).format(tag_name, value))
output[child.tag] = value
return output
def get_convert_rgb_channels(channels_info):
def get_convert_rgb_channels(channel_names):
"""Get first available RGB(A) group from channels info.
## Examples
```
# Ideal situation
channels_info: {
"R": ...,
"G": ...,
"B": ...,
"A": ...
channels_info: [
"R", "G", "B", "A"
}
```
Result will be `("R", "G", "B", "A")`
```
# Not ideal situation
channels_info: {
"beauty.red": ...,
"beuaty.green": ...,
"beauty.blue": ...,
"depth.Z": ...
}
channels_info: [
"beauty.red",
"beuaty.green",
"beauty.blue",
"depth.Z"
]
```
Result will be `("beauty.red", "beauty.green", "beauty.blue", None)`
@ -116,7 +267,7 @@ def get_convert_rgb_channels(channels_info):
"""
rgb_by_main_name = collections.defaultdict(dict)
main_name_order = [""]
for channel_name in channels_info.keys():
for channel_name in channel_names:
name_parts = channel_name.split(".")
rgb_part = name_parts.pop(-1).lower()
main_name = ".".join(name_parts)
@ -166,28 +317,35 @@ def should_convert_for_ffmpeg(src_filepath):
return None
# Load info about info from oiio tool
oiio_info = get_oiio_info_for_input(src_filepath)
input_info = parse_oiio_info(oiio_info)
input_info = get_oiio_info_for_input(src_filepath)
if not input_info:
return None
# Check compression
compression = input_info["compression"]
compression = input_info["attribs"].get("compression")
if compression in ("dwaa", "dwab"):
return True
# Check channels
channels_info = input_info["channels_info"]
review_channels = get_convert_rgb_channels(channels_info)
channel_names = input_info["channelnames"]
review_channels = get_convert_rgb_channels(channel_names)
if review_channels is None:
return None
for attr_value in input_info["attribs"].values():
if (
isinstance(attr_value, str)
and len(attr_value) > MAX_FFMPEG_STRING_LEN
):
return True
return False
def convert_for_ffmpeg(
first_input_path,
output_dir,
input_frame_start,
input_frame_end,
input_frame_start=None,
input_frame_end=None,
logger=None
):
"""Contert source file to format supported in ffmpeg.
@ -221,46 +379,76 @@ def convert_for_ffmpeg(
if input_frame_start is not None and input_frame_end is not None:
is_sequence = int(input_frame_end) != int(input_frame_start)
oiio_info = get_oiio_info_for_input(first_input_path)
input_info = parse_oiio_info(oiio_info)
input_info = get_oiio_info_for_input(first_input_path)
# Change compression only if source compression is "dwaa" or "dwab"
# - they're not supported in ffmpeg
compression = input_info["compression"]
compression = input_info["attribs"].get("compression")
if compression in ("dwaa", "dwab"):
compression = "none"
# Prepare subprocess arguments
oiio_cmd = [
get_oiio_tools_path(),
"--compression", compression,
first_input_path
]
oiio_cmd = [get_oiio_tools_path()]
# Add input compression if available
if compression:
oiio_cmd.extend(["--compression", compression])
channels_info = input_info["channels_info"]
review_channels = get_convert_rgb_channels(channels_info)
# Collect channels to export
channel_names = input_info["channelnames"]
review_channels = get_convert_rgb_channels(channel_names)
if review_channels is None:
raise ValueError(
"Couldn't find channels that can be used for conversion."
)
red, green, blue, alpha = review_channels
input_channels = [red, green, blue]
channels_arg = "R={},G={},B={}".format(red, green, blue)
if alpha is not None:
channels_arg += ",A={}".format(alpha)
oiio_cmd.append("--ch")
oiio_cmd.append(channels_arg)
input_channels.append(alpha)
input_channels_str = ",".join(input_channels)
oiio_cmd.extend([
# Tell oiiotool which channels should be loaded
# - other channels are not loaded to memory so helps to avoid memory
# leak issues
"-i:ch={}".format(input_channels_str), first_input_path,
# Tell oiiotool which channels should be put to top stack (and output)
"--ch", channels_arg
])
# Add frame definitions to arguments
if is_sequence:
oiio_cmd.append("--frames")
oiio_cmd.append("{}-{}".format(input_frame_start, input_frame_end))
oiio_cmd.extend([
"--frames", "{}-{}".format(input_frame_start, input_frame_end)
])
ignore_attr_changes_added = False
for attr_name, attr_value in input_info["attribs"].items():
if not isinstance(attr_value, str):
continue
# Remove attributes that have string value longer than allowed length
# for ffmpeg
if len(attr_value) > MAX_FFMPEG_STRING_LEN:
if not ignore_attr_changes_added:
# Attrite changes won't be added to attributes itself
ignore_attr_changes_added = True
oiio_cmd.append("--sansattrib")
# Set attribute to empty string
logger.info((
"Removed attribute \"{}\" from metadata"
" because has too long value ({} chars)."
).format(attr_name, len(attr_value)))
oiio_cmd.extend(["--eraseattrib", attr_name])
# Add last argument - path to output
base_file_name = os.path.basename(first_input_path)
output_path = os.path.join(output_dir, base_file_name)
oiio_cmd.append("-o")
oiio_cmd.append(output_path)
oiio_cmd.extend([
"-o", output_path
])
logger.debug("Conversion command: {}".format(" ".join(oiio_cmd)))
run_subprocess(oiio_cmd, logger=logger)

View file

@ -237,7 +237,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
'publish',
roothless_metadata_path,
"--targets", "deadline",
"--targets", "filesequence"
"--targets", "farm"
]
# Generate the payload for Deadline submission

View file

@ -2,7 +2,10 @@ import sys
import json
import collections
import ftrack_api
from openpype_modules.ftrack.lib import ServerAction
from openpype_modules.ftrack.lib import (
ServerAction,
query_custom_attributes
)
class PushHierValuesToNonHier(ServerAction):
@ -51,10 +54,6 @@ class PushHierValuesToNonHier(ServerAction):
" from CustomAttributeConfiguration"
" where key in ({})"
)
cust_attr_value_query = (
"select value, entity_id from CustomAttributeValue"
" where entity_id in ({}) and configuration_id in ({})"
)
# configurable
settings_key = "sync_hier_entity_attributes"
@ -344,25 +343,11 @@ class PushHierValuesToNonHier(ServerAction):
all_ids_with_parents.add(parent_id)
_entity_id = parent_id
joined_entity_ids = self.join_query_keys(all_ids_with_parents)
hier_attr_ids = self.join_query_keys(
tuple(hier_attr["id"] for hier_attr in hier_attrs)
)
hier_attr_ids = tuple(hier_attr["id"] for hier_attr in hier_attrs)
hier_attrs_key_by_id = {
hier_attr["id"]: hier_attr["key"]
for hier_attr in hier_attrs
}
call_expr = [{
"action": "query",
"expression": self.cust_attr_value_query.format(
joined_entity_ids, hier_attr_ids
)
}]
if hasattr(session, "call"):
[values] = session.call(call_expr)
else:
[values] = session._call(call_expr)
values_per_entity_id = {}
for entity_id in all_ids_with_parents:
@ -370,7 +355,10 @@ class PushHierValuesToNonHier(ServerAction):
for key in hier_attrs_key_by_id.values():
values_per_entity_id[entity_id][key] = None
for item in values["data"]:
values = query_custom_attributes(
session, all_ids_with_parents, hier_attr_ids, True
)
for item in values:
entity_id = item["entity_id"]
key = hier_attrs_key_by_id[item["configuration_id"]]

View file

@ -17,11 +17,6 @@ class PushFrameValuesToTaskEvent(BaseEvent):
" (object_type_id in ({}) or is_hierarchical is true)"
)
cust_attr_query = (
"select value, entity_id from ContextCustomAttributeValue "
"where entity_id in ({}) and configuration_id in ({})"
)
_cached_task_object_id = None
_cached_interest_object_ids = None
_cached_user_id = None
@ -273,16 +268,23 @@ class PushFrameValuesToTaskEvent(BaseEvent):
hier_attr_ids.append(attr_id)
conf_ids = list(hier_attr_ids)
task_conf_ids = []
for key, attr_id in task_attrs.items():
attr_key_by_id[attr_id] = key
nonhier_id_by_key[key] = attr_id
conf_ids.append(attr_id)
task_conf_ids.append(attr_id)
# Query custom attribute values
# - result does not contain values for all entities only result of
# query callback to ftrack server
result = query_custom_attributes(
session, conf_ids, whole_hierarchy_ids
session, list(hier_attr_ids), whole_hierarchy_ids, True
)
result.extend(
query_custom_attributes(
session, task_conf_ids, whole_hierarchy_ids, False
)
)
# Prepare variables where result will be stored
@ -547,7 +549,7 @@ class PushFrameValuesToTaskEvent(BaseEvent):
)
attr_ids = set(attr_id_to_key.keys())
current_values_by_id = self.current_values(
current_values_by_id = self.get_current_values(
session, attr_ids, entity_ids, task_entity_ids, hier_attrs
)
@ -642,27 +644,17 @@ class PushFrameValuesToTaskEvent(BaseEvent):
return interesting_data, changed_keys_by_object_id
def current_values(
def get_current_values(
self, session, attr_ids, entity_ids, task_entity_ids, hier_attrs
):
current_values_by_id = {}
if not attr_ids or not entity_ids:
return current_values_by_id
joined_conf_ids = self.join_query_keys(attr_ids)
joined_entity_ids = self.join_query_keys(entity_ids)
call_expr = [{
"action": "query",
"expression": self.cust_attr_query.format(
joined_entity_ids, joined_conf_ids
)
}]
if hasattr(session, "call"):
[values] = session.call(call_expr)
else:
[values] = session._call(call_expr)
for item in values["data"]:
values = query_custom_attributes(
session, attr_ids, entity_ids, True
)
for item in values:
entity_id = item["entity_id"]
attr_id = item["configuration_id"]
if entity_id in task_entity_ids and attr_id in hier_attrs:

View file

@ -128,7 +128,7 @@ class SyncLinksToAvalon(BaseEvent):
def _get_mongo_ids_by_ftrack_ids(self, session, attr_id, ftrack_ids):
output = query_custom_attributes(
session, [attr_id], ftrack_ids
session, [attr_id], ftrack_ids, True
)
mongo_id_by_ftrack_id = {}
for item in output:

View file

@ -17,6 +17,7 @@ from avalon.api import AvalonMongoDB
from openpype_modules.ftrack.lib import (
get_openpype_attr,
query_custom_attributes,
CUST_ATTR_ID_KEY,
CUST_ATTR_AUTO_SYNC,
@ -48,8 +49,8 @@ class SyncToAvalonEvent(BaseEvent):
)
entities_query_by_id = (
"select id, name, parent_id, link, custom_attributes from TypedContext"
" where project_id is \"{}\" and id in ({})"
"select id, name, parent_id, link, custom_attributes, description"
" from TypedContext where project_id is \"{}\" and id in ({})"
)
# useful for getting all tasks for asset
@ -1073,9 +1074,8 @@ class SyncToAvalonEvent(BaseEvent):
self.create_entity_in_avalon(entity, parent_avalon_ent)
for child in entity["children"]:
if child.entity_type.lower() == "task":
continue
children_queue.append(child)
if child.entity_type.lower() != "task":
children_queue.append(child)
while children_queue:
entity = children_queue.popleft()
@ -1145,7 +1145,8 @@ class SyncToAvalonEvent(BaseEvent):
"entityType": ftrack_ent.entity_type,
"parents": parents,
"tasks": {},
"visualParent": vis_par
"visualParent": vis_par,
"description": ftrack_ent["description"]
}
}
cust_attrs = self.get_cust_attr_values(ftrack_ent)
@ -1822,7 +1823,15 @@ class SyncToAvalonEvent(BaseEvent):
if ent_cust_attrs is None:
ent_cust_attrs = {}
for key, values in ent_info["changes"].items():
ent_changes = ent_info["changes"]
if "description" in ent_changes:
if "data" not in self.updates[mongo_id]:
self.updates[mongo_id]["data"] = {}
self.updates[mongo_id]["data"]["description"] = (
ent_changes["description"]["new"] or ""
)
for key, values in ent_changes.items():
if key in hier_attrs_by_key:
self.hier_cust_attrs_changes[key].append(ftrack_id)
continue
@ -2122,22 +2131,12 @@ class SyncToAvalonEvent(BaseEvent):
for key in hier_cust_attrs_keys:
configuration_ids.add(hier_attr_id_by_key[key])
entity_ids_joined = self.join_query_keys(cust_attrs_ftrack_ids)
attributes_joined = self.join_query_keys(configuration_ids)
queries = [{
"action": "query",
"expression": (
"select value, entity_id, configuration_id"
" from CustomAttributeValue "
"where entity_id in ({}) and configuration_id in ({})"
).format(entity_ids_joined, attributes_joined)
}]
if hasattr(self.process_session, "call"):
[values] = self.process_session.call(queries)
else:
[values] = self.process_session._call(queries)
values = query_custom_attributes(
self.process_session,
configuration_ids,
cust_attrs_ftrack_ids,
True
)
ftrack_project_id = self.cur_project["id"]
@ -2162,7 +2161,7 @@ class SyncToAvalonEvent(BaseEvent):
# PREPARE DATA BEFORE THIS
avalon_hier = []
for item in values["data"]:
for item in values:
value = item["value"]
if value is None:
continue

View file

@ -2,10 +2,11 @@ import os
from uuid import uuid4
from openpype_modules.ftrack.lib import BaseAction
from openpype.lib import (
from openpype.lib.applications import (
ApplicationManager,
ApplicationLaunchFailed,
ApplictionExecutableNotFound
ApplictionExecutableNotFound,
CUSTOM_LAUNCH_APP_GROUPS
)
from avalon.api import AvalonMongoDB
@ -136,6 +137,9 @@ class AppplicationsAction(BaseAction):
if not app or not app.enabled:
continue
if app.group.name in CUSTOM_LAUNCH_APP_GROUPS:
continue
app_icon = app.icon
if app_icon and self.icon_url:
try:

View file

@ -19,8 +19,8 @@ class CleanHierarchicalAttrsAction(BaseAction):
" from TypedContext where project_id is \"{}\""
)
cust_attr_query = (
"select value, entity_id from CustomAttributeValue "
"where entity_id in ({}) and configuration_id is \"{}\""
"select value, entity_id from CustomAttributeValue"
" where entity_id in ({}) and configuration_id is \"{}\""
)
settings_key = "clean_hierarchical_attr"
@ -65,17 +65,14 @@ class CleanHierarchicalAttrsAction(BaseAction):
)
)
configuration_id = attr["id"]
call_expr = [{
"action": "query",
"expression": self.cust_attr_query.format(
values = session.query(
self.cust_attr_query.format(
entity_ids_joined, configuration_id
)
}]
[values] = self.session.call(call_expr)
).all()
data = {}
for item in values["data"]:
for item in values:
value = item["value"]
if value is None:
data[item["entity_id"]] = value
@ -90,10 +87,10 @@ class CleanHierarchicalAttrsAction(BaseAction):
len(data), configuration_key
))
for entity_id, value in data.items():
entity_key = collections.OrderedDict({
"configuration_id": configuration_id,
"entity_id": entity_id
})
entity_key = collections.OrderedDict((
("configuration_id", configuration_id),
("entity_id", entity_id)
))
session.recorded_operations.push(
ftrack_api.operation.DeleteEntityOperation(
"CustomAttributeValue",

View file

@ -306,8 +306,8 @@ class CustomAttributes(BaseAction):
}
cust_attr_query = (
"select value, entity_id from ContextCustomAttributeValue "
"where configuration_id is {}"
"select value, entity_id from CustomAttributeValue"
" where configuration_id is {}"
)
for attr_def in object_type_attrs:
attr_ent_type = attr_def["entity_type"]
@ -328,21 +328,14 @@ class CustomAttributes(BaseAction):
self.log.debug((
"Converting Avalon MongoID attr for Entity type \"{}\"."
).format(entity_type_label))
call_expr = [{
"action": "query",
"expression": cust_attr_query.format(attr_def["id"])
}]
if hasattr(session, "call"):
[values] = session.call(call_expr)
else:
[values] = session._call(call_expr)
for value in values["data"]:
table_values = collections.OrderedDict({
"configuration_id": hierarchical_attr["id"],
"entity_id": value["entity_id"]
})
values = session.query(
cust_attr_query.format(attr_def["id"])
).all()
for value in values:
table_values = collections.OrderedDict([
("configuration_id", hierarchical_attr["id"]),
("entity_id", value["entity_id"])
])
session.recorded_operations.push(
ftrack_api.operation.UpdateEntityOperation(

View file

@ -303,9 +303,10 @@ class FtrackModule(
# TODO add add permissions check
# TODO add value validations
# - value type and list items
entity_key = collections.OrderedDict()
entity_key["configuration_id"] = configuration["id"]
entity_key["entity_id"] = project_id
entity_key = collections.OrderedDict([
("configuration_id", configuration["id"]),
("entity_id", project_id)
])
session.recorded_operations.push(
ftrack_api.operation.UpdateEntityOperation(

View file

@ -1,11 +1,8 @@
import os
import re
import json
import collections
import copy
import six
from avalon.api import AvalonMongoDB
import avalon
@ -18,7 +15,7 @@ from openpype.api import (
from openpype.lib import ApplicationManager
from .constants import CUST_ATTR_ID_KEY
from .custom_attributes import get_openpype_attr
from .custom_attributes import get_openpype_attr, query_custom_attributes
from bson.objectid import ObjectId
from bson.errors import InvalidId
@ -235,33 +232,19 @@ def get_hierarchical_attributes_values(
entity_ids = [item["id"] for item in entity["link"]]
join_ent_ids = join_query_keys(entity_ids)
join_attribute_ids = join_query_keys(attr_key_by_id.keys())
queries = []
queries.append({
"action": "query",
"expression": (
"select value, configuration_id, entity_id"
" from CustomAttributeValue"
" where entity_id in ({}) and configuration_id in ({})"
).format(join_ent_ids, join_attribute_ids)
})
if hasattr(session, "call"):
[values] = session.call(queries)
else:
[values] = session._call(queries)
values = query_custom_attributes(
session, list(attr_key_by_id.keys()), entity_ids, True
)
hier_values = {}
for key, val in defaults.items():
hier_values[key] = val
if not values["data"]:
if not values:
return hier_values
values_by_entity_id = collections.defaultdict(dict)
for item in values["data"]:
for item in values:
value = item["value"]
if value is None:
continue
@ -304,7 +287,7 @@ class SyncEntitiesFactory:
" from Project where full_name is \"{}\""
)
entities_query = (
"select id, name, type_id, parent_id, link"
"select id, name, type_id, parent_id, link, description"
" from TypedContext where project_id is \"{}\""
)
ignore_custom_attr_key = "avalon_ignore_sync"
@ -861,33 +844,6 @@ class SyncEntitiesFactory:
self.entities_dict[parent_id]["children"].remove(ftrack_id)
def _query_custom_attributes(self, session, conf_ids, entity_ids):
output = []
# Prepare values to query
attributes_joined = join_query_keys(conf_ids)
attributes_len = len(conf_ids)
chunk_size = int(5000 / attributes_len)
for idx in range(0, len(entity_ids), chunk_size):
entity_ids_joined = join_query_keys(
entity_ids[idx:idx + chunk_size]
)
call_expr = [{
"action": "query",
"expression": (
"select value, entity_id from ContextCustomAttributeValue "
"where entity_id in ({}) and configuration_id in ({})"
).format(entity_ids_joined, attributes_joined)
}]
if hasattr(session, "call"):
[result] = session.call(call_expr)
else:
[result] = session._call(call_expr)
for item in result["data"]:
output.append(item)
return output
def set_cutom_attributes(self):
self.log.debug("* Preparing custom attributes")
# Get custom attributes and values
@ -994,7 +950,7 @@ class SyncEntitiesFactory:
copy.deepcopy(prepared_avalon_attr_ca_id)
)
items = self._query_custom_attributes(
items = query_custom_attributes(
self.session,
list(attribute_key_by_id.keys()),
sync_ids
@ -1082,10 +1038,11 @@ class SyncEntitiesFactory:
for key, val in prepare_dict_avalon.items():
entity_dict["avalon_attrs"][key] = val
items = self._query_custom_attributes(
items = query_custom_attributes(
self.session,
list(attribute_key_by_id.keys()),
sync_ids
sync_ids,
True
)
avalon_hier = []
@ -1231,6 +1188,8 @@ class SyncEntitiesFactory:
data[key] = val
if ftrack_id != self.ft_project_id:
data["description"] = entity["description"]
ent_path_items = [ent["name"] for ent in entity["link"]]
parents = ent_path_items[1:len(ent_path_items) - 1:]
@ -1804,10 +1763,10 @@ class SyncEntitiesFactory:
configuration_id = self.entities_dict[ftrack_id][
"avalon_attrs_id"][CUST_ATTR_ID_KEY]
_entity_key = collections.OrderedDict({
"configuration_id": configuration_id,
"entity_id": ftrack_id
})
_entity_key = collections.OrderedDict([
("configuration_id", configuration_id),
("entity_id", ftrack_id)
])
self.session.recorded_operations.push(
ftrack_api.operation.UpdateEntityOperation(

View file

@ -17,7 +17,7 @@ def default_custom_attributes_definition():
def app_definitions_from_app_manager(app_manager):
_app_definitions = []
for app_name, app in app_manager.applications.items():
if app.enabled and app.is_host:
if app.enabled:
_app_definitions.append(
(app_name, app.full_label)
)
@ -88,26 +88,36 @@ def join_query_keys(keys):
return ",".join(["\"{}\"".format(key) for key in keys])
def query_custom_attributes(session, conf_ids, entity_ids, table_name=None):
def query_custom_attributes(
session, conf_ids, entity_ids, only_set_values=False
):
"""Query custom attribute values from ftrack database.
Using ftrack call method result may differ based on used table name and
version of ftrack server.
For hierarchical attributes you shou always use `only_set_values=True`
otherwise result will be default value of custom attribute and it would not
be possible to differentiate if value is set on entity or default value is
used.
Args:
session(ftrack_api.Session): Connected ftrack session.
conf_id(list, set, tuple): Configuration(attribute) ids which are
queried.
entity_ids(list, set, tuple): Entity ids for which are values queried.
table_name(str): Table nam from which values are queried. Not
recommended to change until you know what it means.
only_set_values(bool): Entities that don't have explicitly set
value won't return a value. If is set to False then default custom
attribute value is returned if value is not set.
"""
output = []
# Just skip
if not conf_ids or not entity_ids:
return output
if table_name is None:
if only_set_values:
table_name = "CustomAttributeValue"
else:
table_name = "ContextCustomAttributeValue"
# Prepare values to query
@ -122,19 +132,16 @@ def query_custom_attributes(session, conf_ids, entity_ids, table_name=None):
entity_ids_joined = join_query_keys(
entity_ids[idx:idx + chunk_size]
)
call_expr = [{
"action": "query",
"expression": (
"select value, entity_id from {}"
" where entity_id in ({}) and configuration_id in ({})"
).format(table_name, entity_ids_joined, attributes_joined)
}]
if hasattr(session, "call"):
[result] = session.call(call_expr)
else:
[result] = session._call(call_expr)
for item in result["data"]:
output.append(item)
output.extend(
session.query(
(
"select value, entity_id from {}"
" where entity_id in ({}) and configuration_id in ({})"
).format(
table_name,
entity_ids_joined,
attributes_joined
)
).all()
)
return output

View file

@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
"""Cleanup leftover files from publish."""
import os
import shutil
import pyblish.api
import avalon.api
class CleanUpFarm(pyblish.api.ContextPlugin):
"""Cleans up the staging directory after a successful publish.
This will also clean published renders and delete their parent directories.
"""
order = pyblish.api.IntegratorOrder + 11
label = "Clean Up Farm"
enabled = True
# Keep "filesequence" for backwards compatibility of older jobs
targets = ["filesequence", "farm"]
allowed_hosts = ("maya", )
def process(self, context):
# Get source host from which farm publishing was started
src_host_name = avalon.api.Session.get("AVALON_APP")
self.log.debug("Host name from session is {}".format(src_host_name))
# Skip process if is not in list of source hosts in which this
# plugin should run
if src_host_name not in self.allowed_hosts:
self.log.info((
"Source host \"{}\" is not in list of enabled hosts {}."
" Skipping"
).format(str(src_host_name), str(self.allowed_hosts)))
return
self.log.debug("Preparing filepaths to remove")
# Collect directories to remove
dirpaths_to_remove = set()
for instance in context:
staging_dir = instance.data.get("stagingDir")
if staging_dir:
dirpaths_to_remove.add(os.path.normpath(staging_dir))
if "representations" in instance.data:
for repre in instance.data["representations"]:
staging_dir = repre.get("stagingDir")
if staging_dir:
dirpaths_to_remove.add(os.path.normpath(staging_dir))
if not dirpaths_to_remove:
self.log.info("Nothing to remove. Skipping")
return
self.log.debug("Filepaths to remove are:\n{}".format(
"\n".join(["- {}".format(path) for path in dirpaths_to_remove])
))
# clean dirs which are empty
for dirpath in dirpaths_to_remove:
if not os.path.exists(dirpath):
self.log.debug("Skipping not existing directory \"{}\"".format(
dirpath
))
continue
self.log.debug("Removing directory \"{}\"".format(dirpath))
try:
shutil.rmtree(dirpath)
except OSError:
self.log.warning(
"Failed to remove directory \"{}\"".format(dirpath),
exc_info=True
)

View file

@ -12,11 +12,11 @@ Provides:
context -> anatomyData
"""
import os
import json
from openpype.lib import ApplicationManager
from avalon import api, lib
from openpype.lib import (
get_system_general_anatomy_data
)
from avalon import api
import pyblish.api
@ -44,6 +44,7 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin):
label = "Collect Anatomy Context Data"
def process(self, context):
task_name = api.Session["AVALON_TASK"]
project_entity = context.data["projectEntity"]
@ -79,6 +80,10 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin):
"app": context.data["hostName"]
}
# add system general settings anatomy data
system_general_data = get_system_general_anatomy_data()
context_data.update(system_general_data)
datetime_data = context.data.get("datetimeData") or {}
context_data.update(datetime_data)

View file

@ -21,7 +21,8 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin):
"""
order = pyblish.api.CollectorOrder - 0.2
targets = ["filesequence"]
# Keep "filesequence" for backwards compatibility of older jobs
targets = ["filesequence", "farm"]
label = "Collect rendered frames"
_context = None

View file

@ -5,7 +5,8 @@ class ValidateFileSequences(pyblish.api.ContextPlugin):
"""Validates whether any file sequences were collected."""
order = pyblish.api.ValidatorOrder
targets = ["filesequence"]
# Keep "filesequence" for backwards compatibility of older jobs
targets = ["filesequence", "farm"]
label = "Validate File Sequences"
def process(self, context):

View file

@ -134,7 +134,7 @@ class PypeCommands:
print(f"setting target: {target}")
pyblish.api.register_target(target)
else:
pyblish.api.register_target("filesequence")
pyblish.api.register_target("farm")
os.environ["OPENPYPE_PUBLISH_DATA"] = os.pathsep.join(paths)

View file

@ -14,6 +14,15 @@ def get_resource(*args):
return os.path.normpath(os.path.join(RESOURCES_DIR, *args))
def get_image_path(*args):
"""Helper function to get images.
Args:
*<str>: Filepath part items.
"""
return get_resource("images", *args)
def get_liberation_font_path(bold=False, italic=False):
font_name = "LiberationSans"
suffix = ""

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -5,6 +5,8 @@ from .constants import (
PROJECT_ANATOMY_KEY,
LOCAL_SETTING_KEY,
LEGACY_SETTINGS_VERSION,
SCHEMA_KEY_SYSTEM_SETTINGS,
SCHEMA_KEY_PROJECT_SETTINGS,
@ -37,6 +39,8 @@ __all__ = (
"PROJECT_ANATOMY_KEY",
"LOCAL_SETTING_KEY",
"LEGACY_SETTINGS_VERSION",
"SCHEMA_KEY_SYSTEM_SETTINGS",
"SCHEMA_KEY_PROJECT_SETTINGS",

View file

@ -21,6 +21,8 @@ PROJECT_SETTINGS_KEY = "project_settings"
PROJECT_ANATOMY_KEY = "project_anatomy"
LOCAL_SETTING_KEY = "local_settings"
LEGACY_SETTINGS_VERSION = "legacy"
# Schema hub names
SCHEMA_KEY_SYSTEM_SETTINGS = "system_schema"
SCHEMA_KEY_PROJECT_SETTINGS = "projects_schema"

View file

@ -207,6 +207,9 @@
"CleanUp": {
"paterns": [],
"remove_temp_renders": false
},
"CleanUpFarm": {
"enabled": false
}
},
"tools": {

View file

@ -170,6 +170,11 @@
"optional": true,
"active": true
},
"ValidateFrameRange": {
"enabled": true,
"optional": true,
"active": true
},
"ValidateShaderName": {
"enabled": false,
"regex": "(?P<asset>.*)_(.*)_SHD"

View file

@ -2,17 +2,17 @@
"publish": {
"CollectPublishedFiles": {
"task_type_to_family": {
"Animation": {
"workfile": {
"Animation": [
{
"is_sequence": false,
"extensions": [
"tvp"
],
"families": [],
"tags": [],
"subset_template_name": ""
"result_family": "workfile"
},
"render": {
{
"is_sequence": true,
"extensions": [
"png",
@ -26,20 +26,20 @@
"tags": [
"review"
],
"subset_template_name": ""
"result_family": "render"
}
},
"Compositing": {
"workfile": {
],
"Compositing": [
{
"is_sequence": false,
"extensions": [
"aep"
],
"families": [],
"tags": [],
"subset_template_name": ""
"result_family": "workfile"
},
"render": {
{
"is_sequence": true,
"extensions": [
"png",
@ -53,20 +53,20 @@
"tags": [
"review"
],
"subset_template_name": ""
"result_family": "render"
}
},
"Layout": {
"workfile": {
],
"Layout": [
{
"is_sequence": false,
"extensions": [
"psd"
],
"families": [],
"tags": [],
"subset_template_name": ""
"result_family": "workfile"
},
"image": {
{
"is_sequence": false,
"extensions": [
"png",
@ -81,20 +81,21 @@
"tags": [
"review"
],
"subset_template_name": ""
"result_family": "image"
}
},
"default_task_type": {
"workfile": {
],
"default_task_type": [
{
"is_sequence": false,
"extensions": [
"tvp"
"tvp",
"psd"
],
"families": [],
"tags": [],
"subset_template_name": "{family}{Variant}"
"result_family": "workfile"
},
"render": {
{
"is_sequence": true,
"extensions": [
"png",
@ -108,9 +109,9 @@
"tags": [
"review"
],
"subset_template_name": "{family}{Variant}"
"result_family": "render"
}
},
],
"__dynamic_keys_labels__": {
"default_task_type": "Default task type"
}

View file

@ -1186,58 +1186,6 @@
}
}
},
"shell": {
"enabled": true,
"environment": {},
"variants": {
"python_3-7": {
"use_python_2": true,
"executables": {
"windows": [],
"darwin": [],
"linux": []
},
"arguments": {
"windows": [],
"darwin": [],
"linux": []
},
"environment": {}
},
"python_2-7": {
"use_python_2": true,
"executables": {
"windows": [],
"darwin": [],
"linux": []
},
"arguments": {
"windows": [],
"darwin": [],
"linux": []
},
"environment": {}
},
"terminal": {
"use_python_2": true,
"executables": {
"windows": [],
"darwin": [],
"linux": []
},
"arguments": {
"windows": [],
"darwin": [],
"linux": []
},
"environment": {}
},
"__dynamic_keys_labels__": {
"python_3-7": "Python 3.7",
"python_2-7": "Python 2.7"
}
}
},
"djvview": {
"enabled": true,
"label": "DJV View",
@ -1263,5 +1211,6 @@
"1-1": "1.1"
}
}
}
},
"additional_apps": {}
}

View file

@ -263,6 +263,7 @@ class HostsEnumEntity(BaseEnumEntity):
class AppsEnumEntity(BaseEnumEntity):
"""Enum of applications for project anatomy attributes."""
schema_types = ["apps-enum"]
def _item_initialization(self):
@ -279,16 +280,30 @@ class AppsEnumEntity(BaseEnumEntity):
valid_keys = set()
enum_items_list = []
applications_entity = system_settings_entity["applications"]
app_entities = {}
additional_app_names = set()
additional_apps_entity = None
for group_name, app_group in applications_entity.items():
if group_name != "additional_apps":
app_entities[group_name] = app_group
continue
additional_apps_entity = app_group
for _group_name, _group in app_group.items():
additional_app_names.add(_group_name)
app_entities[_group_name] = _group
for group_name, app_group in app_entities.items():
enabled_entity = app_group.get("enabled")
if enabled_entity and not enabled_entity.value:
continue
host_name_entity = app_group.get("host_name")
if not host_name_entity or not host_name_entity.value:
continue
group_label = app_group["label"].value
if group_name in additional_app_names:
group_label = additional_apps_entity.get_key_label(group_name)
if not group_label:
group_label = group_name
else:
group_label = app_group["label"].value
variants_entity = app_group["variants"]
for variant_name, variant_entity in variants_entity.items():
enabled_entity = variant_entity.get("enabled")

View file

@ -34,15 +34,24 @@ from openpype.settings.lib import (
reset_default_settings,
get_studio_system_settings_overrides,
get_studio_system_settings_overrides_for_version,
save_studio_settings,
get_available_studio_system_settings_overrides_versions,
get_studio_project_settings_overrides,
get_studio_project_settings_overrides_for_version,
get_studio_project_anatomy_overrides,
get_studio_project_anatomy_overrides_for_version,
get_project_settings_overrides,
get_project_settings_overrides_for_version,
get_project_anatomy_overrides,
save_project_settings,
save_project_anatomy,
get_available_project_settings_overrides_versions,
get_available_studio_project_settings_overrides_versions,
get_available_studio_project_anatomy_overrides_versions,
find_environments,
apply_overrides
)
@ -495,17 +504,27 @@ class SystemSettings(RootEntity):
root_key = SYSTEM_SETTINGS_KEY
def __init__(
self, set_studio_state=True, reset=True, schema_hub=None
self,
set_studio_state=True,
reset=True,
schema_hub=None,
source_version=None
):
if schema_hub is None:
# Load system schemas
schema_hub = SchemasHub(SCHEMA_KEY_SYSTEM_SETTINGS)
self._source_version = source_version
super(SystemSettings, self).__init__(schema_hub, reset)
if set_studio_state:
self.set_studio_state()
@property
def source_version(self):
return self._source_version
def get_entity_from_path(self, path):
"""Return system settings entity."""
path_parts = path.split("/")
@ -524,12 +543,24 @@ class SystemSettings(RootEntity):
value = default_value.get(key, NOT_SET)
child_obj.update_default_value(value)
studio_overrides = get_studio_system_settings_overrides()
if self._source_version is None:
studio_overrides, version = get_studio_system_settings_overrides(
return_version=True
)
self._source_version = version
else:
studio_overrides = (
get_studio_system_settings_overrides_for_version(
self._source_version
)
)
for key, child_obj in self.non_gui_children.items():
value = studio_overrides.get(key, NOT_SET)
child_obj.update_studio_value(value)
def reset(self, new_state=None):
def reset(self, new_state=None, source_version=None):
"""Discard changes and reset entit's values.
Reload default values and studio override values and update entities.
@ -547,9 +578,22 @@ class SystemSettings(RootEntity):
if new_state is OverrideState.PROJECT:
raise ValueError("System settings can't store poject overrides.")
if source_version is not None:
self._source_version = source_version
self._reset_values()
self.set_override_state(new_state)
def get_available_source_versions(self, sorted=None):
if self.is_in_studio_state():
return self.get_available_studio_versions(sorted=sorted)
return []
def get_available_studio_versions(self, sorted=None):
return get_available_studio_system_settings_overrides_versions(
sorted=sorted
)
def defaults_dir(self):
"""Path to defaults directory.
@ -566,6 +610,8 @@ class SystemSettings(RootEntity):
json.dumps(settings_value, indent=4)
))
save_studio_settings(settings_value)
# Reset source version after restart
self._source_version = None
def _validate_defaults_to_save(self, value):
"""Valiations of default values before save."""
@ -622,11 +668,15 @@ class ProjectSettings(RootEntity):
project_name=None,
change_state=True,
reset=True,
schema_hub=None
schema_hub=None,
source_version=None,
anatomy_source_version=None
):
self._project_name = project_name
self._system_settings_entity = None
self._source_version = source_version
self._anatomy_source_version = anatomy_source_version
if schema_hub is None:
# Load system schemas
@ -640,6 +690,14 @@ class ProjectSettings(RootEntity):
else:
self.set_project_state()
@property
def source_version(self):
return self._source_version
@property
def anatomy_source_version(self):
return self._anatomy_source_version
@property
def project_name(self):
return self._project_name
@ -682,23 +740,20 @@ class ProjectSettings(RootEntity):
output = output[path_part]
return output
def change_project(self, project_name):
def change_project(self, project_name, source_version=None):
if project_name == self._project_name:
return
if (
source_version is None
or source_version == self._source_version
):
if not self.is_in_project_state():
self.set_project_state()
return
self._project_name = project_name
if project_name is None:
self.set_studio_state()
return
project_override_value = {
PROJECT_SETTINGS_KEY: get_project_settings_overrides(project_name),
PROJECT_ANATOMY_KEY: get_project_anatomy_overrides(project_name)
}
for key, child_obj in self.non_gui_children.items():
value = project_override_value.get(key, NOT_SET)
child_obj.update_project_value(value)
self._source_version = source_version
self._anatomy_source_version = None
self._set_values_for_project(project_name)
self.set_project_state()
def _reset_values(self):
@ -710,27 +765,97 @@ class ProjectSettings(RootEntity):
value = default_values.get(key, NOT_SET)
child_obj.update_default_value(value)
self._set_values_for_project(self.project_name)
def _set_values_for_project(self, project_name):
self._project_name = project_name
if project_name:
project_settings_overrides = (
get_studio_project_settings_overrides()
)
project_anatomy_overrides = (
get_studio_project_anatomy_overrides()
)
else:
if self._source_version is None:
project_settings_overrides, version = (
get_studio_project_settings_overrides(return_version=True)
)
self._source_version = version
else:
project_settings_overrides = (
get_studio_project_settings_overrides_for_version(
self._source_version
)
)
if self._anatomy_source_version is None:
project_anatomy_overrides, anatomy_version = (
get_studio_project_anatomy_overrides(return_version=True)
)
self._anatomy_source_version = anatomy_version
else:
project_anatomy_overrides = (
get_studio_project_anatomy_overrides_for_version(
self._anatomy_source_version
)
)
studio_overrides = {
PROJECT_SETTINGS_KEY: get_studio_project_settings_overrides(),
PROJECT_ANATOMY_KEY: get_studio_project_anatomy_overrides()
PROJECT_SETTINGS_KEY: project_settings_overrides,
PROJECT_ANATOMY_KEY: project_anatomy_overrides
}
for key, child_obj in self.non_gui_children.items():
value = studio_overrides.get(key, NOT_SET)
child_obj.update_studio_value(value)
if not self.project_name:
if not project_name:
return
project_name = self.project_name
if self._source_version is None:
project_settings_overrides, version = (
get_project_settings_overrides(
project_name, return_version=True
)
)
self._source_version = version
else:
project_settings_overrides = (
get_project_settings_overrides_for_version(
project_name, self._source_version
)
)
project_override_value = {
PROJECT_SETTINGS_KEY: get_project_settings_overrides(project_name),
PROJECT_SETTINGS_KEY: project_settings_overrides,
PROJECT_ANATOMY_KEY: get_project_anatomy_overrides(project_name)
}
for key, child_obj in self.non_gui_children.items():
value = project_override_value.get(key, NOT_SET)
child_obj.update_project_value(value)
def get_available_source_versions(self, sorted=None):
if self.is_in_studio_state():
return self.get_available_studio_versions(sorted=sorted)
elif self.is_in_project_state():
return get_available_project_settings_overrides_versions(
self.project_name, sorted=sorted
)
return []
def get_available_studio_versions(self, sorted=None):
return get_available_studio_project_settings_overrides_versions(
sorted=sorted
)
def get_available_anatomy_source_versions(self, sorted=None):
if self.is_in_studio_state():
return get_available_studio_project_anatomy_overrides_versions(
sorted=sorted
)
return []
def reset(self, new_state=None):
"""Discard changes and reset entit's values.
@ -763,6 +888,9 @@ class ProjectSettings(RootEntity):
self._validate_values_to_save(settings_value)
self._source_version = None
self._anatomy_source_version = None
self.log.debug("Saving project settings: {}".format(
json.dumps(settings_value, indent=4)
))

View file

@ -24,10 +24,10 @@
"label": "Task type to family mapping",
"collapsible_key": true,
"object_type": {
"type": "dict-modifiable",
"collapsible": false,
"type": "list",
"collapsible": true,
"key": "task_type",
"collapsible_key": false,
"collapsible_key": true,
"object_type": {
"type": "dict",
"children": [
@ -52,10 +52,13 @@
"type": "schema",
"name": "schema_representation_tags"
},
{
"type": "separator"
},
{
"type": "text",
"key": "subset_template_name",
"label": "Subset template name"
"key": "result_family",
"label": "Resulting family"
}
]
}

View file

@ -677,8 +677,22 @@
"label": "Remove Temp renders",
"default": false
}
]
},
{
"type": "dict",
"collapsible": false,
"key": "CleanUpFarm",
"label": "Clean Up Farm",
"is_group": true,
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
}
]
}
]
}

View file

@ -48,6 +48,16 @@
}
]
},
{
"type": "schema_template",
"name": "template_publish_plugin",
"template_data": [
{
"key": "ValidateFrameRange",
"label": "Validate Frame Range"
}
]
},
{
"type": "dict",
"collapsible": true,

View file

@ -1,35 +0,0 @@
{
"type": "dict",
"key": "shell",
"label": "Shell",
"collapsible": true,
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"key": "environment",
"label": "Environment",
"type": "raw-json"
},
{
"type": "dict-modifiable",
"key": "variants",
"collapsible_key": true,
"use_label_wrap": false,
"object_type": {
"type": "dict",
"collapsible": true,
"children": [
{
"type": "schema_template",
"name": "template_host_variant_items"
}
]
}
}
]
}

View file

@ -87,11 +87,50 @@
},
{
"type": "schema",
"name": "schema_shell"
"name": "schema_djv"
},
{
"type": "schema",
"name": "schema_djv"
"type": "dict-modifiable",
"key": "additional_apps",
"label": "Additional",
"collapsible": true,
"collapsible_key": true,
"object_type": {
"type": "dict",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "schema_template",
"name": "template_host_unchangables",
"skip_paths": ["host_name", "label"]
},
{
"key": "environment",
"label": "Environment",
"type": "raw-json"
},
{
"type": "dict-modifiable",
"key": "variants",
"collapsible_key": true,
"use_label_wrap": false,
"object_type": {
"type": "dict",
"collapsible": true,
"children": [
{
"type": "schema_template",
"name": "template_host_variant_items"
}
]
}
}
]
}
}
]
}

File diff suppressed because it is too large Load diff

View file

@ -266,23 +266,31 @@ def save_project_anatomy(project_name, anatomy_data):
@require_handler
def get_studio_system_settings_overrides():
return _SETTINGS_HANDLER.get_studio_system_settings_overrides()
def get_studio_system_settings_overrides(return_version=False):
return _SETTINGS_HANDLER.get_studio_system_settings_overrides(
return_version
)
@require_handler
def get_studio_project_settings_overrides():
return _SETTINGS_HANDLER.get_studio_project_settings_overrides()
def get_studio_project_settings_overrides(return_version=False):
return _SETTINGS_HANDLER.get_studio_project_settings_overrides(
return_version
)
@require_handler
def get_studio_project_anatomy_overrides():
return _SETTINGS_HANDLER.get_studio_project_anatomy_overrides()
def get_studio_project_anatomy_overrides(return_version=False):
return _SETTINGS_HANDLER.get_studio_project_anatomy_overrides(
return_version
)
@require_handler
def get_project_settings_overrides(project_name):
return _SETTINGS_HANDLER.get_project_settings_overrides(project_name)
def get_project_settings_overrides(project_name, return_version=False):
return _SETTINGS_HANDLER.get_project_settings_overrides(
project_name, return_version
)
@require_handler
@ -290,6 +298,123 @@ def get_project_anatomy_overrides(project_name):
return _SETTINGS_HANDLER.get_project_anatomy_overrides(project_name)
@require_handler
def get_studio_system_settings_overrides_for_version(version):
return (
_SETTINGS_HANDLER
.get_studio_system_settings_overrides_for_version(version)
)
@require_handler
def get_studio_project_anatomy_overrides_for_version(version):
return (
_SETTINGS_HANDLER
.get_studio_project_anatomy_overrides_for_version(version)
)
@require_handler
def get_studio_project_settings_overrides_for_version(version):
return (
_SETTINGS_HANDLER
.get_studio_project_settings_overrides_for_version(version)
)
@require_handler
def get_project_settings_overrides_for_version(
project_name, version
):
return (
_SETTINGS_HANDLER
.get_project_settings_overrides_for_version(project_name, version)
)
@require_handler
def get_available_studio_system_settings_overrides_versions(sorted=None):
return (
_SETTINGS_HANDLER
.get_available_studio_system_settings_overrides_versions(
sorted=sorted
)
)
@require_handler
def get_available_studio_project_anatomy_overrides_versions(sorted=None):
return (
_SETTINGS_HANDLER
.get_available_studio_project_anatomy_overrides_versions(
sorted=sorted
)
)
@require_handler
def get_available_studio_project_settings_overrides_versions(sorted=None):
return (
_SETTINGS_HANDLER
.get_available_studio_project_settings_overrides_versions(
sorted=sorted
)
)
@require_handler
def get_available_project_settings_overrides_versions(
project_name, sorted=None
):
return (
_SETTINGS_HANDLER
.get_available_project_settings_overrides_versions(
project_name, sorted=sorted
)
)
@require_handler
def find_closest_version_for_projects(project_names):
return (
_SETTINGS_HANDLER
.find_closest_version_for_projects(project_names)
)
@require_handler
def clear_studio_system_settings_overrides_for_version(version):
return (
_SETTINGS_HANDLER
.clear_studio_system_settings_overrides_for_version(version)
)
@require_handler
def clear_studio_project_settings_overrides_for_version(version):
return (
_SETTINGS_HANDLER
.clear_studio_project_settings_overrides_for_version(version)
)
@require_handler
def clear_studio_project_anatomy_overrides_for_version(version):
return (
_SETTINGS_HANDLER
.clear_studio_project_anatomy_overrides_for_version(version)
)
@require_handler
def clear_project_settings_overrides_for_version(
version, project_name
):
return _SETTINGS_HANDLER.clear_project_settings_overrides_for_version(
version, project_name
)
@require_local_handler
def save_local_settings(data):
return _LOCAL_SETTINGS_HANDLER.save_local_settings(data)
@ -580,11 +705,26 @@ def apply_local_settings_on_system_settings(system_settings, local_settings):
return
current_platform = platform.system().lower()
apps_settings = system_settings["applications"]
additional_apps = apps_settings["additional_apps"]
for app_group_name, value in local_settings["applications"].items():
if not value or app_group_name not in system_settings["applications"]:
if not value:
continue
variants = system_settings["applications"][app_group_name]["variants"]
if (
app_group_name not in apps_settings
and app_group_name not in additional_apps
):
continue
if app_group_name in apps_settings:
variants = apps_settings[app_group_name]["variants"]
else:
variants = (
apps_settings["additional_apps"][app_group_name]["variants"]
)
for app_name, app_value in value.items():
if (
not app_value

View file

@ -118,7 +118,10 @@
"image-btn-hover": "#189aea",
"image-btn-disabled": "#bfccd6",
"version-exists": "#458056",
"version-not-found": "#ffc671"
"version-not-found": "#ffc671",
"source-version": "#D3D8DE",
"source-version-outdated": "#ffc671"
}
}
}

View file

@ -1117,6 +1117,20 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
#ExpandLabel[state="invalid"]:hover, #SettingsLabel[state="invalid"]:hover {
color: {color:settings:invalid-dark};
}
#SettingsOutdatedSourceVersion {
color: {color:settings:source-version-outdated};
}
#SourceVersionLabel {
padding-left: 3px;
padding-right: 3px;
}
#SourceVersionLabel[state="same"] {
color: {color:settings:source-version};
}
#SourceVersionLabel[state="different"] {
color: {color:settings:source-version-outdated};
}
/* TODO Replace these with explicit widget types if possible */
#SettingsMainWidget QWidget[input-state="modified"] {
@ -1132,8 +1146,8 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
border-color: {color:settings:invalid-dark};
}
#GroupWidget {
border-bottom: 1px solid #21252B;
#SettingsFooter {
border-top: 1px solid #21252B;
}
#ProjectListWidget QLabel {
@ -1141,6 +1155,10 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
font-weight: bold;
}
#ProjectListContentWidget {
background: {color:bg-view};
}
#MultiSelectionComboBox {
font-size: 12px;
}

View file

@ -15,8 +15,12 @@ from .constants import (
from .actions import ApplicationAction
from Qt import QtCore, QtGui
from avalon.vendor import qtawesome
from avalon import style, api
from openpype.lib import ApplicationManager, JSONSettingRegistry
from avalon import api
from openpype.lib import JSONSettingRegistry
from openpype.lib.applications import (
CUSTOM_LAUNCH_APP_GROUPS,
ApplicationManager
)
log = logging.getLogger(__name__)
@ -72,6 +76,9 @@ class ActionModel(QtGui.QStandardItemModel):
if not app or not app.enabled:
continue
if app.group.name in CUSTOM_LAUNCH_APP_GROUPS:
continue
# Get from app definition, if not there from app in project
action = type(
"app_{}".format(app_name),
@ -313,7 +320,7 @@ class ActionModel(QtGui.QStandardItemModel):
action = action[0]
compare_data = {}
if action:
if action and action.label:
compare_data = {
"app_label": action.label.lower(),
"project_name": self.dbcon.Session["AVALON_PROJECT"],

View file

@ -6,6 +6,7 @@ from avalon.vendor import qtawesome
from .delegates import ActionDelegate
from . import lib
from .actions import ApplicationAction
from .models import ActionModel
from openpype.tools.flickcharm import FlickCharm
from .constants import (
@ -239,10 +240,12 @@ class ActionBar(QtWidgets.QWidget):
is_variant_group = index.data(VARIANT_GROUP_ROLE)
if not is_group and not is_variant_group:
action = index.data(ACTION_ROLE)
if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE):
action.data["start_last_workfile"] = False
else:
action.data.pop("start_last_workfile", None)
# Change data of application action
if issubclass(action, ApplicationAction):
if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE):
action.data["start_last_workfile"] = False
else:
action.data.pop("start_last_workfile", None)
self._start_animation(index)
self.action_clicked.emit(action)
return

View file

@ -2,7 +2,9 @@ from Qt import QtWidgets, QtCore
from .widgets import (
NameTextEdit,
FilterComboBox
FilterComboBox,
SpinBoxScrollFixed,
DoubleSpinBoxScrollFixed
)
from .multiselection_combobox import MultiSelectionComboBox
@ -89,9 +91,9 @@ class NumberDelegate(QtWidgets.QStyledItemDelegate):
def createEditor(self, parent, option, index):
if self.decimals > 0:
editor = QtWidgets.QDoubleSpinBox(parent)
editor = DoubleSpinBoxScrollFixed(parent)
else:
editor = QtWidgets.QSpinBox(parent)
editor = SpinBoxScrollFixed(parent)
editor.setObjectName("NumberEditor")
# Set min/max

View file

@ -1,8 +1,8 @@
import os
from Qt import QtCore, QtGui
from openpype.style import get_objected_colors
from avalon.vendor import qtawesome
from openpype.tools.utils import paint_image_with_color
class ResourceCache:
@ -91,17 +91,6 @@ class ResourceCache:
icon.addPixmap(disabled_pix, QtGui.QIcon.Disabled, QtGui.QIcon.Off)
return icon
@classmethod
def get_warning_pixmap(cls):
src_image = get_warning_image()
colors = get_objected_colors()
color_value = colors["delete-btn-bg"]
return paint_image_with_color(
src_image,
color_value.get_qcolor()
)
def get_remove_image():
image_path = os.path.join(
@ -110,36 +99,3 @@ def get_remove_image():
"bin.png"
)
return QtGui.QImage(image_path)
def get_warning_image():
image_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"images",
"warning.png"
)
return QtGui.QImage(image_path)
def paint_image_with_color(image, color):
"""TODO: This function should be imported from utils.
At the moment of creation is not available yet.
"""
width = image.width()
height = image.height()
alpha_mask = image.createAlphaMask()
alpha_region = QtGui.QRegion(QtGui.QBitmap.fromImage(alpha_mask))
pixmap = QtGui.QPixmap(width, height)
pixmap.fill(QtCore.Qt.transparent)
painter = QtGui.QPainter(pixmap)
painter.setClipRegion(alpha_region)
painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(color)
painter.drawRect(QtCore.QRect(0, 0, width, height))
painter.end()
return pixmap

View file

@ -4,14 +4,16 @@ from .constants import (
NAME_ALLOWED_SYMBOLS,
NAME_REGEX
)
from .style import ResourceCache
from openpype.lib import (
create_project,
PROJECT_NAME_ALLOWED_SYMBOLS,
PROJECT_NAME_REGEX
)
from openpype.style import load_stylesheet
from openpype.tools.utils import PlaceholderLineEdit
from openpype.tools.utils import (
PlaceholderLineEdit,
get_warning_pixmap
)
from avalon.api import AvalonMongoDB
from Qt import QtWidgets, QtCore, QtGui
@ -338,7 +340,7 @@ class ConfirmProjectDeletion(QtWidgets.QDialog):
top_widget = QtWidgets.QWidget(self)
warning_pixmap = ResourceCache.get_warning_pixmap()
warning_pixmap = get_warning_pixmap()
warning_icon_label = PixmapLabel(warning_pixmap, top_widget)
message_label = QtWidgets.QLabel(top_widget)
@ -429,3 +431,29 @@ class ConfirmProjectDeletion(QtWidgets.QDialog):
def _on_confirm_text_change(self):
enabled = self._confirm_input.text() == self._project_name
self._confirm_btn.setEnabled(enabled)
class SpinBoxScrollFixed(QtWidgets.QSpinBox):
"""QSpinBox which only allow edits change with scroll wheel when active"""
def __init__(self, *args, **kwargs):
super(SpinBoxScrollFixed, self).__init__(*args, **kwargs)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
def wheelEvent(self, event):
if not self.hasFocus():
event.ignore()
else:
super(SpinBoxScrollFixed, self).wheelEvent(event)
class DoubleSpinBoxScrollFixed(QtWidgets.QDoubleSpinBox):
"""QDoubleSpinBox which only allow edits with scroll wheel when active"""
def __init__(self, *args, **kwargs):
super(DoubleSpinBoxScrollFixed, self).__init__(*args, **kwargs)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
def wheelEvent(self, event):
if not self.hasFocus():
event.ignore()
else:
super(DoubleSpinBoxScrollFixed, self).wheelEvent(event)

View file

@ -180,7 +180,16 @@ class LocalApplicationsWidgets(QtWidgets.QWidget):
self.content_layout.removeItem(item)
self.widgets_by_group_name.clear()
app_items = {}
for key, entity in self.system_settings_entity["applications"].items():
if key != "additional_apps":
app_items[key] = entity
continue
for _key, _entity in entity.items():
app_items[_key] = _entity
for key, entity in app_items.items():
# Filter not enabled app groups
if not entity["enabled"].value:
continue

View file

@ -3,8 +3,10 @@ import sys
import traceback
import contextlib
from enum import Enum
from Qt import QtWidgets, QtCore, QtGui
from Qt import QtWidgets, QtCore
from openpype.lib import get_openpype_version
from openpype.tools.utils import set_style_property
from openpype.settings.entities import (
SystemSettings,
ProjectSettings,
@ -34,7 +36,10 @@ from openpype.settings.entities.op_version_entity import (
)
from openpype.settings import SaveWarningExc
from .widgets import ProjectListWidget
from .widgets import (
ProjectListWidget,
VersionAction
)
from .breadcrumbs_widget import (
BreadcrumbsAddressBar,
SystemSettingsBreadcrumbs,
@ -88,6 +93,20 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
restart_required_trigger = QtCore.Signal()
full_path_requested = QtCore.Signal(str, str)
require_restart_label_text = (
"Your changes require restart of"
" all running OpenPype processes to take affect."
)
outdated_version_label_text = (
"Your settings are loaded from an older version."
)
source_version_tooltip = "Using settings of current OpenPype version"
source_version_tooltip_outdated = (
"Please check that all settings are still correct (blue colour\n"
"indicates potential changes in the new version) and save your\n"
"settings to update them to you current running OpenPype version."
)
def __init__(self, user_role, parent=None):
super(SettingsCategoryWidget, self).__init__(parent)
@ -98,6 +117,10 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
self._state = CategoryState.Idle
self._hide_studio_overrides = False
self._updating_root = False
self._use_version = None
self._current_version = get_openpype_version()
self.ignore_input_changes = IgnoreInputChangesObj(self)
self.keys = []
@ -183,77 +206,126 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
def initialize_attributes(self):
return
@property
def is_modifying_defaults(self):
if self.modify_defaults_checkbox is None:
return False
return self.modify_defaults_checkbox.isChecked()
def create_ui(self):
self.modify_defaults_checkbox = None
scroll_widget = QtWidgets.QScrollArea(self)
scroll_widget.setObjectName("GroupWidget")
content_widget = QtWidgets.QWidget(scroll_widget)
conf_wrapper_widget = QtWidgets.QWidget(self)
configurations_widget = QtWidgets.QWidget(conf_wrapper_widget)
breadcrumbs_label = QtWidgets.QLabel("Path:", content_widget)
breadcrumbs_widget = BreadcrumbsAddressBar(content_widget)
# Breadcrumbs/Path widget
breadcrumbs_widget = QtWidgets.QWidget(self)
breadcrumbs_label = QtWidgets.QLabel("Path:", breadcrumbs_widget)
breadcrumbs_bar = BreadcrumbsAddressBar(breadcrumbs_widget)
breadcrumbs_layout = QtWidgets.QHBoxLayout()
refresh_icon = qtawesome.icon("fa.refresh", color="white")
refresh_btn = QtWidgets.QPushButton(breadcrumbs_widget)
refresh_btn.setIcon(refresh_icon)
breadcrumbs_layout = QtWidgets.QHBoxLayout(breadcrumbs_widget)
breadcrumbs_layout.setContentsMargins(5, 5, 5, 5)
breadcrumbs_layout.setSpacing(5)
breadcrumbs_layout.addWidget(breadcrumbs_label)
breadcrumbs_layout.addWidget(breadcrumbs_widget)
breadcrumbs_layout.addWidget(breadcrumbs_label, 0)
breadcrumbs_layout.addWidget(breadcrumbs_bar, 1)
breadcrumbs_layout.addWidget(refresh_btn, 0)
# Widgets representing settings entities
scroll_widget = QtWidgets.QScrollArea(configurations_widget)
content_widget = QtWidgets.QWidget(scroll_widget)
scroll_widget.setWidgetResizable(True)
scroll_widget.setWidget(content_widget)
content_layout = QtWidgets.QVBoxLayout(content_widget)
content_layout.setContentsMargins(3, 3, 3, 3)
content_layout.setSpacing(5)
content_layout.setAlignment(QtCore.Qt.AlignTop)
scroll_widget.setWidgetResizable(True)
scroll_widget.setWidget(content_widget)
# Footer widget
footer_widget = QtWidgets.QWidget(self)
footer_widget.setObjectName("SettingsFooter")
refresh_icon = qtawesome.icon("fa.refresh", color="white")
refresh_btn = QtWidgets.QPushButton(self)
refresh_btn.setIcon(refresh_icon)
# Info labels
# TODO dynamic labels
labels_alignment = QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter
empty_label = QtWidgets.QLabel(footer_widget)
footer_layout = QtWidgets.QHBoxLayout()
outdated_version_label = QtWidgets.QLabel(
self.outdated_version_label_text, footer_widget
)
outdated_version_label.setToolTip(self.source_version_tooltip_outdated)
outdated_version_label.setAlignment(labels_alignment)
outdated_version_label.setVisible(False)
outdated_version_label.setObjectName("SettingsOutdatedSourceVersion")
require_restart_label = QtWidgets.QLabel(
self.require_restart_label_text, footer_widget
)
require_restart_label.setAlignment(labels_alignment)
require_restart_label.setVisible(False)
# Label showing source version of loaded settings
source_version_label = QtWidgets.QLabel("", footer_widget)
source_version_label.setObjectName("SourceVersionLabel")
set_style_property(source_version_label, "state", "")
source_version_label.setToolTip(self.source_version_tooltip)
save_btn = QtWidgets.QPushButton("Save", footer_widget)
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
footer_layout.setContentsMargins(5, 5, 5, 5)
if self.user_role == "developer":
self._add_developer_ui(footer_layout)
self._add_developer_ui(footer_layout, footer_widget)
save_btn = QtWidgets.QPushButton("Save", self)
require_restart_label = QtWidgets.QLabel(self)
require_restart_label.setAlignment(QtCore.Qt.AlignCenter)
footer_layout.addWidget(refresh_btn, 0)
footer_layout.addWidget(empty_label, 1)
footer_layout.addWidget(outdated_version_label, 1)
footer_layout.addWidget(require_restart_label, 1)
footer_layout.addWidget(source_version_label, 0)
footer_layout.addWidget(save_btn, 0)
configurations_layout = QtWidgets.QVBoxLayout()
configurations_layout = QtWidgets.QVBoxLayout(configurations_widget)
configurations_layout.setContentsMargins(0, 0, 0, 0)
configurations_layout.setSpacing(0)
configurations_layout.addWidget(scroll_widget, 1)
configurations_layout.addLayout(footer_layout, 0)
conf_wrapper_layout = QtWidgets.QHBoxLayout()
conf_wrapper_layout = QtWidgets.QHBoxLayout(conf_wrapper_widget)
conf_wrapper_layout.setContentsMargins(0, 0, 0, 0)
conf_wrapper_layout.setSpacing(0)
conf_wrapper_layout.addLayout(configurations_layout, 1)
conf_wrapper_layout.addWidget(configurations_widget, 1)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
main_layout.addLayout(breadcrumbs_layout, 0)
main_layout.addLayout(conf_wrapper_layout, 1)
main_layout.addWidget(breadcrumbs_widget, 0)
main_layout.addWidget(conf_wrapper_widget, 1)
main_layout.addWidget(footer_widget, 0)
save_btn.clicked.connect(self._save)
refresh_btn.clicked.connect(self._on_refresh)
breadcrumbs_widget.path_edited.connect(self._on_path_edit)
breadcrumbs_bar.path_edited.connect(self._on_path_edit)
self._require_restart_label = require_restart_label
self._outdated_version_label = outdated_version_label
self._empty_label = empty_label
self._is_loaded_version_outdated = False
self.save_btn = save_btn
self.refresh_btn = refresh_btn
self.require_restart_label = require_restart_label
self._source_version_label = source_version_label
self.scroll_widget = scroll_widget
self.content_layout = content_layout
self.content_widget = content_widget
self.breadcrumbs_widget = breadcrumbs_widget
self.breadcrumbs_bar = breadcrumbs_bar
self.breadcrumbs_model = None
self.refresh_btn = refresh_btn
self.conf_wrapper_layout = conf_wrapper_layout
self.main_layout = main_layout
@ -308,21 +380,17 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
pass
def set_path(self, path):
self.breadcrumbs_widget.set_path(path)
self.breadcrumbs_bar.set_path(path)
def _add_developer_ui(self, footer_layout):
modify_defaults_widget = QtWidgets.QWidget()
modify_defaults_checkbox = QtWidgets.QCheckBox(modify_defaults_widget)
def _add_developer_ui(self, footer_layout, footer_widget):
modify_defaults_checkbox = QtWidgets.QCheckBox(footer_widget)
modify_defaults_checkbox.setChecked(self._hide_studio_overrides)
label_widget = QtWidgets.QLabel(
"Modify defaults", modify_defaults_widget
"Modify defaults", footer_widget
)
modify_defaults_layout = QtWidgets.QHBoxLayout(modify_defaults_widget)
modify_defaults_layout.addWidget(label_widget)
modify_defaults_layout.addWidget(modify_defaults_checkbox)
footer_layout.addWidget(modify_defaults_widget, 0)
footer_layout.addWidget(label_widget, 0)
footer_layout.addWidget(modify_defaults_checkbox, 0)
modify_defaults_checkbox.stateChanged.connect(
self._on_modify_defaults
@ -361,6 +429,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
try:
self.entity.save()
self._use_version = None
# NOTE There are relations to previous entities and C++ callbacks
# so it is easier to just use new entity and recreate UI but
@ -420,13 +489,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
return
def _on_require_restart_change(self):
value = ""
if self.entity.require_restart:
value = (
"Your changes require restart of"
" all running OpenPype processes to take affect."
)
self.require_restart_label.setText(value)
self._update_labels_visibility()
def reset(self):
self.set_state(CategoryState.Working)
@ -444,6 +507,8 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
widget.deleteLater()
dialog = None
self._updating_root = True
source_version = ""
try:
self._create_root_entity()
@ -459,6 +524,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
input_field.set_entity_value()
self.ignore_input_changes.set_ignore(False)
source_version = self.entity.source_version
except DefaultsNotDefined:
dialog = QtWidgets.QMessageBox(self)
@ -502,6 +568,27 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
spacer, layout.rowCount(), 0, 1, layout.columnCount()
)
self._updating_root = False
# Update source version label
state_value = ""
tooltip = ""
outdated = False
if source_version:
if source_version != self._current_version:
state_value = "different"
tooltip = self.source_version_tooltip_outdated
outdated = True
else:
state_value = "same"
tooltip = self.source_version_tooltip
self._is_loaded_version_outdated = outdated
self._source_version_label.setText(source_version)
self._source_version_label.setToolTip(tooltip)
set_style_property(self._source_version_label, "state", state_value)
self._update_labels_visibility()
self.set_state(CategoryState.Idle)
if dialog:
@ -510,6 +597,36 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
else:
self._on_reset_success()
def _on_source_version_change(self, version):
if self._updating_root:
return
if version == self._current_version:
version = None
self._use_version = version
QtCore.QTimer.singleShot(20, self.reset)
def add_context_actions(self, menu):
if not self.entity or self.is_modifying_defaults:
return
versions = self.entity.get_available_studio_versions(sorted=True)
if not versions:
return
submenu = QtWidgets.QMenu("Use settings from version", menu)
for version in reversed(versions):
action = VersionAction(version, submenu)
action.version_triggered.connect(
self._on_context_version_trigger
)
submenu.addAction(action)
menu.addMenu(submenu)
def _on_context_version_trigger(self, version):
self._on_source_version_change(version)
def _on_reset_crash(self):
self.save_btn.setEnabled(False)
@ -521,10 +638,10 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
self.save_btn.setEnabled(True)
if self.breadcrumbs_model is not None:
path = self.breadcrumbs_widget.path()
self.breadcrumbs_widget.set_path("")
path = self.breadcrumbs_bar.path()
self.breadcrumbs_bar.set_path("")
self.breadcrumbs_model.set_entity(self.entity)
self.breadcrumbs_widget.change_path(path)
self.breadcrumbs_bar.change_path(path)
def add_children_gui(self):
for child_obj in self.entity.children:
@ -565,10 +682,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
def _save(self):
# Don't trigger restart if defaults are modified
if (
self.modify_defaults_checkbox
and self.modify_defaults_checkbox.isChecked()
):
if self.is_modifying_defaults:
require_restart = False
else:
require_restart = self.entity.require_restart
@ -584,7 +698,29 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
if require_restart:
self.restart_required_trigger.emit()
self.require_restart_label.setText("")
def _update_labels_visibility(self):
visible_label = None
labels = {
self._empty_label,
self._outdated_version_label,
self._require_restart_label,
}
if self.entity.require_restart:
visible_label = self._require_restart_label
elif self._is_loaded_version_outdated:
visible_label = self._outdated_version_label
else:
visible_label = self._empty_label
if visible_label.isVisible():
return
for label in labels:
if label is visible_label:
visible_label.setVisible(True)
else:
label.setVisible(False)
def _on_refresh(self):
self.reset()
@ -594,25 +730,29 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
class SystemWidget(SettingsCategoryWidget):
def __init__(self, *args, **kwargs):
self._actions = []
super(SystemWidget, self).__init__(*args, **kwargs)
def contain_category_key(self, category):
if category == "system_settings":
return True
return False
def set_category_path(self, category, path):
self.breadcrumbs_widget.change_path(path)
self.breadcrumbs_bar.change_path(path)
def _create_root_entity(self):
self.entity = SystemSettings(set_studio_state=False)
self.entity.on_change_callbacks.append(self._on_entity_change)
entity = SystemSettings(
set_studio_state=False, source_version=self._use_version
)
entity.on_change_callbacks.append(self._on_entity_change)
self.entity = entity
try:
if (
self.modify_defaults_checkbox
and self.modify_defaults_checkbox.isChecked()
):
self.entity.set_defaults_state()
if self.is_modifying_defaults:
entity.set_defaults_state()
else:
self.entity.set_studio_state()
entity.set_studio_state()
if self.modify_defaults_checkbox:
self.modify_defaults_checkbox.setEnabled(True)
@ -620,16 +760,16 @@ class SystemWidget(SettingsCategoryWidget):
if not self.modify_defaults_checkbox:
raise
self.entity.set_defaults_state()
entity.set_defaults_state()
self.modify_defaults_checkbox.setChecked(True)
self.modify_defaults_checkbox.setEnabled(False)
def ui_tweaks(self):
self.breadcrumbs_model = SystemSettingsBreadcrumbs()
self.breadcrumbs_widget.set_model(self.breadcrumbs_model)
self.breadcrumbs_bar.set_model(self.breadcrumbs_model)
def _on_modify_defaults(self):
if self.modify_defaults_checkbox.isChecked():
if self.is_modifying_defaults:
if not self.entity.is_in_defaults_state():
self.reset()
else:
@ -638,6 +778,9 @@ class SystemWidget(SettingsCategoryWidget):
class ProjectWidget(SettingsCategoryWidget):
def __init__(self, *args, **kwargs):
super(ProjectWidget, self).__init__(*args, **kwargs)
def contain_category_key(self, category):
if category in ("project_settings", "project_anatomy"):
return True
@ -651,28 +794,28 @@ class ProjectWidget(SettingsCategoryWidget):
else:
path = category
self.breadcrumbs_widget.change_path(path)
self.breadcrumbs_bar.change_path(path)
def initialize_attributes(self):
self.project_name = None
def ui_tweaks(self):
self.breadcrumbs_model = ProjectSettingsBreadcrumbs()
self.breadcrumbs_widget.set_model(self.breadcrumbs_model)
self.breadcrumbs_bar.set_model(self.breadcrumbs_model)
project_list_widget = ProjectListWidget(self)
self.conf_wrapper_layout.insertWidget(0, project_list_widget, 0)
project_list_widget.project_changed.connect(self._on_project_change)
project_list_widget.version_change_requested.connect(
self._on_source_version_change
)
self.project_list_widget = project_list_widget
def get_project_names(self):
if (
self.modify_defaults_checkbox
and self.modify_defaults_checkbox.isChecked()
):
if self.is_modifying_defaults:
return []
return self.project_list_widget.get_project_names()
@ -684,6 +827,10 @@ class ProjectWidget(SettingsCategoryWidget):
if self is saved_tab_widget:
return
def _on_context_version_trigger(self, version):
self.project_list_widget.select_project(None)
super(ProjectWidget, self)._on_context_version_trigger(version)
def _on_reset_start(self):
self.project_list_widget.refresh()
@ -696,32 +843,29 @@ class ProjectWidget(SettingsCategoryWidget):
super(ProjectWidget, self)._on_reset_success()
def _set_enabled_project_list(self, enabled):
if (
enabled
and self.modify_defaults_checkbox
and self.modify_defaults_checkbox.isChecked()
):
if enabled and self.is_modifying_defaults:
enabled = False
if self.project_list_widget.isEnabled() != enabled:
self.project_list_widget.setEnabled(enabled)
def _create_root_entity(self):
self.entity = ProjectSettings(change_state=False)
self.entity.on_change_callbacks.append(self._on_entity_change)
entity = ProjectSettings(
change_state=False, source_version=self._use_version
)
entity.on_change_callbacks.append(self._on_entity_change)
self.project_list_widget.set_entity(entity)
self.entity = entity
try:
if (
self.modify_defaults_checkbox
and self.modify_defaults_checkbox.isChecked()
):
if self.is_modifying_defaults:
self.entity.set_defaults_state()
elif self.project_name is None:
self.entity.set_studio_state()
elif self.project_name == self.entity.project_name:
self.entity.set_project_state()
else:
self.entity.change_project(self.project_name)
self.entity.change_project(
self.project_name, self._use_version
)
if self.modify_defaults_checkbox:
self.modify_defaults_checkbox.setEnabled(True)
@ -754,7 +898,7 @@ class ProjectWidget(SettingsCategoryWidget):
self.set_state(CategoryState.Idle)
def _on_modify_defaults(self):
if self.modify_defaults_checkbox.isChecked():
if self.is_modifying_defaults:
self._set_enabled_project_list(False)
if not self.entity.is_in_defaults_state():
self.reset()

View file

@ -5,6 +5,7 @@ DEFAULT_PROJECT_LABEL = "< Default >"
PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1
PROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 2
PROJECT_IS_SELECTED_ROLE = QtCore.Qt.UserRole + 3
PROJECT_VERSION_ROLE = QtCore.Qt.UserRole + 4
__all__ = (
@ -12,5 +13,6 @@ __all__ = (
"PROJECT_NAME_ROLE",
"PROJECT_IS_ACTIVE_ROLE",
"PROJECT_IS_SELECTED_ROLE"
"PROJECT_IS_SELECTED_ROLE",
"PROJECT_VERSION_ROLE",
)

View file

@ -1,5 +1,6 @@
import os
import copy
import uuid
from Qt import QtWidgets, QtCore, QtGui
from avalon.vendor import qtawesome
from avalon.mongodb import (
@ -12,8 +13,12 @@ from openpype.tools.utils.widgets import ImageButton
from openpype.tools.utils.lib import paint_image_with_color
from openpype.widgets.nice_checkbox import NiceCheckbox
from openpype.tools.utils import PlaceholderLineEdit
from openpype.settings.lib import get_system_settings
from openpype.tools.utils import (
PlaceholderLineEdit,
DynamicQThread
)
from openpype.settings.lib import find_closest_version_for_projects
from openpype.lib import get_openpype_version
from .images import (
get_pixmap,
get_image
@ -21,11 +26,40 @@ from .images import (
from .constants import (
DEFAULT_PROJECT_LABEL,
PROJECT_NAME_ROLE,
PROJECT_VERSION_ROLE,
PROJECT_IS_ACTIVE_ROLE,
PROJECT_IS_SELECTED_ROLE
)
class SettingsTabWidget(QtWidgets.QTabWidget):
context_menu_requested = QtCore.Signal(int)
def __init__(self, *args, **kwargs):
super(SettingsTabWidget, self).__init__(*args, **kwargs)
self._right_click_tab_idx = None
def mousePressEvent(self, event):
super(SettingsTabWidget, self).mousePressEvent(event)
if event.button() == QtCore.Qt.RightButton:
tab_bar = self.tabBar()
pos = tab_bar.mapFromGlobal(event.globalPos())
tab_idx = tab_bar.tabAt(pos)
if tab_idx < 0:
tab_idx = None
self._right_click_tab_idx = tab_idx
def mouseReleaseEvent(self, event):
super(SettingsTabWidget, self).mouseReleaseEvent(event)
if event.button() == QtCore.Qt.RightButton:
tab_bar = self.tabBar()
pos = tab_bar.mapFromGlobal(event.globalPos())
tab_idx = tab_bar.tabAt(pos)
if tab_idx == self._right_click_tab_idx:
self.context_menu_requested.emit(tab_idx)
self._right_click_tab = None
class CompleterFilter(QtCore.QSortFilterProxyModel):
def __init__(self, *args, **kwargs):
super(CompleterFilter, self).__init__(*args, **kwargs)
@ -603,7 +637,7 @@ class UnsavedChangesDialog(QtWidgets.QDialog):
message = "You have unsaved changes. What do you want to do with them?"
def __init__(self, parent=None):
super().__init__(parent)
super(UnsavedChangesDialog, self).__init__(parent)
message_label = QtWidgets.QLabel(self.message)
btns_widget = QtWidgets.QWidget(self)
@ -735,19 +769,65 @@ class SettingsNiceCheckbox(NiceCheckbox):
class ProjectModel(QtGui.QStandardItemModel):
_update_versions = QtCore.Signal()
def __init__(self, only_active, *args, **kwargs):
super(ProjectModel, self).__init__(*args, **kwargs)
self.setColumnCount(2)
self.dbcon = None
self._only_active = only_active
self._default_item = None
self._items_by_name = {}
self._versions_by_project = {}
colors = get_objected_colors()
font_color = colors["font"].get_qcolor()
font_color.setAlpha(67)
self._version_font_color = font_color
self._current_version = get_openpype_version()
self._version_refresh_threads = []
self._version_refresh_id = None
self._update_versions.connect(self._on_update_versions_signal)
def _on_update_versions_signal(self):
for project_name, version in self._versions_by_project.items():
if project_name is None:
item = self._default_item
else:
item = self._items_by_name.get(project_name)
if item and version != self._current_version:
item.setData(version, PROJECT_VERSION_ROLE)
def _fetch_settings_versions(self):
"""Used versions per project are loaded in thread to not stuck UI."""
version_refresh_id = self._version_refresh_id
all_project_names = list(self._items_by_name.keys())
all_project_names.append(None)
closest_by_project_name = find_closest_version_for_projects(
all_project_names
)
if self._version_refresh_id == version_refresh_id:
self._versions_by_project = closest_by_project_name
self._update_versions.emit()
def flags(self, index):
if index.column() == 1:
index = self.index(index.row(), 0, index.parent())
return super(ProjectModel, self).flags(index)
def set_dbcon(self, dbcon):
self.dbcon = dbcon
def refresh(self):
# Change id of versions refresh
self._version_refresh_id = uuid.uuid4()
new_items = []
if self._default_item is None:
item = QtGui.QStandardItem(DEFAULT_PROJECT_LABEL)
@ -757,6 +837,7 @@ class ProjectModel(QtGui.QStandardItemModel):
new_items.append(item)
self._default_item = item
self._default_item.setData("", PROJECT_VERSION_ROLE)
project_names = set()
if self.dbcon is not None:
for project_doc in self.dbcon.projects(
@ -776,6 +857,7 @@ class ProjectModel(QtGui.QStandardItemModel):
is_active = project_doc.get("data", {}).get("active", True)
item.setData(project_name, PROJECT_NAME_ROLE)
item.setData(is_active, PROJECT_IS_ACTIVE_ROLE)
item.setData("", PROJECT_VERSION_ROLE)
item.setData(False, PROJECT_IS_SELECTED_ROLE)
if not is_active:
@ -792,15 +874,87 @@ class ProjectModel(QtGui.QStandardItemModel):
if new_items:
root_item.appendRows(new_items)
# Fetch versions per project in thread
thread = DynamicQThread(self._fetch_settings_versions)
self._version_refresh_threads.append(thread)
thread.start()
class ProjectListView(QtWidgets.QListView):
# Cleanup done threads
for thread in tuple(self._version_refresh_threads):
if thread.isFinished():
self._version_refresh_threads.remove(thread)
def data(self, index, role=QtCore.Qt.DisplayRole):
if index.column() == 1:
if role == QtCore.Qt.TextAlignmentRole:
return QtCore.Qt.AlignRight
if role == QtCore.Qt.ForegroundRole:
return self._version_font_color
index = self.index(index.row(), 0, index.parent())
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
role = PROJECT_VERSION_ROLE
return super(ProjectModel, self).data(index, role)
def setData(self, index, value, role=QtCore.Qt.EditRole):
if index.column() == 1:
index = self.index(index.row(), 0, index.parent())
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
role = PROJECT_VERSION_ROLE
return super(ProjectModel, self).setData(index, value, role)
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
if section == 0:
return "Project name"
elif section == 1:
return "Used version"
return ""
return super(ProjectModel, self).headerData(
section, orientation, role
)
class VersionAction(QtWidgets.QAction):
version_triggered = QtCore.Signal(str)
def __init__(self, version, *args, **kwargs):
super(VersionAction, self).__init__(version, *args, **kwargs)
self._version = version
self.triggered.connect(self._on_trigger)
def _on_trigger(self):
self.version_triggered.emit(self._version)
class ProjectView(QtWidgets.QTreeView):
left_mouse_released_at = QtCore.Signal(QtCore.QModelIndex)
right_mouse_released_at = QtCore.Signal(QtCore.QModelIndex)
def __init__(self, *args, **kwargs):
super(ProjectView, self).__init__(*args, **kwargs)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.setIndentation(0)
# Do not allow editing
self.setEditTriggers(
QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers
)
# Do not automatically handle selection
self.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
def mouseReleaseEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
index = self.indexAt(event.pos())
self.left_mouse_released_at.emit(index)
super(ProjectListView, self).mouseReleaseEvent(event)
elif event.button() == QtCore.Qt.RightButton:
index = self.indexAt(event.pos())
self.right_mouse_released_at.emit(index)
super(ProjectView, self).mouseReleaseEvent(event)
class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):
@ -846,18 +1000,21 @@ class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):
class ProjectListWidget(QtWidgets.QWidget):
project_changed = QtCore.Signal()
version_change_requested = QtCore.Signal(str)
def __init__(self, parent, only_active=False):
self._parent = parent
self._entity = None
self.current_project = None
super(ProjectListWidget, self).__init__(parent)
self.setObjectName("ProjectListWidget")
label_widget = QtWidgets.QLabel("Projects")
content_frame = QtWidgets.QFrame(self)
content_frame.setObjectName("ProjectListContentWidget")
project_list = ProjectListView(self)
project_list = ProjectView(content_frame)
project_model = ProjectModel(only_active)
project_proxy = ProjectSortFilterProxy()
@ -865,33 +1022,37 @@ class ProjectListWidget(QtWidgets.QWidget):
project_proxy.setSourceModel(project_model)
project_list.setModel(project_proxy)
# Do not allow editing
project_list.setEditTriggers(
QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers
)
# Do not automatically handle selection
project_list.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
content_layout = QtWidgets.QVBoxLayout(content_frame)
content_layout.setContentsMargins(0, 0, 0, 0)
content_layout.setSpacing(0)
content_layout.addWidget(project_list, 1)
layout = QtWidgets.QVBoxLayout(self)
layout.setSpacing(3)
layout.addWidget(label_widget, 0)
layout.addWidget(project_list, 1)
inactive_chk = None
if not only_active:
checkbox_wrapper = QtWidgets.QWidget(content_frame)
checkbox_wrapper.setAttribute(QtCore.Qt.WA_TranslucentBackground)
if only_active:
inactive_chk = None
else:
inactive_chk = QtWidgets.QCheckBox(" Show Inactive Projects ")
inactive_chk = QtWidgets.QCheckBox(
"Show Inactive Projects", checkbox_wrapper
)
inactive_chk.setChecked(not project_proxy.is_filter_enabled())
layout.addSpacing(5)
layout.addWidget(inactive_chk, 0)
layout.addSpacing(5)
wrapper_layout = QtWidgets.QHBoxLayout(checkbox_wrapper)
wrapper_layout.addWidget(inactive_chk, 1)
content_layout.addWidget(checkbox_wrapper, 0)
inactive_chk.stateChanged.connect(self.on_inactive_vis_changed)
project_list.left_mouse_released_at.connect(self.on_item_clicked)
layout = QtWidgets.QVBoxLayout(self)
# Margins '3' are matching to configurables widget scroll area on right
layout.setContentsMargins(5, 3, 3, 3)
layout.addWidget(content_frame, 1)
self._default_project_item = None
project_list.left_mouse_released_at.connect(self.on_item_clicked)
project_list.right_mouse_released_at.connect(
self._on_item_right_clicked
)
self.project_list = project_list
self.project_proxy = project_proxy
@ -900,10 +1061,46 @@ class ProjectListWidget(QtWidgets.QWidget):
self.dbcon = None
def on_item_clicked(self, new_index):
new_project_name = new_index.data(QtCore.Qt.DisplayRole)
if new_project_name is None:
def set_entity(self, entity):
self._entity = entity
def _on_item_right_clicked(self, index):
if not index.isValid():
return
project_name = index.data(PROJECT_NAME_ROLE)
if project_name is None:
project_name = DEFAULT_PROJECT_LABEL
if self.current_project != project_name:
self.on_item_clicked(index)
if self.current_project != project_name:
return
if not self._entity:
return
versions = self._entity.get_available_source_versions(sorted=True)
if not versions:
return
menu = QtWidgets.QMenu(self)
submenu = QtWidgets.QMenu("Use settings from version", menu)
for version in reversed(versions):
action = VersionAction(version, submenu)
action.version_triggered.connect(
self.version_change_requested
)
submenu.addAction(action)
menu.addMenu(submenu)
menu.exec_(QtGui.QCursor.pos())
def on_item_clicked(self, new_index):
if not new_index.isValid():
return
new_project_name = new_index.data(PROJECT_NAME_ROLE)
if new_project_name is None:
new_project_name = DEFAULT_PROJECT_LABEL
if self.current_project == new_project_name:
return
@ -963,12 +1160,30 @@ class ProjectListWidget(QtWidgets.QWidget):
index = model.indexFromItem(found_items[0])
model.setData(index, True, PROJECT_IS_SELECTED_ROLE)
index = proxy.mapFromSource(index)
src_indexes = []
col_count = model.columnCount()
if col_count > 1:
for col in range(col_count):
src_indexes.append(
model.index(index.row(), col, index.parent())
)
dst_indexes = []
for index in src_indexes:
dst_indexes.append(proxy.mapFromSource(index))
self.project_list.selectionModel().clear()
self.project_list.selectionModel().setCurrentIndex(
index, QtCore.QItemSelectionModel.SelectionFlag.SelectCurrent
)
selection_model = self.project_list.selectionModel()
selection_model.clear()
first = True
for index in dst_indexes:
if first:
selection_model.setCurrentIndex(
index,
QtCore.QItemSelectionModel.SelectionFlag.SelectCurrent
)
first = False
continue
selection_model.select(index, QtCore.QItemSelectionModel.Select)
def get_project_names(self):
output = []
@ -980,7 +1195,7 @@ class ProjectListWidget(QtWidgets.QWidget):
def refresh(self):
selected_project = None
for index in self.project_list.selectedIndexes():
selected_project = index.data(QtCore.Qt.DisplayRole)
selected_project = index.data(PROJECT_NAME_ROLE)
break
mongo_url = os.environ["OPENPYPE_MONGO"]
@ -1008,5 +1223,6 @@ class ProjectListWidget(QtWidgets.QWidget):
self.select_project(selected_project)
self.current_project = self.project_list.currentIndex().data(
QtCore.Qt.DisplayRole
PROJECT_NAME_ROLE
)
self.project_list.resizeColumnToContents(0)

View file

@ -4,7 +4,11 @@ from .categories import (
SystemWidget,
ProjectWidget
)
from .widgets import ShadowWidget, RestartDialog
from .widgets import (
ShadowWidget,
RestartDialog,
SettingsTabWidget
)
from openpype import style
from openpype.lib import is_admin_password_required
@ -34,7 +38,7 @@ class MainWidget(QtWidgets.QWidget):
self.setStyleSheet(stylesheet)
self.setWindowIcon(QtGui.QIcon(style.app_icon_path()))
header_tab_widget = QtWidgets.QTabWidget(parent=self)
header_tab_widget = SettingsTabWidget(parent=self)
studio_widget = SystemWidget(user_role, header_tab_widget)
project_widget = ProjectWidget(user_role, header_tab_widget)
@ -65,6 +69,10 @@ class MainWidget(QtWidgets.QWidget):
)
tab_widget.full_path_requested.connect(self._on_full_path_request)
header_tab_widget.context_menu_requested.connect(
self._on_context_menu_request
)
self._header_tab_widget = header_tab_widget
self.tab_widgets = tab_widgets
@ -100,6 +108,18 @@ class MainWidget(QtWidgets.QWidget):
tab_widget.set_category_path(category, path)
break
def _on_context_menu_request(self, tab_idx):
widget = self._header_tab_widget.widget(tab_idx)
if not widget:
return
menu = QtWidgets.QMenu(self)
widget.add_context_actions(menu)
if menu.actions():
result = menu.exec_(QtGui.QCursor.pos())
if result is not None:
self._header_tab_widget.setCurrentIndex(tab_idx)
def showEvent(self, event):
super(MainWidget, self).showEvent(event)
if self._reset_on_show:

View file

@ -33,7 +33,8 @@ from openpype.settings import (
)
from openpype.tools.utils import (
WrappedCallbackItem,
paint_image_with_color
paint_image_with_color,
get_warning_pixmap
)
from .pype_info_widget import PypeInfoWidget
@ -76,7 +77,7 @@ class PixmapLabel(QtWidgets.QLabel):
super(PixmapLabel, self).resizeEvent(event)
class VersionDialog(QtWidgets.QDialog):
class VersionUpdateDialog(QtWidgets.QDialog):
restart_requested = QtCore.Signal()
ignore_requested = QtCore.Signal()
@ -84,7 +85,7 @@ class VersionDialog(QtWidgets.QDialog):
_min_height = 130
def __init__(self, parent=None):
super(VersionDialog, self).__init__(parent)
super(VersionUpdateDialog, self).__init__(parent)
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
self.setWindowIcon(icon)
@ -152,11 +153,11 @@ class VersionDialog(QtWidgets.QDialog):
)
def showEvent(self, event):
super().showEvent(event)
super(VersionUpdateDialog, self).showEvent(event)
self._restart_accepted = False
def closeEvent(self, event):
super().closeEvent(event)
super(VersionUpdateDialog, self).closeEvent(event)
if self._restart_accepted or self._current_is_higher:
return
# Trigger ignore requested only if restart was not clicked and current
@ -202,6 +203,63 @@ class VersionDialog(QtWidgets.QDialog):
self.accept()
class BuildVersionDialog(QtWidgets.QDialog):
"""Build/Installation version is too low for current OpenPype version.
This dialog tells to user that it's build OpenPype is too old.
"""
def __init__(self, parent=None):
super(BuildVersionDialog, self).__init__(parent)
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
self.setWindowIcon(icon)
self.setWindowTitle("Outdated OpenPype installation")
self.setWindowFlags(
self.windowFlags()
| QtCore.Qt.WindowStaysOnTopHint
)
top_widget = QtWidgets.QWidget(self)
warning_pixmap = get_warning_pixmap()
warning_icon_label = PixmapLabel(warning_pixmap, top_widget)
message = (
"Your installation of OpenPype <b>does not match minimum"
" requirements</b>.<br/><br/>Please update OpenPype installation"
" to newer version."
)
content_label = QtWidgets.QLabel(message, self)
top_layout = QtWidgets.QHBoxLayout(top_widget)
top_layout.setContentsMargins(0, 0, 0, 0)
top_layout.addWidget(
warning_icon_label, 0,
QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter
)
top_layout.addWidget(content_label, 1)
footer_widget = QtWidgets.QWidget(self)
ok_btn = QtWidgets.QPushButton("I understand", footer_widget)
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
footer_layout.setContentsMargins(0, 0, 0, 0)
footer_layout.addStretch(1)
footer_layout.addWidget(ok_btn)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addWidget(top_widget, 0)
main_layout.addStretch(1)
main_layout.addWidget(footer_widget, 0)
self.setStyleSheet(style.load_stylesheet())
ok_btn.clicked.connect(self._on_ok_clicked)
def _on_ok_clicked(self):
self.close()
class TrayManager:
"""Cares about context of application.
@ -272,7 +330,7 @@ class TrayManager:
return
if self._version_dialog is None:
self._version_dialog = VersionDialog()
self._version_dialog = VersionUpdateDialog()
self._version_dialog.restart_requested.connect(
self._restart_and_install
)
@ -383,6 +441,10 @@ class TrayManager:
self._validate_settings_defaults()
if not op_version_control_available():
dialog = BuildVersionDialog()
dialog.exec_()
def _validate_settings_defaults(self):
valid = True
try:

View file

@ -10,7 +10,10 @@ from .widgets import (
from .error_dialog import ErrorMessageBox
from .lib import (
WrappedCallbackItem,
paint_image_with_color
paint_image_with_color,
get_warning_pixmap,
set_style_property,
DynamicQThread,
)
from .models import (
@ -29,6 +32,9 @@ __all__ = (
"WrappedCallbackItem",
"paint_image_with_color",
"get_warning_pixmap",
"set_style_property",
"DynamicQThread",
"RecursiveSortFilterProxyModel",
)

View file

@ -7,7 +7,6 @@ import Qt
from Qt import QtWidgets, QtGui, QtCore
from avalon.lib import HeroVersionType
from openpype.style import get_objected_colors
from .models import TreeModel
from . import lib

View file

@ -14,6 +14,8 @@ from openpype.api import (
Logger
)
from openpype.lib import filter_profiles
from openpype.style import get_objected_colors
from openpype.resources import get_image_path
def center_window(window):
@ -28,6 +30,18 @@ def center_window(window):
window.move(geo.topLeft())
def set_style_property(widget, property_name, property_value):
"""Set widget's property that may affect style.
If current property value is different then style of widget is polished.
"""
cur_value = widget.property(property_name)
if cur_value == property_value:
return
widget.setProperty(property_name, property_value)
widget.style().polish(widget)
def paint_image_with_color(image, color):
"""Redraw image with single color using it's alpha.
@ -670,3 +684,19 @@ class WrappedCallbackItem:
finally:
self._done = True
def get_warning_pixmap(color=None):
"""Warning icon as QPixmap.
Args:
color(QtGui.QColor): Color that will be used to paint warning icon.
"""
src_image_path = get_image_path("warning.png")
src_image = QtGui.QImage(src_image_path)
if color is None:
colors = get_objected_colors()
color_value = colors["delete-btn-bg"]
color = color_value.get_qcolor()
return paint_image_with_color(src_image, color)

View file

@ -3,9 +3,6 @@ import logging
import Qt
from Qt import QtCore, QtGui
from avalon.vendor import qtawesome
from avalon import style, io
from . import lib
from .constants import (
PROJECT_IS_ACTIVE_ROLE,
PROJECT_NAME_ROLE,

View file

@ -21,14 +21,13 @@ from openpype.tools.utils.tasks_widget import TasksWidget
from openpype.tools.utils.delegates import PrettyTimeDelegate
from openpype.lib import (
Anatomy,
get_workdir,
get_workfile_doc,
create_workfile_doc,
save_workfile_data_to_doc,
get_workfile_template_key,
create_workdir_extra_folders
create_workdir_extra_folders,
get_system_general_anatomy_data
)
from .model import FilesModel
from .view import FilesView
@ -110,6 +109,10 @@ class NameWindow(QtWidgets.QDialog):
"ext": None
}
# add system general settings anatomy data
system_general_data = get_system_general_anatomy_data()
self.data.update(system_general_data)
# Store project anatomy
self.anatomy = anatomy
self.template = anatomy.templates[template_key]["file"]

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.8.2"
__version__ = "3.8.3-nightly.1"

View file

@ -132,12 +132,12 @@ class Popup2(Popup):
"""
parent_widget = self.parent()
app = QtWidgets.QApplication.instance()
desktop = QtWidgets.QApplication.desktop()
if parent_widget:
screen = app.desktop().screenNumber(parent_widget)
screen = desktop.screenNumber(parent_widget)
else:
screen = app.desktop().screenNumber(app.desktop().cursor().pos())
center_point = app.desktop().screenGeometry(screen).center()
screen = desktop.screenNumber(desktop.cursor().pos())
center_point = desktop.screenGeometry(screen).center()
frame_geo = self.frameGeometry()
frame_geo.moveCenter(center_point)

View file

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

View file

@ -85,8 +85,8 @@ openpype_console eventserver --ftrack-url=<url> --ftrack-user=<user> --ftrack-ap
| `--asset` | Asset name (default taken from `AVALON_ASSET` if set) |
| `--task` | Task name (default taken from `AVALON_TASK` is set) |
| `--tools` | *Optional: Additional tools to add* |
| `--user` | *Optional: User on behalf to run* |
| `--ftrack-server` / `-fs` | *Optional: Ftrack server URL* |
| `--user` | *Optional: User on behalf to run* |
| `--ftrack-server` / `-fs` | *Optional: Ftrack server URL* |
| `--ftrack-user` / `-fu` | *Optional: Ftrack user* |
| `--ftrack-key` / `-fk` | *Optional: Ftrack API key* |
@ -166,3 +166,6 @@ Takes path to unzipped and possibly modified OpenPype version. Files will be
zipped, checksums recalculated and version will be determined by folder name
(and written to `version.py`).
```shell
./openpype_console repack-version /path/to/some/modified/unzipped/version/openpype-v3.8.3-modified
```