Merge branch 'develop' into feature/OP-1933_nuke-toggle-baking-colorspace

This commit is contained in:
Jakub Jezek 2021-11-18 10:54:05 +01:00
commit 899ba680fa
No known key found for this signature in database
GPG key ID: D8548FBF690B100A
32 changed files with 3434 additions and 623 deletions

View file

@ -33,7 +33,7 @@ jobs:
id: version
if: steps.version_type.outputs.type != 'skip'
run: |
RESULT=$(python ./tools/ci_tools.py --nightly)
RESULT=$(python ./tools/ci_tools.py --nightly --github_token ${{ secrets.GITHUB_TOKEN }})
echo ::set-output name=next_tag::$RESULT

View file

@ -1,18 +1,51 @@
# Changelog
## [3.6.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD)
## [3.6.2-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.5.0...HEAD)
**🆕 New features**
- Maya : Colorspace configuration [\#2170](https://github.com/pypeclub/OpenPype/pull/2170)
- Blender: Added support for audio [\#2168](https://github.com/pypeclub/OpenPype/pull/2168)
- Flame: a host basic integration [\#2165](https://github.com/pypeclub/OpenPype/pull/2165)
- Houdini: simple HDA workflow [\#2072](https://github.com/pypeclub/OpenPype/pull/2072)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.1...HEAD)
**🚀 Enhancements**
- Tools: SceneInventory in OpenPype [\#2255](https://github.com/pypeclub/OpenPype/pull/2255)
- Tools: Tasks widget [\#2251](https://github.com/pypeclub/OpenPype/pull/2251)
- Added endpoint for configured extensions [\#2221](https://github.com/pypeclub/OpenPype/pull/2221)
**🐛 Bug fixes**
- Burnins: Support mxf metadata [\#2247](https://github.com/pypeclub/OpenPype/pull/2247)
## [3.6.1](https://github.com/pypeclub/OpenPype/tree/3.6.1) (2021-11-16)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.1-nightly.1...3.6.1)
**🐛 Bug fixes**
- Loader doesn't allow changing of version before loading [\#2254](https://github.com/pypeclub/OpenPype/pull/2254)
## [3.6.0](https://github.com/pypeclub/OpenPype/tree/3.6.0) (2021-11-15)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.0-nightly.6...3.6.0)
### 📖 Documentation
- Add alternative sites for Site Sync [\#2206](https://github.com/pypeclub/OpenPype/pull/2206)
- Add command line way of running site sync server [\#2188](https://github.com/pypeclub/OpenPype/pull/2188)
**🆕 New features**
- Add validate active site button to sync queue on a project [\#2176](https://github.com/pypeclub/OpenPype/pull/2176)
- Maya : Colorspace configuration [\#2170](https://github.com/pypeclub/OpenPype/pull/2170)
- Blender: Added support for audio [\#2168](https://github.com/pypeclub/OpenPype/pull/2168)
**🚀 Enhancements**
- Tools: Subset manager in OpenPype [\#2243](https://github.com/pypeclub/OpenPype/pull/2243)
- General: Skip module directories without init file [\#2239](https://github.com/pypeclub/OpenPype/pull/2239)
- General: Static interfaces [\#2238](https://github.com/pypeclub/OpenPype/pull/2238)
- Style: Fix transparent image in style [\#2235](https://github.com/pypeclub/OpenPype/pull/2235)
- Add a "following workfile versioning" option on publish [\#2225](https://github.com/pypeclub/OpenPype/pull/2225)
- Modules: Module can add cli commands [\#2224](https://github.com/pypeclub/OpenPype/pull/2224)
- Webpublisher: Separate webpublisher logic [\#2222](https://github.com/pypeclub/OpenPype/pull/2222)
- Add both side availability on Site Sync sites to Loader [\#2220](https://github.com/pypeclub/OpenPype/pull/2220)
- Tools: Center loader and library loader on show [\#2219](https://github.com/pypeclub/OpenPype/pull/2219)
- Maya : Validate shape zero [\#2212](https://github.com/pypeclub/OpenPype/pull/2212)
@ -21,65 +54,46 @@
- Ftrack: Replace Queue with deque in event handlers logic [\#2204](https://github.com/pypeclub/OpenPype/pull/2204)
- Tools: New select context dialog [\#2200](https://github.com/pypeclub/OpenPype/pull/2200)
- Maya : Validate mesh ngons [\#2199](https://github.com/pypeclub/OpenPype/pull/2199)
- Dirmap in Nuke [\#2198](https://github.com/pypeclub/OpenPype/pull/2198)
- Delivery: Check 'frame' key in template for sequence delivery [\#2196](https://github.com/pypeclub/OpenPype/pull/2196)
- Settings: Site sync project settings improvement [\#2193](https://github.com/pypeclub/OpenPype/pull/2193)
- Usage of tools code [\#2185](https://github.com/pypeclub/OpenPype/pull/2185)
- Settings: Dictionary based on project roots [\#2184](https://github.com/pypeclub/OpenPype/pull/2184)
- Subset name: Be able to pass asset document to get subset name [\#2179](https://github.com/pypeclub/OpenPype/pull/2179)
- Tools: Experimental tools [\#2167](https://github.com/pypeclub/OpenPype/pull/2167)
- Loader: Refactor and use OpenPype stylesheets [\#2166](https://github.com/pypeclub/OpenPype/pull/2166)
- Add loader for linked smart objects in photoshop [\#2149](https://github.com/pypeclub/OpenPype/pull/2149)
**🐛 Bug fixes**
- Ftrack: Sync project ftrack id cache issue [\#2250](https://github.com/pypeclub/OpenPype/pull/2250)
- Ftrack: Session creation and Prepare project [\#2245](https://github.com/pypeclub/OpenPype/pull/2245)
- Added queue for studio processing in PS [\#2237](https://github.com/pypeclub/OpenPype/pull/2237)
- Python 2: Unicode to string conversion [\#2236](https://github.com/pypeclub/OpenPype/pull/2236)
- Fix - enum for color coding in PS [\#2234](https://github.com/pypeclub/OpenPype/pull/2234)
- Pyblish Tool: Fix targets handling [\#2232](https://github.com/pypeclub/OpenPype/pull/2232)
- Ftrack: Base event fix of 'get\_project\_from\_entity' method [\#2214](https://github.com/pypeclub/OpenPype/pull/2214)
- Maya : multiple subsets review broken [\#2210](https://github.com/pypeclub/OpenPype/pull/2210)
- Fix - different command used for Linux and Mac OS [\#2207](https://github.com/pypeclub/OpenPype/pull/2207)
- Tools: Workfiles tool don't use avalon widgets [\#2205](https://github.com/pypeclub/OpenPype/pull/2205)
- Ftrack: Fill missing ftrack id on mongo project [\#2203](https://github.com/pypeclub/OpenPype/pull/2203)
- Project Manager: Fix copying of tasks [\#2191](https://github.com/pypeclub/OpenPype/pull/2191)
- StandalonePublisher: Source validator don't expect representations [\#2190](https://github.com/pypeclub/OpenPype/pull/2190)
- Blender: Fix trying to pack an image when the shader node has no texture [\#2183](https://github.com/pypeclub/OpenPype/pull/2183)
- MacOS: Launching of applications may cause Permissions error [\#2175](https://github.com/pypeclub/OpenPype/pull/2175)
- Maya: review viewport settings [\#2177](https://github.com/pypeclub/OpenPype/pull/2177)
- Maya: Aspect ratio [\#2174](https://github.com/pypeclub/OpenPype/pull/2174)
- Blender: Fix 'Deselect All' with object not in 'Object Mode' [\#2163](https://github.com/pypeclub/OpenPype/pull/2163)
- Maya: Fix hotbox broken by scriptsmenu [\#2151](https://github.com/pypeclub/OpenPype/pull/2151)
- Added validator for source files for Standalone Publisher [\#2138](https://github.com/pypeclub/OpenPype/pull/2138)
**Merged pull requests:**
- Settings: Site sync project settings improvement [\#2193](https://github.com/pypeclub/OpenPype/pull/2193)
- Add validate active site button to sync queue on a project [\#2176](https://github.com/pypeclub/OpenPype/pull/2176)
- Bump pillow from 8.2.0 to 8.3.2 [\#2162](https://github.com/pypeclub/OpenPype/pull/2162)
## [3.5.0](https://github.com/pypeclub/OpenPype/tree/3.5.0) (2021-10-17)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.5.0-nightly.8...3.5.0)
**Deprecated:**
- Maya: Change mayaAscii family to mayaScene [\#2106](https://github.com/pypeclub/OpenPype/pull/2106)
**🆕 New features**
- Added project and task into context change message in Maya [\#2131](https://github.com/pypeclub/OpenPype/pull/2131)
- Add ExtractBurnin to photoshop review [\#2124](https://github.com/pypeclub/OpenPype/pull/2124)
- PYPE-1218 - changed namespace to contain subset name in Maya [\#2114](https://github.com/pypeclub/OpenPype/pull/2114)
- Added running configurable disk mapping command before start of OP [\#2091](https://github.com/pypeclub/OpenPype/pull/2091)
- SFTP provider [\#2073](https://github.com/pypeclub/OpenPype/pull/2073)
**🚀 Enhancements**
- Maya: make rig validators configurable in settings [\#2137](https://github.com/pypeclub/OpenPype/pull/2137)
- Settings: Updated readme for entity types in settings [\#2132](https://github.com/pypeclub/OpenPype/pull/2132)
- Nuke: unified clip loader [\#2128](https://github.com/pypeclub/OpenPype/pull/2128)
- Settings UI: Project model refreshing and sorting [\#2104](https://github.com/pypeclub/OpenPype/pull/2104)
- Create Read From Rendered - Disable Relative paths by default [\#2093](https://github.com/pypeclub/OpenPype/pull/2093)
- Added choosing different dirmap mapping if workfile synched locally [\#2088](https://github.com/pypeclub/OpenPype/pull/2088)
- General: Remove IdleManager module [\#2084](https://github.com/pypeclub/OpenPype/pull/2084)
- Tray UI: Message box about missing settings defaults [\#2080](https://github.com/pypeclub/OpenPype/pull/2080)
- Tray UI: Show menu where first click happened [\#2079](https://github.com/pypeclub/OpenPype/pull/2079)
- Global: add global validators to settings [\#2078](https://github.com/pypeclub/OpenPype/pull/2078)
- Use CRF for burnin when available [\#2070](https://github.com/pypeclub/OpenPype/pull/2070)
- Project manager: Filter first item after selection of project [\#2069](https://github.com/pypeclub/OpenPype/pull/2069)
**🐛 Bug fixes**
@ -87,24 +101,6 @@
- Fix - oiiotool wasn't recognized even if present [\#2129](https://github.com/pypeclub/OpenPype/pull/2129)
- General: Disk mapping group [\#2120](https://github.com/pypeclub/OpenPype/pull/2120)
- Hiero: publishing effect first time makes wrong resources path [\#2115](https://github.com/pypeclub/OpenPype/pull/2115)
- Add startup script for Houdini Core. [\#2110](https://github.com/pypeclub/OpenPype/pull/2110)
- TVPaint: Behavior name of loop also accept repeat [\#2109](https://github.com/pypeclub/OpenPype/pull/2109)
- Ftrack: Project settings save custom attributes skip unknown attributes [\#2103](https://github.com/pypeclub/OpenPype/pull/2103)
- Blender: Fix NoneType error when animation\_data is missing for a rig [\#2101](https://github.com/pypeclub/OpenPype/pull/2101)
- Fix broken import in sftp provider [\#2100](https://github.com/pypeclub/OpenPype/pull/2100)
- Global: Fix docstring on publish plugin extract review [\#2097](https://github.com/pypeclub/OpenPype/pull/2097)
- Delivery Action Files Sequence fix [\#2096](https://github.com/pypeclub/OpenPype/pull/2096)
- General: Cloud mongo ca certificate issue [\#2095](https://github.com/pypeclub/OpenPype/pull/2095)
- TVPaint: Creator use context from workfile [\#2087](https://github.com/pypeclub/OpenPype/pull/2087)
- Blender: fix texture missing when publishing blend files [\#2085](https://github.com/pypeclub/OpenPype/pull/2085)
- General: Startup validations oiio tool path fix on linux [\#2083](https://github.com/pypeclub/OpenPype/pull/2083)
- Deadline: Collect deadline server does not check existence of deadline key [\#2082](https://github.com/pypeclub/OpenPype/pull/2082)
- Blender: fixed Curves with modifiers in Rigs [\#2081](https://github.com/pypeclub/OpenPype/pull/2081)
- Nuke UI scaling [\#2077](https://github.com/pypeclub/OpenPype/pull/2077)
**Merged pull requests:**
- Bump pywin32 from 300 to 301 [\#2086](https://github.com/pypeclub/OpenPype/pull/2086)
## [3.4.1](https://github.com/pypeclub/OpenPype/tree/3.4.1) (2021-09-23)

View file

@ -17,11 +17,10 @@ class ClosePS(pyblish.api.ContextPlugin):
active = True
hosts = ["photoshop"]
targets = ["remotepublish"]
def process(self, context):
self.log.info("ClosePS")
if not os.environ.get("IS_HEADLESS"):
return
stub = photoshop.stub()
self.log.info("Shutting down PS")

View file

@ -21,6 +21,7 @@ class CollectRemoteInstances(pyblish.api.ContextPlugin):
label = "Instances"
order = pyblish.api.CollectorOrder
hosts = ["photoshop"]
targets = ["remotepublish"]
# configurable by Settings
color_code_mapping = []
@ -28,9 +29,6 @@ class CollectRemoteInstances(pyblish.api.ContextPlugin):
def process(self, context):
self.log.info("CollectRemoteInstances")
self.log.info("mapping:: {}".format(self.color_code_mapping))
if not os.environ.get("IS_HEADLESS"):
self.log.debug("Not headless publishing, skipping.")
return
# parse variant if used in webpublishing, comes from webpublisher batch
batch_dir = os.environ.get("OPENPYPE_PUBLISH_DATA")

View file

@ -11,7 +11,8 @@ from avalon.api import AvalonMongoDB
from openpype.lib import OpenPypeMongoConnection
from openpype_modules.avalon_apps.rest_api import _RestApiEndpoint
from openpype.lib.plugin_tools import parse_json
from openpype.lib.remote_publish import get_task_data
from openpype.settings import get_project_settings
from openpype.lib import PypeLogger
@ -39,6 +40,8 @@ class RestApiResource:
return value.isoformat()
if isinstance(value, ObjectId):
return str(value)
if isinstance(value, set):
return list(value)
raise TypeError(value)
@classmethod
@ -205,7 +208,7 @@ class WebpublisherBatchPublishEndpoint(_RestApiEndpoint):
# Make sure targets are set to None for cases that default
# would change
# - targets argument is not used in 'remotepublishfromapp'
"targets": None
"targets": ["remotepublish"]
},
# does publish need to be handled by a queue, eg. only
# single process running concurrently?
@ -213,7 +216,7 @@ class WebpublisherBatchPublishEndpoint(_RestApiEndpoint):
}
]
batch_path = os.path.join(self.resource.upload_dir, content["batch"])
batch_dir = os.path.join(self.resource.upload_dir, content["batch"])
# Default command and arguments
command = "remotepublish"
@ -227,18 +230,9 @@ class WebpublisherBatchPublishEndpoint(_RestApiEndpoint):
add_to_queue = False
if content.get("studio_processing"):
log.info("Post processing called")
log.info("Post processing called for {}".format(batch_dir))
batch_data = parse_json(os.path.join(batch_path, "manifest.json"))
if not batch_data:
raise ValueError(
"Cannot parse batch manifest in {}".format(batch_path))
task_dir_name = batch_data["tasks"][0]
task_data = parse_json(os.path.join(batch_path, task_dir_name,
"manifest.json"))
if not task_data:
raise ValueError(
"Cannot parse task manifest in {}".format(task_data))
task_data = get_task_data(batch_dir)
for process_filter in studio_processing_filters:
filter_extensions = process_filter.get("extensions") or []
@ -257,7 +251,7 @@ class WebpublisherBatchPublishEndpoint(_RestApiEndpoint):
args = [
openpype_app,
command,
batch_path
batch_dir
]
for key, value in add_args.items():
@ -315,3 +309,36 @@ class PublishesStatusEndpoint(_RestApiEndpoint):
body=self.resource.encode(output),
content_type="application/json"
)
class ConfiguredExtensionsEndpoint(_RestApiEndpoint):
"""Returns dict of extensions which have mapping to family.
Returns:
{
"file_exts": [],
"sequence_exts": []
}
"""
async def get(self, project_name=None) -> Response:
sett = get_project_settings(project_name)
configured = {
"file_exts": set(),
"sequence_exts": set(),
# workfiles that could have "Studio Procesing" hardcoded for now
"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"])
return Response(
status=200,
body=self.resource.encode(dict(configured)),
content_type="application/json"
)

View file

@ -16,7 +16,8 @@ from .webpublish_routes import (
WebpublisherHiearchyEndpoint,
WebpublisherProjectsEndpoint,
BatchStatusEndpoint,
PublishesStatusEndpoint
PublishesStatusEndpoint,
ConfiguredExtensionsEndpoint
)
@ -54,6 +55,13 @@ def run_webserver(*args, **kwargs):
hiearchy_endpoint.dispatch
)
configured_ext_endpoint = ConfiguredExtensionsEndpoint(resource)
server_manager.add_route(
"GET",
"/api/webpublish/configured_ext/{project_name}",
configured_ext_endpoint.dispatch
)
# triggers publish
webpublisher_task_publish_endpoint = \
WebpublisherBatchPublishEndpoint(resource)

View file

@ -8,6 +8,7 @@ import pyblish.api
from openpype import uninstall
from openpype.lib.mongo import OpenPypeMongoConnection
from openpype.lib.plugin_tools import parse_json
def get_webpublish_conn():
@ -31,7 +32,8 @@ def start_webpublish_log(dbcon, batch_id, user):
"batch_id": batch_id,
"start_date": datetime.now(),
"user": user,
"status": "in_progress"
"status": "in_progress",
"progress": 0.0
}).inserted_id
@ -157,3 +159,28 @@ def _get_close_plugin(close_plugin_name, log):
return plugin
log.warning("Close plugin not found, app might not close.")
def get_task_data(batch_dir):
"""Return parsed data from first task manifest.json
Used for `remotepublishfromapp` command where batch contains only
single task with publishable workfile.
Returns:
(dict)
Throws:
(ValueError) if batch or task manifest not found or broken
"""
batch_data = parse_json(os.path.join(batch_dir, "manifest.json"))
if not batch_data:
raise ValueError(
"Cannot parse batch meta in {} folder".format(batch_dir))
task_dir_name = batch_data["tasks"][0]
task_data = parse_json(os.path.join(batch_dir, task_dir_name,
"manifest.json"))
if not task_data:
raise ValueError(
"Cannot parse batch meta in {} folder".format(task_data))
return task_data

View file

@ -194,6 +194,7 @@ class SyncToAvalonEvent(BaseEvent):
ftrack_id = proj["data"].get("ftrackId")
if ftrack_id is None:
ftrack_id = self._update_project_ftrack_id()
proj["data"]["ftrackId"] = ftrack_id
self._avalon_ents_by_ftrack_id[ftrack_id] = proj
for ent in ents:
ftrack_id = ent["data"].get("ftrackId")
@ -584,6 +585,10 @@ class SyncToAvalonEvent(BaseEvent):
continue
ftrack_id = ftrack_id[0]
# Skip deleted projects
if action == "remove" and entityType == "show":
return True
# task modified, collect parent id of task, handle separately
if entity_type.lower() == "task":
changes = ent_info.get("changes") or {}

View file

@ -27,16 +27,12 @@ class CollectUsername(pyblish.api.ContextPlugin):
order = pyblish.api.CollectorOrder - 0.488
label = "Collect ftrack username"
hosts = ["webpublisher", "photoshop"]
targets = ["remotepublish", "filespublish"]
_context = None
def process(self, context):
self.log.info("CollectUsername")
# photoshop could be triggered remotely in webpublisher fashion
if os.environ["AVALON_APP"] == "photoshop":
if not os.environ.get("IS_HEADLESS"):
self.log.debug("Regular process, skipping")
return
os.environ["FTRACK_API_USER"] = os.environ["FTRACK_BOT_API_USER"]
os.environ["FTRACK_API_KEY"] = os.environ["FTRACK_BOT_API_KEY"]

View file

@ -176,6 +176,7 @@ class PythonCodeEditor(QtWidgets.QPlainTextEdit):
class PythonTabWidget(QtWidgets.QWidget):
add_tab_requested = QtCore.Signal()
before_execute = QtCore.Signal(str)
def __init__(self, parent):
@ -185,11 +186,15 @@ class PythonTabWidget(QtWidgets.QWidget):
self.setFocusProxy(code_input)
add_tab_btn = QtWidgets.QPushButton("Add tab...", self)
add_tab_btn.setToolTip("Add new tab")
execute_btn = QtWidgets.QPushButton("Execute", self)
execute_btn.setToolTip("Execute command (Ctrl + Enter)")
btns_layout = QtWidgets.QHBoxLayout()
btns_layout.setContentsMargins(0, 0, 0, 0)
btns_layout.addWidget(add_tab_btn)
btns_layout.addStretch(1)
btns_layout.addWidget(execute_btn)
@ -198,12 +203,16 @@ class PythonTabWidget(QtWidgets.QWidget):
layout.addWidget(code_input, 1)
layout.addLayout(btns_layout, 0)
add_tab_btn.clicked.connect(self._on_add_tab_clicked)
execute_btn.clicked.connect(self._on_execute_clicked)
code_input.execute_requested.connect(self.execute)
self._code_input = code_input
self._interpreter = InteractiveInterpreter()
def _on_add_tab_clicked(self):
self.add_tab_requested.emit()
def _on_execute_clicked(self):
self.execute()
@ -352,9 +361,6 @@ class PythonInterpreterWidget(QtWidgets.QWidget):
tab_widget.setTabsClosable(False)
tab_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
add_tab_btn = QtWidgets.QPushButton("+", tab_widget)
tab_widget.setCornerWidget(add_tab_btn, QtCore.Qt.TopLeftCorner)
widgets_splitter = QtWidgets.QSplitter(self)
widgets_splitter.setOrientation(QtCore.Qt.Vertical)
widgets_splitter.addWidget(output_widget)
@ -371,14 +377,12 @@ class PythonInterpreterWidget(QtWidgets.QWidget):
line_check_timer.setInterval(200)
line_check_timer.timeout.connect(self._on_timer_timeout)
add_tab_btn.clicked.connect(self._on_add_clicked)
tab_bar.right_clicked.connect(self._on_tab_right_click)
tab_bar.double_clicked.connect(self._on_tab_double_click)
tab_bar.mid_clicked.connect(self._on_tab_mid_click)
tab_widget.tabCloseRequested.connect(self._on_tab_close_req)
self._widgets_splitter = widgets_splitter
self._add_tab_btn = add_tab_btn
self._output_widget = output_widget
self._tab_widget = tab_widget
self._line_check_timer = line_check_timer
@ -459,14 +463,41 @@ class PythonInterpreterWidget(QtWidgets.QWidget):
return
menu = QtWidgets.QMenu(self._tab_widget)
menu.addAction("Rename")
add_tab_action = QtWidgets.QAction("Add tab...", menu)
add_tab_action.setToolTip("Add new tab")
rename_tab_action = QtWidgets.QAction("Rename...", menu)
rename_tab_action.setToolTip("Rename tab")
duplicate_tab_action = QtWidgets.QAction("Duplicate...", menu)
duplicate_tab_action.setToolTip("Duplicate code to new tab")
close_tab_action = QtWidgets.QAction("Close", menu)
close_tab_action.setToolTip("Close tab and lose content")
close_tab_action.setEnabled(self._tab_widget.tabsClosable())
menu.addAction(add_tab_action)
menu.addAction(rename_tab_action)
menu.addAction(duplicate_tab_action)
menu.addAction(close_tab_action)
result = menu.exec_(global_point)
if result is None:
return
if result.text() == "Rename":
if result is rename_tab_action:
self._rename_tab_req(tab_idx)
elif result is add_tab_action:
self._on_add_requested()
elif result is duplicate_tab_action:
self._duplicate_requested(tab_idx)
elif result is close_tab_action:
self._on_tab_close_req(tab_idx)
def _rename_tab_req(self, tab_idx):
dialog = TabNameDialog(self)
dialog.set_tab_name(self._tab_widget.tabText(tab_idx))
@ -475,6 +506,16 @@ class PythonInterpreterWidget(QtWidgets.QWidget):
if tab_name:
self._tab_widget.setTabText(tab_idx, tab_name)
def _duplicate_requested(self, tab_idx=None):
if tab_idx is None:
tab_idx = self._tab_widget.currentIndex()
src_widget = self._tab_widget.widget(tab_idx)
dst_widget = self._add_tab()
if dst_widget is None:
return
dst_widget.set_code(src_widget.get_code())
def _on_tab_mid_click(self, global_point):
point = self._tab_widget.mapFromGlobal(global_point)
tab_bar = self._tab_widget.tabBar()
@ -525,12 +566,17 @@ class PythonInterpreterWidget(QtWidgets.QWidget):
lines.append(self.ansi_escape.sub("", line))
self._append_lines(lines)
def _on_add_clicked(self):
def _on_add_requested(self):
self._add_tab()
def _add_tab(self):
dialog = TabNameDialog(self)
dialog.exec_()
tab_name = dialog.result()
if tab_name:
self.add_tab(tab_name)
return self.add_tab(tab_name)
return None
def _on_before_execute(self, code_text):
at_max = self._output_widget.vertical_scroll_at_max()
@ -562,6 +608,7 @@ class PythonInterpreterWidget(QtWidgets.QWidget):
def add_tab(self, tab_name, index=None):
widget = PythonTabWidget(self)
widget.before_execute.connect(self._on_before_execute)
widget.add_tab_requested.connect(self._on_add_requested)
if index is None:
if self._tab_widget.count() > 0:
index = self._tab_widget.currentIndex() + 1

View file

@ -13,7 +13,8 @@ from openpype.lib.remote_publish import (
start_webpublish_log,
publish_and_log,
fail_batch,
find_variant_key
find_variant_key,
get_task_data
)
@ -160,7 +161,8 @@ class PypeCommands:
log.info("Publish finished.")
@staticmethod
def remotepublishfromapp(project, batch_dir, host, user, targets=None):
def remotepublishfromapp(project, batch_dir, host_name,
user, targets=None):
"""Opens installed variant of 'host' and run remote publish there.
Currently implemented and tested for Photoshop where customer
@ -175,9 +177,7 @@ class PypeCommands:
Runs publish process as user would, in automatic fashion.
"""
SLEEP = 5 # seconds for another loop check for concurrently runs
WAIT_FOR = 300 # seconds to wait for conc. runs
import pyblish.api
from openpype.api import Logger
from openpype.lib import ApplicationManager
@ -185,54 +185,29 @@ class PypeCommands:
log.info("remotepublishphotoshop command")
application_manager = ApplicationManager()
found_variant_key = find_variant_key(application_manager, host)
app_name = "{}/{}".format(host, found_variant_key)
batch_data = None
if batch_dir and os.path.exists(batch_dir):
batch_data = parse_json(os.path.join(batch_dir, "manifest.json"))
if not batch_data:
raise ValueError(
"Cannot parse batch meta in {} folder".format(batch_dir))
asset, task_name, _task_type = get_batch_asset_task_info(
batch_data["context"])
# processing from app expects JUST ONE task in batch and 1 workfile
task_dir_name = batch_data["tasks"][0]
task_data = parse_json(os.path.join(batch_dir, task_dir_name,
"manifest.json"))
task_data = get_task_data(batch_dir)
workfile_path = os.path.join(batch_dir,
task_dir_name,
task_data["task"],
task_data["files"][0])
print("workfile_path {}".format(workfile_path))
_, batch_id = os.path.split(batch_dir)
batch_id = task_data["batch"]
dbcon = get_webpublish_conn()
# safer to start logging here, launch might be broken altogether
_id = start_webpublish_log(dbcon, batch_id, user)
in_progress = True
slept_times = 0
while in_progress:
batches_in_progress = list(dbcon.find({
"status": "in_progress"
}))
if len(batches_in_progress) > 1:
if slept_times * SLEEP >= WAIT_FOR:
fail_batch(_id, batches_in_progress, dbcon)
batches_in_progress = list(dbcon.find({"status": "in_progress"}))
if len(batches_in_progress) > 1:
fail_batch(_id, batches_in_progress, dbcon)
print("Another batch running, probably stuck, ask admin for help")
print("Another batch running, sleeping for a bit")
time.sleep(SLEEP)
slept_times += 1
else:
in_progress = False
asset, task_name, _ = get_batch_asset_task_info(task_data["context"])
application_manager = ApplicationManager()
found_variant_key = find_variant_key(application_manager, host_name)
app_name = "{}/{}".format(host_name, found_variant_key)
# must have for proper launch of app
env = get_app_environments_for_context(
@ -244,9 +219,21 @@ class PypeCommands:
os.environ.update(env)
os.environ["OPENPYPE_PUBLISH_DATA"] = batch_dir
os.environ["IS_HEADLESS"] = "true"
# must pass identifier to update log lines for a batch
os.environ["BATCH_LOG_ID"] = str(_id)
os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib
pyblish.api.register_host(host_name)
if targets:
if isinstance(targets, str):
targets = [targets]
current_targets = os.environ.get("PYBLISH_TARGETS", "").split(
os.pathsep)
for target in targets:
current_targets.append(target)
os.environ["PYBLISH_TARGETS"] = os.pathsep.join(
set(current_targets))
data = {
"last_workfile_path": workfile_path,

View file

@ -18,7 +18,6 @@
"green-light": "hsl(155, 80%, 80%)"
},
"color": {
"font": "#D3D8DE",
"font-hover": "#F0F2F5",
"font-disabled": "#99A3B2",
@ -50,7 +49,16 @@
"border": "#373D48",
"border-hover": "rgba(168, 175, 189, .3)",
"border-focus": "hsl(200, 60%, 60%)",
"border-focus": "rgb(92, 173, 214)",
"tab-widget": {
"bg": "#21252B",
"bg-selected": "#434a56",
"bg-hover": "#373D48",
"color": "#99A3B2",
"color-selected": "#F0F2F5",
"color-hover": "#F0F2F5"
},
"loader": {
"asset-view": {

View file

@ -325,47 +325,38 @@ QTabWidget::pane {
/* move to the right to not mess with borders of widget underneath */
QTabWidget::tab-bar {
left: 2px;
alignment: left;
}
QTabBar::tab {
padding: 5px;
border-left: 3px solid transparent;
border-top: 1px solid {color:border};
border-left: 1px solid {color:border};
border-right: 1px solid {color:border};
/* must be single like because of Nuke*/
background: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0,stop: 0.5 {color:bg}, stop: 1.0 {color:bg-inputs});
padding: 5px;
background: {color:tab-widget:bg};
color: {color:tab-widget:color};
}
QTabBar::tab:selected {
background: {color:grey-lighter};
border-left: 3px solid {color:border-focus};
/* must be single like because of Nuke*/
background: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0,stop: 0.5 {color:bg}, stop: 1.0 {color:border});
}
QTabBar::tab:!selected {
background: {color:grey-light};
border-left-color: {color:tab-widget:bg-selected};
border-right-color: {color:tab-widget:bg-selected};
border-top-color: {color:border-focus};
background: {color:tab-widget:bg-selected};
color: {color:tab-widget:color-selected};
}
QTabBar::tab:!selected {}
QTabBar::tab:!selected:hover {
background: {color:grey-lighter};
background: {color:tab-widget:bg-hover};
color: {color:tab-widget:color-hover};
}
QTabBar::tab:first {
border-left: 1px solid {color:border};
}
QTabBar::tab:first:selected {
margin-left: 0;
border-left: 3px solid {color:border-focus};
}
QTabBar::tab:last:selected {
margin-right: 0;
}
QTabBar::tab:only-one {
margin: 0;
QTabBar::tab:first {}
QTabBar::tab:first:selected {}
QTabBar::tab:last:!selected {
border-right: 1px solid {color:border};
}
QTabBar::tab:last:selected {}
QTabBar::tab:only-one {}
QHeaderView {
border: 0px solid {color:border};
@ -774,6 +765,7 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
/* Python console interpreter */
#PythonInterpreterOutput, #PythonCodeEditor {
font-family: "Roboto Mono";
border-radius: 0px;
}
#SubsetView::item, #RepresentationView:item {

View file

@ -8,14 +8,12 @@ from openpype import style
from openpype.tools.utils.lib import center_window
from openpype.tools.utils.widgets import AssetWidget
from openpype.tools.utils.constants import (
TASK_NAME_ROLE,
PROJECT_NAME_ROLE
)
from openpype.tools.utils.tasks_widget import TasksWidget
from openpype.tools.utils.models import (
ProjectModel,
ProjectSortFilterProxy,
TasksModel,
TasksProxyModel
ProjectSortFilterProxy
)
@ -77,15 +75,11 @@ class ContextDialog(QtWidgets.QDialog):
left_side_layout.addWidget(assets_widget)
# Right side of window contains only tasks
task_view = QtWidgets.QListView(main_splitter)
task_model = TasksModel(dbcon)
task_proxy = TasksProxyModel()
task_proxy.setSourceModel(task_model)
task_view.setModel(task_proxy)
tasks_widget = TasksWidget(dbcon, main_splitter)
# Add widgets to main splitter
main_splitter.addWidget(left_side_widget)
main_splitter.addWidget(task_view)
main_splitter.addWidget(tasks_widget)
# Set stretch of both sides
main_splitter.setStretchFactor(0, 7)
@ -119,9 +113,7 @@ class ContextDialog(QtWidgets.QDialog):
assets_widget.selection_changed.connect(self._on_asset_change)
assets_widget.refresh_triggered.connect(self._on_asset_refresh_trigger)
assets_widget.refreshed.connect(self._on_asset_widget_refresh_finished)
task_view.selectionModel().selectionChanged.connect(
self._on_task_change
)
tasks_widget.task_changed.connect(self._on_task_change)
ok_btn.clicked.connect(self._on_ok_click)
self._dbcon = dbcon
@ -133,9 +125,7 @@ class ContextDialog(QtWidgets.QDialog):
self._assets_widget = assets_widget
self._task_view = task_view
self._task_model = task_model
self._task_proxy = task_proxy
self._tasks_widget = tasks_widget
self._ok_btn = ok_btn
@ -279,15 +269,13 @@ class ContextDialog(QtWidgets.QDialog):
self._dbcon.Session["AVALON_ASSET"] = self._set_context_asset
self._assets_widget.setEnabled(False)
self._assets_widget.select_assets(self._set_context_asset)
self._set_asset_to_task_model()
self._set_asset_to_tasks_widget()
else:
self._assets_widget.setEnabled(True)
self._assets_widget.set_current_asset_btn_visibility(False)
# Refresh tasks
self._task_model.refresh()
# Sort tasks
self._task_proxy.sort(0, QtCore.Qt.AscendingOrder)
self._tasks_widget.refresh()
self._ignore_value_changes = False
@ -314,20 +302,19 @@ class ContextDialog(QtWidgets.QDialog):
"""Selected assets have changed"""
if self._ignore_value_changes:
return
self._set_asset_to_task_model()
self._set_asset_to_tasks_widget()
def _on_task_change(self):
self._validate_strict()
def _set_asset_to_task_model(self):
def _set_asset_to_tasks_widget(self):
# filter None docs they are silo
asset_docs = self._assets_widget.get_selected_assets()
asset_ids = [asset_doc["_id"] for asset_doc in asset_docs]
asset_id = None
if asset_ids:
asset_id = asset_ids[0]
self._task_model.set_asset_id(asset_id)
self._task_proxy.sort(0, QtCore.Qt.AscendingOrder)
self._tasks_widget.set_asset_id(asset_id)
def _confirm_values(self):
"""Store values to output."""
@ -355,11 +342,7 @@ class ContextDialog(QtWidgets.QDialog):
def get_selected_task(self):
"""Currently selected task."""
task_name = None
index = self._task_view.selectionModel().currentIndex()
if index.isValid():
task_name = index.data(TASK_NAME_ROLE)
return task_name
return self._tasks_widget.get_selected_task_name()
def _validate_strict(self):
if not self._strict:

View file

@ -19,102 +19,6 @@ from openpype.lib import ApplicationManager
log = logging.getLogger(__name__)
class TaskModel(QtGui.QStandardItemModel):
"""A model listing the tasks combined for a list of assets"""
def __init__(self, dbcon, parent=None):
super(TaskModel, self).__init__(parent=parent)
self.dbcon = dbcon
self._num_assets = 0
self.default_icon = qtawesome.icon(
"fa.male", color=style.colors.default
)
self.no_task_icon = qtawesome.icon(
"fa.exclamation-circle", color=style.colors.mid
)
self._icons = {}
self._get_task_icons()
def _get_task_icons(self):
if not self.dbcon.Session.get("AVALON_PROJECT"):
return
# Get the project configured icons from database
project = self.dbcon.find_one({"type": "project"})
for task in project["config"].get("tasks") or []:
icon_name = task.get("icon")
if icon_name:
self._icons[task["name"]] = qtawesome.icon(
"fa.{}".format(icon_name), color=style.colors.default
)
def set_assets(self, asset_ids=None, asset_docs=None):
"""Set assets to track by their database id
Arguments:
asset_ids (list): List of asset ids.
asset_docs (list): List of asset entities from MongoDB.
"""
if asset_docs is None and asset_ids is not None:
# find assets in db by query
asset_docs = list(self.dbcon.find({
"type": "asset",
"_id": {"$in": asset_ids}
}))
db_assets_ids = tuple(asset_doc["_id"] for asset_doc in asset_docs)
# check if all assets were found
not_found = tuple(
str(asset_id)
for asset_id in asset_ids
if asset_id not in db_assets_ids
)
assert not not_found, "Assets not found by id: {0}".format(
", ".join(not_found)
)
self.clear()
if not asset_docs:
return
task_names = set()
for asset_doc in asset_docs:
asset_tasks = asset_doc.get("data", {}).get("tasks") or set()
task_names.update(asset_tasks)
self.beginResetModel()
if not task_names:
item = QtGui.QStandardItem(self.no_task_icon, "No task")
item.setEnabled(False)
self.appendRow(item)
else:
for task_name in sorted(task_names):
icon = self._icons.get(task_name, self.default_icon)
item = QtGui.QStandardItem(icon, task_name)
self.appendRow(item)
self.endResetModel()
def headerData(self, section, orientation, role):
if (
role == QtCore.Qt.DisplayRole
and orientation == QtCore.Qt.Horizontal
and section == 0
):
return "Tasks"
return super(TaskModel, self).headerData(section, orientation, role)
class ActionModel(QtGui.QStandardItemModel):
def __init__(self, dbcon, parent=None):
super(ActionModel, self).__init__(parent=parent)

View file

@ -6,7 +6,7 @@ from avalon.vendor import qtawesome
from .delegates import ActionDelegate
from . import lib
from .models import TaskModel, ActionModel
from .models import ActionModel
from openpype.tools.flickcharm import FlickCharm
from .constants import (
ACTION_ROLE,
@ -90,9 +90,6 @@ class ActionBar(QtWidgets.QWidget):
self.project_handler = project_handler
self.dbcon = dbcon
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(8, 0, 8, 0)
view = QtWidgets.QListView(self)
view.setProperty("mode", "icon")
view.setObjectName("IconView")
@ -116,6 +113,8 @@ class ActionBar(QtWidgets.QWidget):
)
view.setItemDelegate(delegate)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(view)
self.model = model
@ -261,92 +260,6 @@ class ActionBar(QtWidgets.QWidget):
self.action_clicked.emit(action)
class TasksWidget(QtWidgets.QWidget):
"""Widget showing active Tasks"""
task_changed = QtCore.Signal()
selection_mode = (
QtCore.QItemSelectionModel.Select | QtCore.QItemSelectionModel.Rows
)
def __init__(self, dbcon, parent=None):
super(TasksWidget, self).__init__(parent)
self.dbcon = dbcon
view = QtWidgets.QTreeView(self)
view.setIndentation(0)
view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers)
model = TaskModel(self.dbcon)
view.setModel(model)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(view)
view.selectionModel().selectionChanged.connect(self.task_changed)
self.model = model
self.view = view
self._last_selected_task = None
def set_asset(self, asset_id):
if asset_id is None:
# Asset deselected
self.model.set_assets()
return
# Try and preserve the last selected task and reselect it
# after switching assets. If there's no currently selected
# asset keep whatever the "last selected" was prior to it.
current = self.get_current_task()
if current:
self._last_selected_task = current
self.model.set_assets([asset_id])
if self._last_selected_task:
self.select_task(self._last_selected_task)
# Force a task changed emit.
self.task_changed.emit()
def select_task(self, task_name):
"""Select a task by name.
If the task does not exist in the current model then selection is only
cleared.
Args:
task (str): Name of the task to select.
"""
# Clear selection
self.view.selectionModel().clearSelection()
# Select the task
for row in range(self.model.rowCount()):
index = self.model.index(row, 0)
_task_name = index.data(QtCore.Qt.DisplayRole)
if _task_name == task_name:
self.view.selectionModel().select(index, self.selection_mode)
# Set the currently active index
self.view.setCurrentIndex(index)
break
def get_current_task(self):
"""Return name of task at current index (selected)
Returns:
str: Name of the current task.
"""
index = self.view.currentIndex()
if self.view.selectionModel().isSelected(index):
return index.data(QtCore.Qt.DisplayRole)
class ActionHistory(QtWidgets.QPushButton):
trigger_history = QtCore.Signal(tuple)

View file

@ -9,13 +9,14 @@ from openpype import style
from openpype.api import resources
from openpype.tools.utils.widgets import AssetWidget
from openpype.tools.utils.tasks_widget import TasksWidget
from avalon.vendor import qtawesome
from .models import ProjectModel
from .lib import get_action_label, ProjectHandler
from .widgets import (
ProjectBar,
ActionBar,
TasksWidget,
ActionHistory,
SlidePageWidget
)
@ -91,8 +92,6 @@ class ProjectsPanel(QtWidgets.QWidget):
def __init__(self, project_handler, parent=None):
super(ProjectsPanel, self).__init__(parent=parent)
layout = QtWidgets.QVBoxLayout(self)
view = ProjectIconView(parent=self)
view.setSelectionMode(QtWidgets.QListView.NoSelection)
flick = FlickCharm(parent=self)
@ -100,6 +99,8 @@ class ProjectsPanel(QtWidgets.QWidget):
view.setModel(project_handler.model)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(view)
view.clicked.connect(self.on_clicked)
@ -123,28 +124,21 @@ class AssetsPanel(QtWidgets.QWidget):
self.dbcon = dbcon
# project bar
project_bar_widget = QtWidgets.QWidget(self)
layout = QtWidgets.QHBoxLayout(project_bar_widget)
layout.setSpacing(4)
# Project bar
btn_back_icon = qtawesome.icon("fa.angle-left", color="white")
btn_back = QtWidgets.QPushButton(project_bar_widget)
btn_back = QtWidgets.QPushButton(self)
btn_back.setIcon(btn_back_icon)
project_bar = ProjectBar(project_handler, project_bar_widget)
project_bar = ProjectBar(project_handler, self)
layout.addWidget(btn_back)
layout.addWidget(project_bar)
project_bar_layout = QtWidgets.QHBoxLayout()
project_bar_layout.setContentsMargins(0, 0, 0, 0)
project_bar_layout.setSpacing(4)
project_bar_layout.addWidget(btn_back)
project_bar_layout.addWidget(project_bar)
# assets
assets_proxy_widgets = QtWidgets.QWidget(self)
assets_proxy_widgets.setContentsMargins(0, 0, 0, 0)
assets_layout = QtWidgets.QVBoxLayout(assets_proxy_widgets)
assets_widget = AssetWidget(
dbcon=self.dbcon, parent=assets_proxy_widgets
)
# Assets widget
assets_widget = AssetWidget(dbcon=self.dbcon, parent=self)
# Make assets view flickable
flick = FlickCharm(parent=self)
@ -152,18 +146,19 @@ class AssetsPanel(QtWidgets.QWidget):
assets_widget.view.setVerticalScrollMode(
assets_widget.view.ScrollPerPixel
)
assets_layout.addWidget(assets_widget)
# tasks
# Tasks widget
tasks_widget = TasksWidget(self.dbcon, self)
body = QtWidgets.QSplitter()
# Body
body = QtWidgets.QSplitter(self)
body.setContentsMargins(0, 0, 0, 0)
body.setSizePolicy(
QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding
)
body.setOrientation(QtCore.Qt.Horizontal)
body.addWidget(assets_proxy_widgets)
body.addWidget(assets_widget)
body.addWidget(tasks_widget)
body.setStretchFactor(0, 100)
body.setStretchFactor(1, 65)
@ -171,22 +166,21 @@ class AssetsPanel(QtWidgets.QWidget):
# main layout
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(project_bar_widget)
layout.addLayout(project_bar_layout)
layout.addWidget(body)
# signals
project_handler.project_changed.connect(self.on_project_changed)
assets_widget.selection_changed.connect(self.on_asset_changed)
assets_widget.refreshed.connect(self.on_asset_changed)
tasks_widget.task_changed.connect(self.on_task_change)
project_handler.project_changed.connect(self._on_project_changed)
assets_widget.selection_changed.connect(self._on_asset_changed)
assets_widget.refreshed.connect(self._on_asset_changed)
tasks_widget.task_changed.connect(self._on_task_change)
btn_back.clicked.connect(self.back_clicked)
self.project_handler = project_handler
self.project_bar = project_bar
self.assets_widget = assets_widget
self.tasks_widget = tasks_widget
self._tasks_widget = tasks_widget
self._btn_back = btn_back
def showEvent(self, event):
@ -197,12 +191,16 @@ class AssetsPanel(QtWidgets.QWidget):
btn_size = self.project_bar.height()
self._btn_back.setFixedSize(QtCore.QSize(btn_size, btn_size))
def on_project_changed(self):
def select_task_name(self, task_name):
self._on_asset_changed()
self._tasks_widget.select_task_name(task_name)
def _on_project_changed(self):
self.session_changed.emit()
self.assets_widget.refresh()
def on_asset_changed(self):
def _on_asset_changed(self):
"""Callback on asset selection changed
This updates the task view.
@ -237,16 +235,17 @@ class AssetsPanel(QtWidgets.QWidget):
asset_id = None
if asset_doc:
asset_id = asset_doc["_id"]
self.tasks_widget.set_asset(asset_id)
self._tasks_widget.set_asset_id(asset_id)
def on_task_change(self):
task_name = self.tasks_widget.get_current_task()
def _on_task_change(self):
task_name = self._tasks_widget.get_selected_task_name()
self.dbcon.Session["AVALON_TASK"] = task_name
self.session_changed.emit()
class LauncherWindow(QtWidgets.QDialog):
"""Launcher interface"""
message_timeout = 5000
def __init__(self, parent=None):
super(LauncherWindow, self).__init__(parent)
@ -283,20 +282,17 @@ class LauncherWindow(QtWidgets.QDialog):
actions_bar = ActionBar(project_handler, self.dbcon, self)
# statusbar
statusbar = QtWidgets.QWidget()
layout = QtWidgets.QHBoxLayout(statusbar)
message_label = QtWidgets.QLabel(self)
message_label = QtWidgets.QLabel()
message_label.setFixedHeight(15)
action_history = ActionHistory()
action_history = ActionHistory(self)
action_history.setStatusTip("Show Action History")
layout.addWidget(message_label)
layout.addWidget(action_history)
status_layout = QtWidgets.QHBoxLayout()
status_layout.addWidget(message_label, 1)
status_layout.addWidget(action_history, 0)
# Vertically split Pages and Actions
body = QtWidgets.QSplitter()
body = QtWidgets.QSplitter(self)
body.setContentsMargins(0, 0, 0, 0)
body.setSizePolicy(
QtWidgets.QSizePolicy.Expanding,
@ -314,19 +310,13 @@ class LauncherWindow(QtWidgets.QDialog):
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(body)
layout.addWidget(statusbar)
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
layout.addLayout(status_layout)
self.project_handler = project_handler
message_timer = QtCore.QTimer()
message_timer.setInterval(self.message_timeout)
message_timer.setSingleShot(True)
self.message_label = message_label
self.project_panel = project_panel
self.asset_panel = asset_panel
self.actions_bar = actions_bar
self.action_history = action_history
self.page_slider = page_slider
self._page = 0
message_timer.timeout.connect(self._on_message_timeout)
# signals
actions_bar.action_clicked.connect(self.on_action_clicked)
@ -338,6 +328,19 @@ class LauncherWindow(QtWidgets.QDialog):
self.resize(520, 740)
self._page = 0
self._message_timer = message_timer
self.project_handler = project_handler
self._message_label = message_label
self.project_panel = project_panel
self.asset_panel = asset_panel
self.actions_bar = actions_bar
self.action_history = action_history
self.page_slider = page_slider
def showEvent(self, event):
self.project_handler.set_active(True)
self.project_handler.start_timer(True)
@ -363,9 +366,12 @@ class LauncherWindow(QtWidgets.QDialog):
self._page = page
self.page_slider.slide_view(page, direction=direction)
def _on_message_timeout(self):
self._message_label.setText("")
def echo(self, message):
self.message_label.setText(str(message))
QtCore.QTimer.singleShot(5000, lambda: self.message_label.setText(""))
self._message_label.setText(str(message))
self._message_timer.start()
self.log.debug(message)
def on_session_changed(self):
@ -448,5 +454,4 @@ class LauncherWindow(QtWidgets.QDialog):
if task_name:
# requires a forced refresh first
self.asset_panel.on_asset_changed()
self.asset_panel.tasks_widget.select_task(task_name)
self.asset_panel.select_task_name(task_name)

View file

@ -243,9 +243,9 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
# update availability on active site when version changes
if self.sync_server.enabled and version:
site = self.active_site
query = self._repre_per_version_pipeline([version["_id"]],
site)
self.active_site,
self.remote_site)
docs = list(self.dbcon.aggregate(query))
if docs:
repre = docs.pop()
@ -801,47 +801,63 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
{"$unwind": "$files"},
{'$addFields': {
'order_local': {
'$filter': {'input': '$files.sites', 'as': 'p',
'cond': {'$eq': ['$$p.name', active_site]}
}}
'$filter': {
'input': '$files.sites', 'as': 'p',
'cond': {'$eq': ['$$p.name', active_site]}
}
}
}},
{'$addFields': {
'order_remote': {
'$filter': {'input': '$files.sites', 'as': 'p',
'cond': {'$eq': ['$$p.name', remote_site]}
}}
'$filter': {
'input': '$files.sites', 'as': 'p',
'cond': {'$eq': ['$$p.name', remote_site]}
}
}
}},
{'$addFields': {
'progress_local': {"$arrayElemAt": [{
'$cond': [{'$size': "$order_local.progress"},
"$order_local.progress",
# if exists created_dt count is as available
{'$cond': [
{'$size': "$order_local.created_dt"},
[1],
[0]
]}
]}, 0]}
'$cond': [
{'$size': "$order_local.progress"},
"$order_local.progress",
# if exists created_dt count is as available
{'$cond': [
{'$size': "$order_local.created_dt"},
[1],
[0]
]}
]},
0
]}
}},
{'$addFields': {
'progress_remote': {"$arrayElemAt": [{
'$cond': [{'$size': "$order_remote.progress"},
"$order_remote.progress",
# if exists created_dt count is as available
{'$cond': [
{'$size': "$order_remote.created_dt"},
[1],
[0]
]}
]}, 0]}
'$cond': [
{'$size': "$order_remote.progress"},
"$order_remote.progress",
# if exists created_dt count is as available
{'$cond': [
{'$size': "$order_remote.created_dt"},
[1],
[0]
]}
]},
0
]}
}},
{'$group': { # first group by repre
'_id': '$_id',
'parent': {'$first': '$parent'},
'avail_ratio_local': {'$first': {
'$divide': [{'$sum': "$progress_local"}, {'$sum': 1}]}},
'avail_ratio_remote': {'$first': {
'$divide': [{'$sum': "$progress_remote"}, {'$sum': 1}]}}
'avail_ratio_local': {
'$first': {
'$divide': [{'$sum': "$progress_local"}, {'$sum': 1}]
}
},
'avail_ratio_remote': {
'$first': {
'$divide': [{'$sum': "$progress_remote"}, {'$sum': 1}]
}
}
}},
{'$group': { # second group by parent, eg version_id
'_id': '$parent',

View file

@ -38,6 +38,7 @@ class App(QtWidgets.QWidget):
# Store callback references
self._callbacks = []
self._connections_set_up = False
filename = get_workfile()
@ -46,17 +47,10 @@ class App(QtWidgets.QWidget):
self.setWindowFlags(QtCore.Qt.Window)
self.setParent(parent)
# Force to delete the window on close so it triggers
# closeEvent only once. Otherwise it's retriggered when
# the widget gets garbage collected.
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.resize(750, 500)
self.setup_ui()
self.setup_connections()
# Force refresh check on initialization
self._on_renderlayer_switch()
@ -111,6 +105,16 @@ class App(QtWidgets.QWidget):
asset_outliner.view.setColumnWidth(0, 200)
look_outliner.view.setColumnWidth(0, 150)
asset_outliner.selection_changed.connect(
self.on_asset_selection_changed)
asset_outliner.refreshed.connect(
lambda: self.echo("Loaded assets..")
)
look_outliner.menu_apply_action.connect(self.on_process_selected)
remove_unused_btn.clicked.connect(remove_unused_looks)
# Open widgets
self.asset_outliner = asset_outliner
self.look_outliner = look_outliner
@ -123,15 +127,8 @@ class App(QtWidgets.QWidget):
def setup_connections(self):
"""Connect interactive widgets with actions"""
self.asset_outliner.selection_changed.connect(
self.on_asset_selection_changed)
self.asset_outliner.refreshed.connect(
lambda: self.echo("Loaded assets.."))
self.look_outliner.menu_apply_action.connect(self.on_process_selected)
self.remove_unused.clicked.connect(remove_unused_looks)
if self._connections_set_up:
return
# Maya renderlayer switch callback
callback = om.MEventMessage.addEventCallback(
@ -139,14 +136,23 @@ class App(QtWidgets.QWidget):
self._on_renderlayer_switch
)
self._callbacks.append(callback)
self._connections_set_up = True
def closeEvent(self, event):
def remove_connection(self):
# Delete callbacks
for callback in self._callbacks:
om.MMessage.removeCallback(callback)
return super(App, self).closeEvent(event)
self._callbacks = []
self._connections_set_up = False
def showEvent(self, event):
self.setup_connections()
super(App, self).showEvent(event)
def closeEvent(self, event):
self.remove_connection()
super(App, self).closeEvent(event)
def _on_renderlayer_switch(self, *args):
"""Callback that updates on Maya renderlayer switch"""

View file

@ -0,0 +1,9 @@
from .window import (
show,
SceneInventoryWindow
)
__all__ = (
"show",
"SceneInventoryWindow"
)

View file

@ -0,0 +1,82 @@
import os
from openpype_modules import sync_server
from Qt import QtGui
def walk_hierarchy(node):
"""Recursively yield group node."""
for child in node.children():
if child.get("isGroupNode"):
yield child
for _child in walk_hierarchy(child):
yield _child
def get_site_icons():
resource_path = os.path.join(
os.path.dirname(sync_server.sync_server_module.__file__),
"providers",
"resources"
)
icons = {}
# TODO get from sync module
for provider in ["studio", "local_drive", "gdrive"]:
pix_url = "{}/{}.png".format(resource_path, provider)
icons[provider] = QtGui.QIcon(pix_url)
return icons
def get_progress_for_repre(repre_doc, active_site, remote_site):
"""
Calculates average progress for representation.
If site has created_dt >> fully available >> progress == 1
Could be calculated in aggregate if it would be too slow
Args:
repre_doc(dict): representation dict
Returns:
(dict) with active and remote sites progress
{'studio': 1.0, 'gdrive': -1} - gdrive site is not present
-1 is used to highlight the site should be added
{'studio': 1.0, 'gdrive': 0.0} - gdrive site is present, not
uploaded yet
"""
progress = {active_site: -1, remote_site: -1}
if not repre_doc:
return progress
files = {active_site: 0, remote_site: 0}
doc_files = repre_doc.get("files") or []
for doc_file in doc_files:
if not isinstance(doc_file, dict):
continue
sites = doc_file.get("sites") or []
for site in sites:
if (
# Pype 2 compatibility
not isinstance(site, dict)
# Check if site name is one of progress sites
or site["name"] not in progress
):
continue
files[site["name"]] += 1
norm_progress = max(progress[site["name"]], 0)
if site.get("created_dt"):
progress[site["name"]] = norm_progress + 1
elif site.get("progress"):
progress[site["name"]] = norm_progress + site["progress"]
else: # site exists, might be failed, do not add again
progress[site["name"]] = 0
# for example 13 fully avail. files out of 26 >> 13/26 = 0.5
avg_progress = {
active_site: progress[active_site] / max(files[active_site], 1),
remote_site: progress[remote_site] / max(files[remote_site], 1)
}
return avg_progress

View file

@ -0,0 +1,576 @@
import re
import logging
from collections import defaultdict
from Qt import QtCore, QtGui
from avalon import api, io, style, schema
from avalon.vendor import qtawesome
from avalon.lib import HeroVersionType
from avalon.tools.models import TreeModel, Item
from .lib import (
get_site_icons,
walk_hierarchy,
get_progress_for_repre
)
from openpype.modules import ModulesManager
class InventoryModel(TreeModel):
"""The model for the inventory"""
Columns = ["Name", "version", "count", "family", "loader", "objectName"]
OUTDATED_COLOR = QtGui.QColor(235, 30, 30)
CHILD_OUTDATED_COLOR = QtGui.QColor(200, 160, 30)
GRAYOUT_COLOR = QtGui.QColor(160, 160, 160)
UniqueRole = QtCore.Qt.UserRole + 2 # unique label role
def __init__(self, family_config_cache, parent=None):
super(InventoryModel, self).__init__(parent)
self.log = logging.getLogger(self.__class__.__name__)
self.family_config_cache = family_config_cache
self._hierarchy_view = False
manager = ModulesManager()
sync_server = manager.modules_by_name["sync_server"]
self.sync_enabled = sync_server.enabled
self._site_icons = {}
self.active_site = self.remote_site = None
self.active_provider = self.remote_provider = None
if not self.sync_enabled:
return
project_name = io.Session["AVALON_PROJECT"]
active_site = sync_server.get_active_site(project_name)
remote_site = sync_server.get_remote_site(project_name)
active_provider = "studio"
remote_provider = "studio"
if active_site != "studio":
# sanitized for icon
active_provider = sync_server.get_provider_for_site(
project_name, active_site
)
if remote_site != "studio":
remote_provider = sync_server.get_provider_for_site(
project_name, remote_site
)
# self.sync_server = sync_server
self.active_site = active_site
self.active_provider = active_provider
self.remote_site = remote_site
self.remote_provider = remote_provider
self._site_icons = get_site_icons()
if "active_site" not in self.Columns:
self.Columns.append("active_site")
if "remote_site" not in self.Columns:
self.Columns.append("remote_site")
def outdated(self, item):
value = item.get("version")
if isinstance(value, HeroVersionType):
return False
if item.get("version") == item.get("highest_version"):
return False
return True
def data(self, index, role):
if not index.isValid():
return
item = index.internalPointer()
if role == QtCore.Qt.FontRole:
# Make top-level entries bold
if item.get("isGroupNode") or item.get("isNotSet"): # group-item
font = QtGui.QFont()
font.setBold(True)
return font
if role == QtCore.Qt.ForegroundRole:
# Set the text color to the OUTDATED_COLOR when the
# collected version is not the same as the highest version
key = self.Columns[index.column()]
if key == "version": # version
if item.get("isGroupNode"): # group-item
if self.outdated(item):
return self.OUTDATED_COLOR
if self._hierarchy_view:
# If current group is not outdated, check if any
# outdated children.
for _node in walk_hierarchy(item):
if self.outdated(_node):
return self.CHILD_OUTDATED_COLOR
else:
if self._hierarchy_view:
# Although this is not a group item, we still need
# to distinguish which one contain outdated child.
for _node in walk_hierarchy(item):
if self.outdated(_node):
return self.CHILD_OUTDATED_COLOR.darker(150)
return self.GRAYOUT_COLOR
if key == "Name" and not item.get("isGroupNode"):
return self.GRAYOUT_COLOR
# Add icons
if role == QtCore.Qt.DecorationRole:
if index.column() == 0:
# Override color
color = item.get("color", style.colors.default)
if item.get("isGroupNode"): # group-item
return qtawesome.icon("fa.folder", color=color)
if item.get("isNotSet"):
return qtawesome.icon("fa.exclamation-circle", color=color)
return qtawesome.icon("fa.file-o", color=color)
if index.column() == 3:
# Family icon
return item.get("familyIcon", None)
if item.get("isGroupNode"):
column_name = self.Columns[index.column()]
if column_name == "active_site":
provider = item.get("active_site_provider")
return self._site_icons.get(provider)
if column_name == "remote_site":
provider = item.get("remote_site_provider")
return self._site_icons.get(provider)
if role == QtCore.Qt.DisplayRole and item.get("isGroupNode"):
column_name = self.Columns[index.column()]
progress = None
if column_name == 'active_site':
progress = item.get("active_site_progress", 0)
elif column_name == 'remote_site':
progress = item.get("remote_site_progress", 0)
if progress is not None:
return "{}%".format(max(progress, 0) * 100)
if role == self.UniqueRole:
return item["representation"] + item.get("objectName", "<none>")
return super(InventoryModel, self).data(index, role)
def set_hierarchy_view(self, state):
"""Set whether to display subsets in hierarchy view."""
state = bool(state)
if state != self._hierarchy_view:
self._hierarchy_view = state
def refresh(self, selected=None, items=None):
"""Refresh the model"""
host = api.registered_host()
if not items: # for debugging or testing, injecting items from outside
items = host.ls()
self.clear()
if self._hierarchy_view and selected:
if not hasattr(host.pipeline, "update_hierarchy"):
# If host doesn't support hierarchical containers, then
# cherry-pick only.
self.add_items((item for item in items
if item["objectName"] in selected))
# Update hierarchy info for all containers
items_by_name = {item["objectName"]: item
for item in host.pipeline.update_hierarchy(items)}
selected_items = set()
def walk_children(names):
"""Select containers and extend to chlid containers"""
for name in [n for n in names if n not in selected_items]:
selected_items.add(name)
item = items_by_name[name]
yield item
for child in walk_children(item["children"]):
yield child
items = list(walk_children(selected)) # Cherry-picked and extended
# Cut unselected upstream containers
for item in items:
if not item.get("parent") in selected_items:
# Parent not in selection, this is root item.
item["parent"] = None
parents = [self._root_item]
# The length of `items` array is the maximum depth that a
# hierarchy could be.
# Take this as an easiest way to prevent looping forever.
maximum_loop = len(items)
count = 0
while items:
if count > maximum_loop:
self.log.warning("Maximum loop count reached, possible "
"missing parent node.")
break
_parents = list()
for parent in parents:
_unparented = list()
def _children():
"""Child item provider"""
for item in items:
if item.get("parent") == parent.get("objectName"):
# (NOTE)
# Since `self._root_node` has no "objectName"
# entry, it will be paired with root item if
# the value of key "parent" is None, or not
# having the key.
yield item
else:
# Not current parent's child, try next
_unparented.append(item)
self.add_items(_children(), parent)
items[:] = _unparented
# Parents of next level
for group_node in parent.children():
_parents += group_node.children()
parents[:] = _parents
count += 1
else:
self.add_items(items)
def add_items(self, items, parent=None):
"""Add the items to the model.
The items should be formatted similar to `api.ls()` returns, an item
is then represented as:
{"filename_v001.ma": [full/filename/of/loaded/filename_v001.ma,
full/filename/of/loaded/filename_v001.ma],
"nodetype" : "reference",
"node": "referenceNode1"}
Note: When performing an additional call to `add_items` it will *not*
group the new items with previously existing item groups of the
same type.
Args:
items (generator): the items to be processed as returned by `ls()`
parent (Item, optional): Set this item as parent for the added
items when provided. Defaults to the root of the model.
Returns:
node.Item: root node which has children added based on the data
"""
self.beginResetModel()
# Group by representation
grouped = defaultdict(lambda: {"items": list()})
for item in items:
grouped[item["representation"]]["items"].append(item)
# Add to model
not_found = defaultdict(list)
not_found_ids = []
for repre_id, group_dict in sorted(grouped.items()):
group_items = group_dict["items"]
# Get parenthood per group
representation = io.find_one({"_id": io.ObjectId(repre_id)})
if not representation:
not_found["representation"].append(group_items)
not_found_ids.append(repre_id)
continue
version = io.find_one({"_id": representation["parent"]})
if not version:
not_found["version"].append(group_items)
not_found_ids.append(repre_id)
continue
elif version["type"] == "hero_version":
_version = io.find_one({
"_id": version["version_id"]
})
version["name"] = HeroVersionType(_version["name"])
version["data"] = _version["data"]
subset = io.find_one({"_id": version["parent"]})
if not subset:
not_found["subset"].append(group_items)
not_found_ids.append(repre_id)
continue
asset = io.find_one({"_id": subset["parent"]})
if not asset:
not_found["asset"].append(group_items)
not_found_ids.append(repre_id)
continue
grouped[repre_id].update({
"representation": representation,
"version": version,
"subset": subset,
"asset": asset
})
for id in not_found_ids:
grouped.pop(id)
for where, group_items in not_found.items():
# create the group header
group_node = Item()
name = "< NOT FOUND - {} >".format(where)
group_node["Name"] = name
group_node["representation"] = name
group_node["count"] = len(group_items)
group_node["isGroupNode"] = False
group_node["isNotSet"] = True
self.add_child(group_node, parent=parent)
for _group_items in group_items:
item_node = Item()
item_node["Name"] = ", ".join(
[item["objectName"] for item in _group_items]
)
self.add_child(item_node, parent=group_node)
for repre_id, group_dict in sorted(grouped.items()):
group_items = group_dict["items"]
representation = grouped[repre_id]["representation"]
version = grouped[repre_id]["version"]
subset = grouped[repre_id]["subset"]
asset = grouped[repre_id]["asset"]
# Get the primary family
no_family = ""
maj_version, _ = schema.get_schema_version(subset["schema"])
if maj_version < 3:
prim_family = version["data"].get("family")
if not prim_family:
families = version["data"].get("families")
prim_family = families[0] if families else no_family
else:
families = subset["data"].get("families") or []
prim_family = families[0] if families else no_family
# Get the label and icon for the family if in configuration
family_config = self.family_config_cache.family_config(prim_family)
family = family_config.get("label", prim_family)
family_icon = family_config.get("icon", None)
# Store the highest available version so the model can know
# whether current version is currently up-to-date.
highest_version = io.find_one({
"type": "version",
"parent": version["parent"]
}, sort=[("name", -1)])
# create the group header
group_node = Item()
group_node["Name"] = "%s_%s: (%s)" % (asset["name"],
subset["name"],
representation["name"])
group_node["representation"] = repre_id
group_node["version"] = version["name"]
group_node["highest_version"] = highest_version["name"]
group_node["family"] = family
group_node["familyIcon"] = family_icon
group_node["count"] = len(group_items)
group_node["isGroupNode"] = True
if self.sync_enabled:
progress = get_progress_for_repre(
representation, self.active_site, self.remote_site
)
group_node["active_site"] = self.active_site
group_node["active_site_provider"] = self.active_provider
group_node["remote_site"] = self.remote_site
group_node["remote_site_provider"] = self.remote_provider
group_node["active_site_progress"] = progress[self.active_site]
group_node["remote_site_progress"] = progress[self.remote_site]
self.add_child(group_node, parent=parent)
for item in group_items:
item_node = Item()
item_node.update(item)
# store the current version on the item
item_node["version"] = version["name"]
# Remapping namespace to item name.
# Noted that the name key is capital "N", by doing this, we
# can view namespace in GUI without changing container data.
item_node["Name"] = item["namespace"]
self.add_child(item_node, parent=group_node)
self.endResetModel()
return self._root_item
class FilterProxyModel(QtCore.QSortFilterProxyModel):
"""Filter model to where key column's value is in the filtered tags"""
def __init__(self, *args, **kwargs):
super(FilterProxyModel, self).__init__(*args, **kwargs)
self._filter_outdated = False
self._hierarchy_view = False
def filterAcceptsRow(self, row, parent):
model = self.sourceModel()
source_index = model.index(row, self.filterKeyColumn(), parent)
# Always allow bottom entries (individual containers), since their
# parent group hidden if it wouldn't have been validated.
rows = model.rowCount(source_index)
if not rows:
return True
# Filter by regex
if not self.filterRegExp().isEmpty():
pattern = re.escape(self.filterRegExp().pattern())
if not self._matches(row, parent, pattern):
return False
if self._filter_outdated:
# When filtering to outdated we filter the up to date entries
# thus we "allow" them when they are outdated
if not self._is_outdated(row, parent):
return False
return True
def set_filter_outdated(self, state):
"""Set whether to show the outdated entries only."""
state = bool(state)
if state != self._filter_outdated:
self._filter_outdated = bool(state)
self.invalidateFilter()
def set_hierarchy_view(self, state):
state = bool(state)
if state != self._hierarchy_view:
self._hierarchy_view = state
def _is_outdated(self, row, parent):
"""Return whether row is outdated.
A row is considered outdated if it has "version" and "highest_version"
data and in the internal data structure, and they are not of an
equal value.
"""
def outdated(node):
version = node.get("version", None)
highest = node.get("highest_version", None)
# Always allow indices that have no version data at all
if version is None and highest is None:
return True
# If either a version or highest is present but not the other
# consider the item invalid.
if not self._hierarchy_view:
# Skip this check if in hierarchy view, or the child item
# node will be hidden even it's actually outdated.
if version is None or highest is None:
return False
return version != highest
index = self.sourceModel().index(row, self.filterKeyColumn(), parent)
# The scene contents are grouped by "representation", e.g. the same
# "representation" loaded twice is grouped under the same header.
# Since the version check filters these parent groups we skip that
# check for the individual children.
has_parent = index.parent().isValid()
if has_parent and not self._hierarchy_view:
return True
# Filter to those that have the different version numbers
node = index.internalPointer()
if outdated(node):
return True
if self._hierarchy_view:
for _node in walk_hierarchy(node):
if outdated(_node):
return True
return False
def _matches(self, row, parent, pattern):
"""Return whether row matches regex pattern.
Args:
row (int): row number in model
parent (QtCore.QModelIndex): parent index
pattern (regex.pattern): pattern to check for in key
Returns:
bool
"""
model = self.sourceModel()
column = self.filterKeyColumn()
role = self.filterRole()
def matches(row, parent, pattern):
index = model.index(row, column, parent)
key = model.data(index, role)
if re.search(pattern, key, re.IGNORECASE):
return True
if matches(row, parent, pattern):
return True
# Also allow if any of the children matches
source_index = model.index(row, column, parent)
rows = model.rowCount(source_index)
if any(
matches(idx, source_index, pattern)
for idx in range(rows)
):
return True
if not self._hierarchy_view:
return False
for idx in range(rows):
child_index = model.index(idx, column, source_index)
child_rows = model.rowCount(child_index)
return any(
self._matches(child_idx, child_index, pattern)
for child_idx in range(child_rows)
)
return True

View file

@ -0,0 +1,993 @@
import collections
import logging
from Qt import QtWidgets, QtCore
from avalon import io, api
from avalon.vendor import qtawesome
from .widgets import SearchComboBox
log = logging.getLogger("SwitchAssetDialog")
class ValidationState:
def __init__(self):
self.asset_ok = True
self.subset_ok = True
self.repre_ok = True
@property
def all_ok(self):
return (
self.asset_ok
and self.subset_ok
and self.repre_ok
)
class SwitchAssetDialog(QtWidgets.QDialog):
"""Widget to support asset switching"""
MIN_WIDTH = 550
switched = QtCore.Signal()
def __init__(self, parent=None, items=None):
super(SwitchAssetDialog, self).__init__(parent)
self.setWindowTitle("Switch selected items ...")
# Force and keep focus dialog
self.setModal(True)
assets_combox = SearchComboBox(self)
subsets_combox = SearchComboBox(self)
repres_combobox = SearchComboBox(self)
assets_combox.set_placeholder("<asset>")
subsets_combox.set_placeholder("<subset>")
repres_combobox.set_placeholder("<representation>")
asset_label = QtWidgets.QLabel(self)
subset_label = QtWidgets.QLabel(self)
repre_label = QtWidgets.QLabel(self)
current_asset_btn = QtWidgets.QPushButton("Use current asset")
accept_icon = qtawesome.icon("fa.check", color="white")
accept_btn = QtWidgets.QPushButton(self)
accept_btn.setIcon(accept_icon)
main_layout = QtWidgets.QGridLayout(self)
# Asset column
main_layout.addWidget(current_asset_btn, 0, 0)
main_layout.addWidget(assets_combox, 1, 0)
main_layout.addWidget(asset_label, 2, 0)
# Subset column
main_layout.addWidget(subsets_combox, 1, 1)
main_layout.addWidget(subset_label, 2, 1)
# Representation column
main_layout.addWidget(repres_combobox, 1, 2)
main_layout.addWidget(repre_label, 2, 2)
# Btn column
main_layout.addWidget(accept_btn, 1, 3)
main_layout.setColumnStretch(0, 1)
main_layout.setColumnStretch(1, 1)
main_layout.setColumnStretch(2, 1)
main_layout.setColumnStretch(3, 0)
assets_combox.currentIndexChanged.connect(
self._combobox_value_changed
)
subsets_combox.currentIndexChanged.connect(
self._combobox_value_changed
)
repres_combobox.currentIndexChanged.connect(
self._combobox_value_changed
)
accept_btn.clicked.connect(self._on_accept)
current_asset_btn.clicked.connect(self._on_current_asset)
self._current_asset_btn = current_asset_btn
self._assets_box = assets_combox
self._subsets_box = subsets_combox
self._representations_box = repres_combobox
self._asset_label = asset_label
self._subset_label = subset_label
self._repre_label = repre_label
self._accept_btn = accept_btn
self._init_asset_name = None
self._init_subset_name = None
self._init_repre_name = None
self._fill_check = False
self._items = items
self._prepare_content_data()
self.refresh(True)
self.setMinimumWidth(self.MIN_WIDTH)
# Set default focus to accept button so you don't directly type in
# first asset field, this also allows to see the placeholder value.
accept_btn.setFocus()
def _prepare_content_data(self):
repre_ids = [
io.ObjectId(item["representation"])
for item in self._items
]
repres = list(io.find({
"type": {"$in": ["representation", "archived_representation"]},
"_id": {"$in": repre_ids}
}))
repres_by_id = {repre["_id"]: repre for repre in repres}
# stash context values, works only for single representation
if len(repres) == 1:
self._init_asset_name = repres[0]["context"]["asset"]
self._init_subset_name = repres[0]["context"]["subset"]
self._init_repre_name = repres[0]["context"]["representation"]
content_repres = {}
archived_repres = []
missing_repres = []
version_ids = []
for repre_id in repre_ids:
if repre_id not in repres_by_id:
missing_repres.append(repre_id)
elif repres_by_id[repre_id]["type"] == "archived_representation":
repre = repres_by_id[repre_id]
archived_repres.append(repre)
version_ids.append(repre["parent"])
else:
repre = repres_by_id[repre_id]
content_repres[repre_id] = repres_by_id[repre_id]
version_ids.append(repre["parent"])
versions = io.find({
"type": {"$in": ["version", "hero_version"]},
"_id": {"$in": list(set(version_ids))}
})
content_versions = {}
hero_version_ids = set()
for version in versions:
content_versions[version["_id"]] = version
if version["type"] == "hero_version":
hero_version_ids.add(version["_id"])
missing_versions = []
subset_ids = []
for version_id in version_ids:
if version_id not in content_versions:
missing_versions.append(version_id)
else:
subset_ids.append(content_versions[version_id]["parent"])
subsets = io.find({
"type": {"$in": ["subset", "archived_subset"]},
"_id": {"$in": subset_ids}
})
subsets_by_id = {sub["_id"]: sub for sub in subsets}
asset_ids = []
archived_subsets = []
missing_subsets = []
content_subsets = {}
for subset_id in subset_ids:
if subset_id not in subsets_by_id:
missing_subsets.append(subset_id)
elif subsets_by_id[subset_id]["type"] == "archived_subset":
subset = subsets_by_id[subset_id]
asset_ids.append(subset["parent"])
archived_subsets.append(subset)
else:
subset = subsets_by_id[subset_id]
asset_ids.append(subset["parent"])
content_subsets[subset_id] = subset
assets = io.find({
"type": {"$in": ["asset", "archived_asset"]},
"_id": {"$in": list(asset_ids)}
})
assets_by_id = {asset["_id"]: asset for asset in assets}
missing_assets = []
archived_assets = []
content_assets = {}
for asset_id in asset_ids:
if asset_id not in assets_by_id:
missing_assets.append(asset_id)
elif assets_by_id[asset_id]["type"] == "archived_asset":
archived_assets.append(assets_by_id[asset_id])
else:
content_assets[asset_id] = assets_by_id[asset_id]
self.content_assets = content_assets
self.content_subsets = content_subsets
self.content_versions = content_versions
self.content_repres = content_repres
self.hero_version_ids = hero_version_ids
self.missing_assets = missing_assets
self.missing_versions = missing_versions
self.missing_subsets = missing_subsets
self.missing_repres = missing_repres
self.missing_docs = (
bool(missing_assets)
or bool(missing_versions)
or bool(missing_subsets)
or bool(missing_repres)
)
self.archived_assets = archived_assets
self.archived_subsets = archived_subsets
self.archived_repres = archived_repres
def _combobox_value_changed(self, *args, **kwargs):
self.refresh()
def refresh(self, init_refresh=False):
"""Build the need comboboxes with content"""
if not self._fill_check and not init_refresh:
return
self._fill_check = False
if init_refresh:
asset_values = self._get_asset_box_values()
self._fill_combobox(asset_values, "asset")
validation_state = ValidationState()
# Set other comboboxes to empty if any document is missing or any asset
# of loaded representations is archived.
self._is_asset_ok(validation_state)
if validation_state.asset_ok:
subset_values = self._get_subset_box_values()
self._fill_combobox(subset_values, "subset")
self._is_subset_ok(validation_state)
if validation_state.asset_ok and validation_state.subset_ok:
repre_values = sorted(self._representations_box_values())
self._fill_combobox(repre_values, "repre")
self._is_repre_ok(validation_state)
# Fill comboboxes with values
self.set_labels()
self.apply_validations(validation_state)
if init_refresh: # pre select context if possible
self._assets_box.set_valid_value(self._init_asset_name)
self._subsets_box.set_valid_value(self._init_subset_name)
self._representations_box.set_valid_value(self._init_repre_name)
self._fill_check = True
def _get_loaders(self, representations):
if not representations:
return list()
available_loaders = filter(
lambda l: not (hasattr(l, "is_utility") and l.is_utility),
api.discover(api.Loader)
)
loaders = set()
for representation in representations:
for loader in api.loaders_from_representation(
available_loaders,
representation
):
loaders.add(loader)
return loaders
def _fill_combobox(self, values, combobox_type):
if combobox_type == "asset":
combobox_widget = self._assets_box
elif combobox_type == "subset":
combobox_widget = self._subsets_box
elif combobox_type == "repre":
combobox_widget = self._representations_box
else:
return
selected_value = combobox_widget.get_valid_value()
# Fill combobox
if values is not None:
combobox_widget.populate(list(sorted(values)))
if selected_value and selected_value in values:
index = None
for idx in range(combobox_widget.count()):
if selected_value == str(combobox_widget.itemText(idx)):
index = idx
break
if index is not None:
combobox_widget.setCurrentIndex(index)
def set_labels(self):
asset_label = self._assets_box.get_valid_value()
subset_label = self._subsets_box.get_valid_value()
repre_label = self._representations_box.get_valid_value()
default = "*No changes"
self._asset_label.setText(asset_label or default)
self._subset_label.setText(subset_label or default)
self._repre_label.setText(repre_label or default)
def apply_validations(self, validation_state):
error_msg = "*Please select"
error_sheet = "border: 1px solid red;"
success_sheet = "border: 1px solid green;"
asset_sheet = None
subset_sheet = None
repre_sheet = None
accept_sheet = None
if validation_state.asset_ok is False:
asset_sheet = error_sheet
self._asset_label.setText(error_msg)
elif validation_state.subset_ok is False:
subset_sheet = error_sheet
self._subset_label.setText(error_msg)
elif validation_state.repre_ok is False:
repre_sheet = error_sheet
self._repre_label.setText(error_msg)
if validation_state.all_ok:
accept_sheet = success_sheet
self._assets_box.setStyleSheet(asset_sheet or "")
self._subsets_box.setStyleSheet(subset_sheet or "")
self._representations_box.setStyleSheet(repre_sheet or "")
self._accept_btn.setEnabled(validation_state.all_ok)
self._accept_btn.setStyleSheet(accept_sheet or "")
def _get_asset_box_values(self):
asset_docs = io.find(
{"type": "asset"},
{"_id": 1, "name": 1}
)
asset_names_by_id = {
asset_doc["_id"]: asset_doc["name"]
for asset_doc in asset_docs
}
subsets = io.find(
{
"type": "subset",
"parent": {"$in": list(asset_names_by_id.keys())}
},
{
"parent": 1
}
)
filtered_assets = []
for subset in subsets:
asset_name = asset_names_by_id[subset["parent"]]
if asset_name not in filtered_assets:
filtered_assets.append(asset_name)
return sorted(filtered_assets)
def _get_subset_box_values(self):
selected_asset = self._assets_box.get_valid_value()
if selected_asset:
asset_doc = io.find_one({"type": "asset", "name": selected_asset})
asset_ids = [asset_doc["_id"]]
else:
asset_ids = list(self.content_assets.keys())
subsets = io.find(
{
"type": "subset",
"parent": {"$in": asset_ids}
},
{
"parent": 1,
"name": 1
}
)
subset_names_by_parent_id = collections.defaultdict(set)
for subset in subsets:
subset_names_by_parent_id[subset["parent"]].add(subset["name"])
possible_subsets = None
for subset_names in subset_names_by_parent_id.values():
if possible_subsets is None:
possible_subsets = subset_names
else:
possible_subsets = (possible_subsets & subset_names)
if not possible_subsets:
break
return list(possible_subsets or list())
def _representations_box_values(self):
# NOTE hero versions are not used because it is expected that
# hero version has same representations as latests
selected_asset = self._assets_box.currentText()
selected_subset = self._subsets_box.currentText()
# If nothing is selected
# [ ] [ ] [?]
if not selected_asset and not selected_subset:
# Find all representations of selection's subsets
possible_repres = list(io.find(
{
"type": "representation",
"parent": {"$in": list(self.content_versions.keys())}
},
{
"parent": 1,
"name": 1
}
))
possible_repres_by_parent = collections.defaultdict(set)
for repre in possible_repres:
possible_repres_by_parent[repre["parent"]].add(repre["name"])
output_repres = None
for repre_names in possible_repres_by_parent.values():
if output_repres is None:
output_repres = repre_names
else:
output_repres = (output_repres & repre_names)
if not output_repres:
break
return list(output_repres or list())
# [x] [x] [?]
if selected_asset and selected_subset:
asset_doc = io.find_one(
{"type": "asset", "name": selected_asset},
{"_id": 1}
)
subset_doc = io.find_one(
{
"type": "subset",
"name": selected_subset,
"parent": asset_doc["_id"]
},
{"_id": 1}
)
subset_id = subset_doc["_id"]
last_versions_by_subset_id = self.find_last_versions([subset_id])
version_doc = last_versions_by_subset_id.get(subset_id)
repre_docs = io.find(
{
"type": "representation",
"parent": version_doc["_id"]
},
{
"name": 1
}
)
return [
repre_doc["name"]
for repre_doc in repre_docs
]
# [x] [ ] [?]
# If asset only is selected
if selected_asset:
asset_doc = io.find_one(
{"type": "asset", "name": selected_asset},
{"_id": 1}
)
if not asset_doc:
return list()
# Filter subsets by subset names from content
subset_names = set()
for subset_doc in self.content_subsets.values():
subset_names.add(subset_doc["name"])
subset_docs = io.find(
{
"type": "subset",
"parent": asset_doc["_id"],
"name": {"$in": list(subset_names)}
},
{"_id": 1}
)
subset_ids = [
subset_doc["_id"]
for subset_doc in subset_docs
]
if not subset_ids:
return list()
last_versions_by_subset_id = self.find_last_versions(subset_ids)
subset_id_by_version_id = {}
for subset_id, last_version in last_versions_by_subset_id.items():
version_id = last_version["_id"]
subset_id_by_version_id[version_id] = subset_id
if not subset_id_by_version_id:
return list()
repre_docs = list(io.find(
{
"type": "representation",
"parent": {"$in": list(subset_id_by_version_id.keys())}
},
{
"name": 1,
"parent": 1
}
))
if not repre_docs:
return list()
repre_names_by_parent = collections.defaultdict(set)
for repre_doc in repre_docs:
repre_names_by_parent[repre_doc["parent"]].add(
repre_doc["name"]
)
available_repres = None
for repre_names in repre_names_by_parent.values():
if available_repres is None:
available_repres = repre_names
continue
available_repres = available_repres.intersection(repre_names)
return list(available_repres)
# [ ] [x] [?]
subset_docs = list(io.find(
{
"type": "subset",
"parent": {"$in": list(self.content_assets.keys())},
"name": selected_subset
},
{"_id": 1, "parent": 1}
))
if not subset_docs:
return list()
subset_docs_by_id = {
subset_doc["_id"]: subset_doc
for subset_doc in subset_docs
}
last_versions_by_subset_id = self.find_last_versions(
subset_docs_by_id.keys()
)
subset_id_by_version_id = {}
for subset_id, last_version in last_versions_by_subset_id.items():
version_id = last_version["_id"]
subset_id_by_version_id[version_id] = subset_id
if not subset_id_by_version_id:
return list()
repre_docs = list(io.find(
{
"type": "representation",
"parent": {"$in": list(subset_id_by_version_id.keys())}
},
{
"name": 1,
"parent": 1
}
))
if not repre_docs:
return list()
repre_names_by_asset_id = {}
for repre_doc in repre_docs:
subset_id = subset_id_by_version_id[repre_doc["parent"]]
asset_id = subset_docs_by_id[subset_id]["parent"]
if asset_id not in repre_names_by_asset_id:
repre_names_by_asset_id[asset_id] = set()
repre_names_by_asset_id[asset_id].add(repre_doc["name"])
available_repres = None
for repre_names in repre_names_by_asset_id.values():
if available_repres is None:
available_repres = repre_names
continue
available_repres = available_repres.intersection(repre_names)
return list(available_repres)
def _is_asset_ok(self, validation_state):
selected_asset = self._assets_box.get_valid_value()
if (
selected_asset is None
and (self.missing_docs or self.archived_assets)
):
validation_state.asset_ok = False
def _is_subset_ok(self, validation_state):
selected_asset = self._assets_box.get_valid_value()
selected_subset = self._subsets_box.get_valid_value()
# [?] [x] [?]
# If subset is selected then must be ok
if selected_subset is not None:
return
# [ ] [ ] [?]
if selected_asset is None:
# If there were archived subsets and asset is not selected
if self.archived_subsets:
validation_state.subset_ok = False
return
# [x] [ ] [?]
asset_doc = io.find_one(
{"type": "asset", "name": selected_asset},
{"_id": 1}
)
subset_docs = io.find(
{"type": "subset", "parent": asset_doc["_id"]},
{"name": 1}
)
subset_names = set(
subset_doc["name"]
for subset_doc in subset_docs
)
for subset_doc in self.content_subsets.values():
if subset_doc["name"] not in subset_names:
validation_state.subset_ok = False
break
def find_last_versions(self, subset_ids):
_pipeline = [
# Find all versions of those subsets
{"$match": {
"type": "version",
"parent": {"$in": list(subset_ids)}
}},
# Sorting versions all together
{"$sort": {"name": 1}},
# Group them by "parent", but only take the last
{"$group": {
"_id": "$parent",
"_version_id": {"$last": "$_id"},
"type": {"$last": "$type"}
}}
]
last_versions_by_subset_id = dict()
for doc in io.aggregate(_pipeline):
doc["parent"] = doc["_id"]
doc["_id"] = doc.pop("_version_id")
last_versions_by_subset_id[doc["parent"]] = doc
return last_versions_by_subset_id
def _is_repre_ok(self, validation_state):
selected_asset = self._assets_box.get_valid_value()
selected_subset = self._subsets_box.get_valid_value()
selected_repre = self._representations_box.get_valid_value()
# [?] [?] [x]
# If subset is selected then must be ok
if selected_repre is not None:
return
# [ ] [ ] [ ]
if selected_asset is None and selected_subset is None:
if (
self.archived_repres
or self.missing_versions
or self.missing_repres
):
validation_state.repre_ok = False
return
# [x] [x] [ ]
if selected_asset is not None and selected_subset is not None:
asset_doc = io.find_one(
{"type": "asset", "name": selected_asset},
{"_id": 1}
)
subset_doc = io.find_one(
{
"type": "subset",
"parent": asset_doc["_id"],
"name": selected_subset
},
{"_id": 1}
)
last_versions_by_subset_id = self.find_last_versions(
[subset_doc["_id"]]
)
last_version = last_versions_by_subset_id.get(subset_doc["_id"])
if not last_version:
validation_state.repre_ok = False
return
repre_docs = io.find(
{
"type": "representation",
"parent": last_version["_id"]
},
{"name": 1}
)
repre_names = set(
repre_doc["name"]
for repre_doc in repre_docs
)
for repre_doc in self.content_repres.values():
if repre_doc["name"] not in repre_names:
validation_state.repre_ok = False
break
return
# [x] [ ] [ ]
if selected_asset is not None:
asset_doc = io.find_one(
{"type": "asset", "name": selected_asset},
{"_id": 1}
)
subset_docs = list(io.find(
{
"type": "subset",
"parent": asset_doc["_id"]
},
{"_id": 1, "name": 1}
))
subset_name_by_id = {}
subset_ids = set()
for subset_doc in subset_docs:
subset_id = subset_doc["_id"]
subset_ids.add(subset_id)
subset_name_by_id[subset_id] = subset_doc["name"]
last_versions_by_subset_id = self.find_last_versions(subset_ids)
subset_id_by_version_id = {}
for subset_id, last_version in last_versions_by_subset_id.items():
version_id = last_version["_id"]
subset_id_by_version_id[version_id] = subset_id
repre_docs = io.find(
{
"type": "representation",
"parent": {"$in": list(subset_id_by_version_id.keys())}
},
{
"name": 1,
"parent": 1
}
)
repres_by_subset_name = {}
for repre_doc in repre_docs:
subset_id = subset_id_by_version_id[repre_doc["parent"]]
subset_name = subset_name_by_id[subset_id]
if subset_name not in repres_by_subset_name:
repres_by_subset_name[subset_name] = set()
repres_by_subset_name[subset_name].add(repre_doc["name"])
for repre_doc in self.content_repres.values():
version_doc = self.content_versions[repre_doc["parent"]]
subset_doc = self.content_subsets[version_doc["parent"]]
repre_names = (
repres_by_subset_name.get(subset_doc["name"]) or []
)
if repre_doc["name"] not in repre_names:
validation_state.repre_ok = False
break
return
# [ ] [x] [ ]
# Subset documents
subset_docs = io.find(
{
"type": "subset",
"parent": {"$in": list(self.content_assets.keys())},
"name": selected_subset
},
{"_id": 1, "name": 1, "parent": 1}
)
subset_docs_by_id = {}
for subset_doc in subset_docs:
subset_docs_by_id[subset_doc["_id"]] = subset_doc
last_versions_by_subset_id = self.find_last_versions(
subset_docs_by_id.keys()
)
subset_id_by_version_id = {}
for subset_id, last_version in last_versions_by_subset_id.items():
version_id = last_version["_id"]
subset_id_by_version_id[version_id] = subset_id
repre_docs = io.find(
{
"type": "representation",
"parent": {"$in": list(subset_id_by_version_id.keys())}
},
{
"name": 1,
"parent": 1
}
)
repres_by_asset_id = {}
for repre_doc in repre_docs:
subset_id = subset_id_by_version_id[repre_doc["parent"]]
asset_id = subset_docs_by_id[subset_id]["parent"]
if asset_id not in repres_by_asset_id:
repres_by_asset_id[asset_id] = set()
repres_by_asset_id[asset_id].add(repre_doc["name"])
for repre_doc in self.content_repres.values():
version_doc = self.content_versions[repre_doc["parent"]]
subset_doc = self.content_subsets[version_doc["parent"]]
asset_id = subset_doc["parent"]
repre_names = (
repres_by_asset_id.get(asset_id) or []
)
if repre_doc["name"] not in repre_names:
validation_state.repre_ok = False
break
def _on_current_asset(self):
# Set initial asset as current.
asset_name = io.Session["AVALON_ASSET"]
index = self._assets_box.findText(
asset_name, QtCore.Qt.MatchFixedString
)
if index >= 0:
print("Setting asset to {}".format(asset_name))
self._assets_box.setCurrentIndex(index)
def _on_accept(self):
# Use None when not a valid value or when placeholder value
selected_asset = self._assets_box.get_valid_value()
selected_subset = self._subsets_box.get_valid_value()
selected_representation = self._representations_box.get_valid_value()
if selected_asset:
asset_doc = io.find_one({"type": "asset", "name": selected_asset})
asset_docs_by_id = {asset_doc["_id"]: asset_doc}
else:
asset_docs_by_id = self.content_assets
asset_docs_by_name = {
asset_doc["name"]: asset_doc
for asset_doc in asset_docs_by_id.values()
}
asset_ids = list(asset_docs_by_id.keys())
subset_query = {
"type": "subset",
"parent": {"$in": asset_ids}
}
if selected_subset:
subset_query["name"] = selected_subset
subset_docs = list(io.find(subset_query))
subset_ids = []
subset_docs_by_parent_and_name = collections.defaultdict(dict)
for subset in subset_docs:
subset_ids.append(subset["_id"])
parent_id = subset["parent"]
name = subset["name"]
subset_docs_by_parent_and_name[parent_id][name] = subset
# versions
version_docs = list(io.find({
"type": "version",
"parent": {"$in": subset_ids}
}, sort=[("name", -1)]))
hero_version_docs = list(io.find({
"type": "hero_version",
"parent": {"$in": subset_ids}
}))
version_ids = list()
version_docs_by_parent_id = {}
for version_doc in version_docs:
parent_id = version_doc["parent"]
if parent_id not in version_docs_by_parent_id:
version_ids.append(version_doc["_id"])
version_docs_by_parent_id[parent_id] = version_doc
hero_version_docs_by_parent_id = {}
for hero_version_doc in hero_version_docs:
version_ids.append(hero_version_doc["_id"])
parent_id = hero_version_doc["parent"]
hero_version_docs_by_parent_id[parent_id] = hero_version_doc
repre_docs = io.find({
"type": "representation",
"parent": {"$in": version_ids}
})
repre_docs_by_parent_id_by_name = collections.defaultdict(dict)
for repre_doc in repre_docs:
parent_id = repre_doc["parent"]
name = repre_doc["name"]
repre_docs_by_parent_id_by_name[parent_id][name] = repre_doc
for container in self._items:
container_repre_id = io.ObjectId(container["representation"])
container_repre = self.content_repres[container_repre_id]
container_repre_name = container_repre["name"]
container_version_id = container_repre["parent"]
container_version = self.content_versions[container_version_id]
container_subset_id = container_version["parent"]
container_subset = self.content_subsets[container_subset_id]
container_subset_name = container_subset["name"]
container_asset_id = container_subset["parent"]
container_asset = self.content_assets[container_asset_id]
container_asset_name = container_asset["name"]
if selected_asset:
asset_doc = asset_docs_by_name[selected_asset]
else:
asset_doc = asset_docs_by_name[container_asset_name]
subsets_by_name = subset_docs_by_parent_and_name[asset_doc["_id"]]
if selected_subset:
subset_doc = subsets_by_name[selected_subset]
else:
subset_doc = subsets_by_name[container_subset_name]
repre_doc = None
subset_id = subset_doc["_id"]
if container_version["type"] == "hero_version":
hero_version = hero_version_docs_by_parent_id.get(
subset_id
)
if hero_version:
_repres = repre_docs_by_parent_id_by_name.get(
hero_version["_id"]
)
if selected_representation:
repre_doc = _repres.get(selected_representation)
else:
repre_doc = _repres.get(container_repre_name)
if not repre_doc:
version_doc = version_docs_by_parent_id[subset_id]
version_id = version_doc["_id"]
repres_by_name = repre_docs_by_parent_id_by_name[version_id]
if selected_representation:
repre_doc = repres_by_name[selected_representation]
else:
repre_doc = repres_by_name[container_repre_name]
try:
api.switch(container, repre_doc)
except Exception:
msg = (
"Couldn't switch asset."
"See traceback for more information."
)
log.warning(msg, exc_info=True)
dialog = QtWidgets.QMessageBox(self)
dialog.setWindowTitle("Switch asset failed")
dialog.setText(
"Switch asset failed. Search console log for more details"
)
dialog.exec_()
self.switched.emit()
self.close()

View file

@ -0,0 +1,794 @@
import collections
import logging
from functools import partial
from Qt import QtWidgets, QtCore
from avalon import io, api, style
from avalon.vendor import qtawesome
from avalon.lib import HeroVersionType
from avalon.tools import lib as tools_lib
from openpype.modules import ModulesManager
from .switch_dialog import SwitchAssetDialog
from .model import InventoryModel
DEFAULT_COLOR = "#fb9c15"
log = logging.getLogger("SceneInventory")
class SceneInvetoryView(QtWidgets.QTreeView):
data_changed = QtCore.Signal()
hierarchy_view_changed = QtCore.Signal(bool)
def __init__(self, parent=None):
super(SceneInvetoryView, self).__init__(parent=parent)
# view settings
self.setIndentation(12)
self.setAlternatingRowColors(True)
self.setSortingEnabled(True)
self.setSelectionMode(self.ExtendedSelection)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._show_right_mouse_menu)
self._hierarchy_view = False
self._selected = None
manager = ModulesManager()
self.sync_server = manager.modules_by_name["sync_server"]
self.sync_enabled = self.sync_server.enabled
def _set_hierarchy_view(self, enabled):
if enabled == self._hierarchy_view:
return
self._hierarchy_view = enabled
self.hierarchy_view_changed.emit(enabled)
def _enter_hierarchy(self, items):
self._selected = set(i["objectName"] for i in items)
self._set_hierarchy_view(True)
self.data_changed.emit()
self.expandToDepth(1)
self.setStyleSheet("""
QTreeView {
border-color: #fb9c15;
}
""")
def _leave_hierarchy(self):
self._set_hierarchy_view(False)
self.data_changed.emit()
self.setStyleSheet("QTreeView {}")
def _build_item_menu_for_selection(self, items, menu):
if not items:
return
repre_ids = []
for item in items:
item_id = io.ObjectId(item["representation"])
if item_id not in repre_ids:
repre_ids.append(item_id)
repre_docs = io.find(
{
"type": "representation",
"_id": {"$in": repre_ids}
},
{"parent": 1}
)
version_ids = []
for repre_doc in repre_docs:
version_id = repre_doc["parent"]
if version_id not in version_ids:
version_ids.append(version_id)
loaded_versions = io.find({
"_id": {"$in": version_ids},
"type": {"$in": ["version", "hero_version"]}
})
loaded_hero_versions = []
versions_by_parent_id = collections.defaultdict(list)
version_parents = []
for version in loaded_versions:
if version["type"] == "hero_version":
loaded_hero_versions.append(version)
else:
parent_id = version["parent"]
versions_by_parent_id[parent_id].append(version)
if parent_id not in version_parents:
version_parents.append(parent_id)
all_versions = io.find({
"type": {"$in": ["hero_version", "version"]},
"parent": {"$in": version_parents}
})
hero_versions = []
versions = []
for version in all_versions:
if version["type"] == "hero_version":
hero_versions.append(version)
else:
versions.append(version)
has_loaded_hero_versions = len(loaded_hero_versions) > 0
has_available_hero_version = len(hero_versions) > 0
has_outdated = False
for version in versions:
parent_id = version["parent"]
current_versions = versions_by_parent_id[parent_id]
for current_version in current_versions:
if current_version["name"] < version["name"]:
has_outdated = True
break
if has_outdated:
break
switch_to_versioned = None
if has_loaded_hero_versions:
def _on_switch_to_versioned(items):
repre_ids = []
for item in items:
item_id = io.ObjectId(item["representation"])
if item_id not in repre_ids:
repre_ids.append(item_id)
repre_docs = io.find(
{
"type": "representation",
"_id": {"$in": repre_ids}
},
{"parent": 1}
)
version_ids = []
version_id_by_repre_id = {}
for repre_doc in repre_docs:
version_id = repre_doc["parent"]
version_id_by_repre_id[repre_doc["_id"]] = version_id
if version_id not in version_ids:
version_ids.append(version_id)
hero_versions = io.find(
{
"_id": {"$in": version_ids},
"type": "hero_version"
},
{"version_id": 1}
)
version_ids = set()
for hero_version in hero_versions:
version_id = hero_version["version_id"]
version_ids.add(version_id)
hero_version_id = hero_version["_id"]
for _repre_id, current_version_id in (
version_id_by_repre_id.items()
):
if current_version_id == hero_version_id:
version_id_by_repre_id[_repre_id] = version_id
version_docs = io.find(
{
"_id": {"$in": list(version_ids)},
"type": "version"
},
{"name": 1}
)
version_name_by_id = {}
for version_doc in version_docs:
version_name_by_id[version_doc["_id"]] = \
version_doc["name"]
for item in items:
repre_id = io.ObjectId(item["representation"])
version_id = version_id_by_repre_id.get(repre_id)
version_name = version_name_by_id.get(version_id)
if version_name is not None:
try:
api.update(item, version_name)
except AssertionError:
self._show_version_error_dialog(
version_name, [item]
)
log.warning("Update failed", exc_info=True)
self.data_changed.emit()
update_icon = qtawesome.icon(
"fa.asterisk",
color=DEFAULT_COLOR
)
switch_to_versioned = QtWidgets.QAction(
update_icon,
"Switch to versioned",
menu
)
switch_to_versioned.triggered.connect(
lambda: _on_switch_to_versioned(items)
)
update_to_latest_action = None
if has_outdated or has_loaded_hero_versions:
# update to latest version
def _on_update_to_latest(items):
for item in items:
try:
api.update(item, -1)
except AssertionError:
self._show_version_error_dialog(None, [item])
log.warning("Update failed", exc_info=True)
self.data_changed.emit()
update_icon = qtawesome.icon(
"fa.angle-double-up",
color=DEFAULT_COLOR
)
update_to_latest_action = QtWidgets.QAction(
update_icon,
"Update to latest",
menu
)
update_to_latest_action.triggered.connect(
lambda: _on_update_to_latest(items)
)
change_to_hero = None
if has_available_hero_version:
# change to hero version
def _on_update_to_hero(items):
for item in items:
try:
api.update(item, HeroVersionType(-1))
except AssertionError:
self._show_version_error_dialog('hero', [item])
log.warning("Update failed", exc_info=True)
self.data_changed.emit()
# TODO change icon
change_icon = qtawesome.icon(
"fa.asterisk",
color="#00b359"
)
change_to_hero = QtWidgets.QAction(
change_icon,
"Change to hero",
menu
)
change_to_hero.triggered.connect(
lambda: _on_update_to_hero(items)
)
# set version
set_version_icon = qtawesome.icon("fa.hashtag", color=DEFAULT_COLOR)
set_version_action = QtWidgets.QAction(
set_version_icon,
"Set version",
menu
)
set_version_action.triggered.connect(
lambda: self._show_version_dialog(items))
# switch asset
switch_asset_icon = qtawesome.icon("fa.sitemap", color=DEFAULT_COLOR)
switch_asset_action = QtWidgets.QAction(
switch_asset_icon,
"Switch Asset",
menu
)
switch_asset_action.triggered.connect(
lambda: self._show_switch_dialog(items))
# remove
remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR)
remove_action = QtWidgets.QAction(remove_icon, "Remove items", menu)
remove_action.triggered.connect(
lambda: self._show_remove_warning_dialog(items))
# add the actions
if switch_to_versioned:
menu.addAction(switch_to_versioned)
if update_to_latest_action:
menu.addAction(update_to_latest_action)
if change_to_hero:
menu.addAction(change_to_hero)
menu.addAction(set_version_action)
menu.addAction(switch_asset_action)
menu.addSeparator()
menu.addAction(remove_action)
self._handle_sync_server(menu, repre_ids)
def _handle_sync_server(self, menu, repre_ids):
"""
Adds actions for download/upload when SyncServer is enabled
Args:
menu (OptionMenu)
repre_ids (list) of object_ids
Returns:
(OptionMenu)
"""
if not self.sync_enabled:
return
menu.addSeparator()
download_icon = qtawesome.icon("fa.download", color=DEFAULT_COLOR)
download_active_action = QtWidgets.QAction(
download_icon,
"Download",
menu
)
download_active_action.triggered.connect(
lambda: self._add_sites(repre_ids, 'active_site'))
upload_icon = qtawesome.icon("fa.upload", color=DEFAULT_COLOR)
upload_remote_action = QtWidgets.QAction(
upload_icon,
"Upload",
menu
)
upload_remote_action.triggered.connect(
lambda: self._add_sites(repre_ids, 'remote_site'))
menu.addAction(download_active_action)
menu.addAction(upload_remote_action)
def _add_sites(self, repre_ids, side):
"""
(Re)sync all 'repre_ids' to specific site.
It checks if opposite site has fully available content to limit
accidents. (ReSync active when no remote >> losing active content)
Args:
repre_ids (list)
side (str): 'active_site'|'remote_site'
"""
project_name = io.Session["AVALON_PROJECT"]
active_site = self.sync_server.get_active_site(project_name)
remote_site = self.sync_server.get_remote_site(project_name)
repre_docs = io.find({
"type": "representation",
"_id": {"$in": repre_ids}
})
repre_docs_by_id = {
repre_doc["_id"]: repre_doc
for repre_doc in repre_docs
}
for repre_id in repre_ids:
repre_doc = repre_docs_by_id.get(repre_id)
if not repre_doc:
continue
progress = tools_lib.get_progress_for_repre(
repre_doc,
active_site,
remote_site
)
if side == "active_site":
# check opposite from added site, must be 1 or unable to sync
check_progress = progress[remote_site]
site = active_site
else:
check_progress = progress[active_site]
site = remote_site
if check_progress == 1:
self.sync_server.add_site(
project_name, repre_id, site, force=True
)
self.data_changed.emit()
def _build_item_menu(self, items=None):
"""Create menu for the selected items"""
if not items:
items = []
menu = QtWidgets.QMenu(self)
# add the actions
self._build_item_menu_for_selection(items, menu)
# These two actions should be able to work without selection
# expand all items
expandall_action = QtWidgets.QAction(menu, text="Expand all items")
expandall_action.triggered.connect(self.expandAll)
# collapse all items
collapse_action = QtWidgets.QAction(menu, text="Collapse all items")
collapse_action.triggered.connect(self.collapseAll)
menu.addAction(expandall_action)
menu.addAction(collapse_action)
custom_actions = self._get_custom_actions(containers=items)
if custom_actions:
submenu = QtWidgets.QMenu("Actions", self)
for action in custom_actions:
color = action.color or DEFAULT_COLOR
icon = qtawesome.icon("fa.%s" % action.icon, color=color)
action_item = QtWidgets.QAction(icon, action.label, submenu)
action_item.triggered.connect(
partial(self._process_custom_action, action, items))
submenu.addAction(action_item)
menu.addMenu(submenu)
# go back to flat view
if self._hierarchy_view:
back_to_flat_icon = qtawesome.icon("fa.list", color=DEFAULT_COLOR)
back_to_flat_action = QtWidgets.QAction(
back_to_flat_icon,
"Back to Full-View",
menu
)
back_to_flat_action.triggered.connect(self._leave_hierarchy)
# send items to hierarchy view
enter_hierarchy_icon = qtawesome.icon("fa.indent", color="#d8d8d8")
enter_hierarchy_action = QtWidgets.QAction(
enter_hierarchy_icon,
"Cherry-Pick (Hierarchy)",
menu
)
enter_hierarchy_action.triggered.connect(
lambda: self._enter_hierarchy(items))
if items:
menu.addAction(enter_hierarchy_action)
if self._hierarchy_view:
menu.addAction(back_to_flat_action)
return menu
def _get_custom_actions(self, containers):
"""Get the registered Inventory Actions
Args:
containers(list): collection of containers
Returns:
list: collection of filter and initialized actions
"""
def sorter(Plugin):
"""Sort based on order attribute of the plugin"""
return Plugin.order
# Fedd an empty dict if no selection, this will ensure the compat
# lookup always work, so plugin can interact with Scene Inventory
# reversely.
containers = containers or [dict()]
# Check which action will be available in the menu
Plugins = api.discover(api.InventoryAction)
compatible = [p() for p in Plugins if
any(p.is_compatible(c) for c in containers)]
return sorted(compatible, key=sorter)
def _process_custom_action(self, action, containers):
"""Run action and if results are returned positive update the view
If the result is list or dict, will select view items by the result.
Args:
action (InventoryAction): Inventory Action instance
containers (list): Data of currently selected items
Returns:
None
"""
result = action.process(containers)
if result:
self.data_changed.emit()
if isinstance(result, (list, set)):
self._select_items_by_action(result)
if isinstance(result, dict):
self._select_items_by_action(
result["objectNames"], result["options"]
)
def _select_items_by_action(self, object_names, options=None):
"""Select view items by the result of action
Args:
object_names (list or set): A list/set of container object name
options (dict): GUI operation options.
Returns:
None
"""
options = options or dict()
if options.get("clear", True):
self.clearSelection()
object_names = set(object_names)
if (
self._hierarchy_view
and not self._selected.issuperset(object_names)
):
# If any container not in current cherry-picked view, update
# view before selecting them.
self._selected.update(object_names)
self.data_changed.emit()
model = self.model()
selection_model = self.selectionModel()
select_mode = {
"select": selection_model.Select,
"deselect": selection_model.Deselect,
"toggle": selection_model.Toggle,
}[options.get("mode", "select")]
for item in tools_lib.iter_model_rows(model, 0):
item = item.data(InventoryModel.ItemRole)
if item.get("isGroupNode"):
continue
name = item.get("objectName")
if name in object_names:
self.scrollTo(item) # Ensure item is visible
flags = select_mode | selection_model.Rows
selection_model.select(item, flags)
object_names.remove(name)
if len(object_names) == 0:
break
def _show_right_mouse_menu(self, pos):
"""Display the menu when at the position of the item clicked"""
globalpos = self.viewport().mapToGlobal(pos)
if not self.selectionModel().hasSelection():
print("No selection")
# Build menu without selection, feed an empty list
menu = self._build_item_menu()
menu.exec_(globalpos)
return
active = self.currentIndex() # index under mouse
active = active.sibling(active.row(), 0) # get first column
# move index under mouse
indices = self.get_indices()
if active in indices:
indices.remove(active)
indices.append(active)
# Extend to the sub-items
all_indices = self._extend_to_children(indices)
items = [dict(i.data(InventoryModel.ItemRole)) for i in all_indices
if i.parent().isValid()]
if self._hierarchy_view:
# Ensure no group item
items = [n for n in items if not n.get("isGroupNode")]
menu = self._build_item_menu(items)
menu.exec_(globalpos)
def get_indices(self):
"""Get the selected rows"""
selection_model = self.selectionModel()
return selection_model.selectedRows()
def _extend_to_children(self, indices):
"""Extend the indices to the children indices.
Top-level indices are extended to its children indices. Sub-items
are kept as is.
Args:
indices (list): The indices to extend.
Returns:
list: The children indices
"""
def get_children(i):
model = i.model()
rows = model.rowCount(parent=i)
for row in range(rows):
child = model.index(row, 0, parent=i)
yield child
subitems = set()
for i in indices:
valid_parent = i.parent().isValid()
if valid_parent and i not in subitems:
subitems.add(i)
if self._hierarchy_view:
# Assume this is a group item
for child in get_children(i):
subitems.add(child)
else:
# is top level item
for child in get_children(i):
subitems.add(child)
return list(subitems)
def _show_version_dialog(self, items):
"""Create a dialog with the available versions for the selected file
Args:
items (list): list of items to run the "set_version" for
Returns:
None
"""
active = items[-1]
# Get available versions for active representation
representation_id = io.ObjectId(active["representation"])
representation = io.find_one({"_id": representation_id})
version = io.find_one({
"_id": representation["parent"]
})
versions = list(io.find(
{
"parent": version["parent"],
"type": "version"
},
sort=[("name", 1)]
))
hero_version = io.find_one({
"parent": version["parent"],
"type": "hero_version"
})
if hero_version:
_version_id = hero_version["version_id"]
for _version in versions:
if _version["_id"] != _version_id:
continue
hero_version["name"] = HeroVersionType(
_version["name"]
)
hero_version["data"] = _version["data"]
break
# Get index among the listed versions
current_item = None
current_version = active["version"]
if isinstance(current_version, HeroVersionType):
current_item = hero_version
else:
for version in versions:
if version["name"] == current_version:
current_item = version
break
all_versions = []
if hero_version:
all_versions.append(hero_version)
all_versions.extend(reversed(versions))
if current_item:
index = all_versions.index(current_item)
else:
index = 0
versions_by_label = dict()
labels = []
for version in all_versions:
is_hero = version["type"] == "hero_version"
label = tools_lib.format_version(version["name"], is_hero)
labels.append(label)
versions_by_label[label] = version["name"]
label, state = QtWidgets.QInputDialog.getItem(
self,
"Set version..",
"Set version number to",
labels,
current=index,
editable=False
)
if not state:
return
if label:
version = versions_by_label[label]
for item in items:
try:
api.update(item, version)
except AssertionError:
self._show_version_error_dialog(version, [item])
log.warning("Update failed", exc_info=True)
# refresh model when done
self.data_changed.emit()
def _show_switch_dialog(self, items):
"""Display Switch dialog"""
dialog = SwitchAssetDialog(self, items)
dialog.switched.connect(self.data_changed.emit)
dialog.show()
def _show_remove_warning_dialog(self, items):
"""Prompt a dialog to inform the user the action will remove items"""
accept = QtWidgets.QMessageBox.Ok
buttons = accept | QtWidgets.QMessageBox.Cancel
state = QtWidgets.QMessageBox.question(
self,
"Are you sure?",
"Are you sure you want to remove {} item(s)".format(len(items)),
buttons=buttons,
defaultButton=accept
)
if state != accept:
return
for item in items:
api.remove(item)
self.data_changed.emit()
def _show_version_error_dialog(self, version, items):
"""Shows QMessageBox when version switch doesn't work
Args:
version: str or int or None
"""
if not version:
version_str = "latest"
elif version == "hero":
version_str = "hero"
elif isinstance(version, int):
version_str = "v{:03d}".format(version)
else:
version_str = version
dialog = QtWidgets.QMessageBox()
dialog.setIcon(QtWidgets.QMessageBox.Warning)
dialog.setStyleSheet(style.load_stylesheet())
dialog.setWindowTitle("Update failed")
switch_btn = dialog.addButton(
"Switch Asset",
QtWidgets.QMessageBox.ActionRole
)
switch_btn.clicked.connect(lambda: self._show_switch_dialog(items))
dialog.addButton(QtWidgets.QMessageBox.Cancel)
msg = (
"Version update to '{}' failed as representation doesn't exist."
"\n\nPlease update to version with a valid representation"
" OR \n use 'Switch Asset' button to change asset."
).format(version_str)
dialog.setText(msg)
dialog.exec_()

View file

@ -0,0 +1,51 @@
from Qt import QtWidgets, QtCore
class SearchComboBox(QtWidgets.QComboBox):
"""Searchable ComboBox with empty placeholder value as first value"""
def __init__(self, parent=None):
super(SearchComboBox, self).__init__(parent)
self.setEditable(True)
self.setInsertPolicy(self.NoInsert)
# Apply completer settings
completer = self.completer()
completer.setCompletionMode(completer.PopupCompletion)
completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
# Force style sheet on popup menu
# It won't take the parent stylesheet for some reason
# todo: better fix for completer popup stylesheet
# if module.window:
# popup = completer.popup()
# popup.setStyleSheet(module.window.styleSheet())
def set_placeholder(self, placeholder):
self.lineEdit().setPlaceholderText(placeholder)
def populate(self, items):
self.clear()
self.addItems([""]) # ensure first item is placeholder
self.addItems(items)
def get_valid_value(self):
"""Return the current text if it's a valid value else None
Note: The empty placeholder value is valid and returns as ""
"""
text = self.currentText()
lookup = set(self.itemText(i) for i in range(self.count()))
if text not in lookup:
return None
return text or None
def set_valid_value(self, value):
"""Try to locate 'value' and pre-select it in dropdown."""
index = self.findText(value)
if index > -1:
self.setCurrentIndex(index)

View file

@ -0,0 +1,203 @@
import os
import sys
from Qt import QtWidgets, QtCore
from avalon.vendor import qtawesome
from avalon import io, api
from openpype import style
from openpype.tools.utils.delegates import VersionDelegate
from openpype.tools.utils.lib import (
qt_app_context,
preserve_expanded_rows,
preserve_selection,
FamilyConfigCache
)
from .model import (
InventoryModel,
FilterProxyModel
)
from .view import SceneInvetoryView
module = sys.modules[__name__]
module.window = None
class SceneInventoryWindow(QtWidgets.QDialog):
"""Scene Inventory window"""
def __init__(self, parent=None):
super(SceneInventoryWindow, self).__init__(parent)
if not parent:
self.setWindowFlags(
self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint
)
project_name = os.getenv("AVALON_PROJECT") or "<Project not set>"
self.setWindowTitle("Scene Inventory 1.0 - {}".format(project_name))
self.setObjectName("SceneInventory")
# Maya only property
self.setProperty("saveWindowPref", True)
self.resize(1100, 480)
# region control
filter_label = QtWidgets.QLabel("Search", self)
text_filter = QtWidgets.QLineEdit(self)
outdated_only_checkbox = QtWidgets.QCheckBox(
"Filter to outdated", self
)
outdated_only_checkbox.setToolTip("Show outdated files only")
outdated_only_checkbox.setChecked(False)
icon = qtawesome.icon("fa.refresh", color="white")
refresh_button = QtWidgets.QPushButton(self)
refresh_button.setIcon(icon)
control_layout = QtWidgets.QHBoxLayout()
control_layout.addWidget(filter_label)
control_layout.addWidget(text_filter)
control_layout.addWidget(outdated_only_checkbox)
control_layout.addWidget(refresh_button)
# endregion control
family_config_cache = FamilyConfigCache(io)
model = InventoryModel(family_config_cache)
proxy = FilterProxyModel()
proxy.setSourceModel(model)
proxy.setDynamicSortFilter(True)
proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
view = SceneInvetoryView(self)
view.setModel(proxy)
# set some nice default widths for the view
view.setColumnWidth(0, 250) # name
view.setColumnWidth(1, 55) # version
view.setColumnWidth(2, 55) # count
view.setColumnWidth(3, 150) # family
view.setColumnWidth(4, 100) # namespace
# apply delegates
version_delegate = VersionDelegate(io, self)
column = model.Columns.index("version")
view.setItemDelegateForColumn(column, version_delegate)
layout = QtWidgets.QVBoxLayout(self)
layout.addLayout(control_layout)
layout.addWidget(view)
# signals
text_filter.textChanged.connect(self._on_text_filter_change)
outdated_only_checkbox.stateChanged.connect(
self._on_outdated_state_change
)
view.hierarchy_view_changed.connect(
self._on_hiearchy_view_change
)
view.data_changed.connect(self.refresh)
refresh_button.clicked.connect(self.refresh)
self._outdated_only_checkbox = outdated_only_checkbox
self._view = view
self._model = model
self._proxy = proxy
self._version_delegate = version_delegate
self._family_config_cache = family_config_cache
self._first_show = True
family_config_cache.refresh()
def showEvent(self, event):
super(SceneInventoryWindow, self).showEvent(event)
if self._first_show:
self._first_show = False
self.setStyleSheet(style.load_stylesheet())
def keyPressEvent(self, event):
"""Custom keyPressEvent.
Override keyPressEvent to do nothing so that Maya's panels won't
take focus when pressing "SHIFT" whilst mouse is over viewport or
outliner. This way users don't accidently perform Maya commands
whilst trying to name an instance.
"""
def refresh(self, items=None):
with preserve_expanded_rows(
tree_view=self._view,
role=self._model.UniqueRole
):
with preserve_selection(
tree_view=self._view,
role=self._model.UniqueRole,
current_index=False
):
kwargs = {"items": items}
# TODO do not touch view's inner attribute
if self._view._hierarchy_view:
kwargs["selected"] = self._view._selected
self._model.refresh(**kwargs)
def _on_hiearchy_view_change(self, enabled):
self._proxy.set_hierarchy_view(enabled)
self._model.set_hierarchy_view(enabled)
def _on_text_filter_change(self, text_filter):
self._proxy.setFilterRegExp(text_filter)
def _on_outdated_state_change(self):
self._proxy.set_filter_outdated(
self._outdated_only_checkbox.isChecked()
)
def show(root=None, debug=False, parent=None, items=None):
"""Display Scene Inventory GUI
Arguments:
debug (bool, optional): Run in debug-mode,
defaults to False
parent (QtCore.QObject, optional): When provided parent the interface
to this QObject.
items (list) of dictionaries - for injection of items for standalone
testing
"""
try:
module.window.close()
del module.window
except (RuntimeError, AttributeError):
pass
if debug is True:
io.install()
if not os.environ.get("AVALON_PROJECT"):
any_project = next(
project for project in io.projects()
if project.get("active", True) is not False
)
api.Session["AVALON_PROJECT"] = any_project["name"]
else:
api.Session["AVALON_PROJECT"] = os.environ.get("AVALON_PROJECT")
with qt_app_context():
window = SceneInventoryWindow(parent)
window.show()
window.refresh(items=items)
module.window = window
# Pull window to the front.
module.window.raise_()
module.window.activateWindow()

View file

@ -7,7 +7,7 @@ PROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 102
TASK_NAME_ROLE = QtCore.Qt.UserRole + 301
TASK_TYPE_ROLE = QtCore.Qt.UserRole + 302
TASK_ORDER_ROLE = QtCore.Qt.UserRole + 403
TASK_ORDER_ROLE = QtCore.Qt.UserRole + 303
LOCAL_PROVIDER_ROLE = QtCore.Qt.UserRole + 500 # provider of active site
REMOTE_PROVIDER_ROLE = QtCore.Qt.UserRole + 501 # provider of remote site

View file

@ -154,21 +154,20 @@ class HostToolsHelper:
def get_scene_inventory_tool(self, parent):
"""Create, cache and return scene inventory tool window."""
if self._scene_inventory_tool is None:
from avalon.tools.sceneinventory.app import Window
from openpype.tools.sceneinventory import SceneInventoryWindow
scene_inventory_window = Window(parent=parent or self._parent)
scene_inventory_window = SceneInventoryWindow(
parent=parent or self._parent
)
self._scene_inventory_tool = scene_inventory_window
return self._scene_inventory_tool
def show_scene_inventory(self, parent=None):
"""Show tool maintain loaded containers."""
from avalon import style
scene_inventory_tool = self.get_scene_inventory_tool(parent)
scene_inventory_tool.show()
scene_inventory_tool.refresh()
scene_inventory_tool.setStyleSheet(style.load_stylesheet())
# Pull window to the front.
scene_inventory_tool.raise_()

View file

@ -0,0 +1,299 @@
from Qt import QtWidgets, QtCore, QtGui
from avalon import style
from avalon.vendor import qtawesome
from .views import DeselectableTreeView
from .constants import (
TASK_ORDER_ROLE,
TASK_TYPE_ROLE,
TASK_NAME_ROLE
)
class TasksModel(QtGui.QStandardItemModel):
"""A model listing the tasks combined for a list of assets"""
def __init__(self, dbcon, parent=None):
super(TasksModel, self).__init__(parent=parent)
self.dbcon = dbcon
self.setHeaderData(
0, QtCore.Qt.Horizontal, "Tasks", QtCore.Qt.DisplayRole
)
self._default_icon = qtawesome.icon(
"fa.male",
color=style.colors.default
)
self._no_tasks_icon = qtawesome.icon(
"fa.exclamation-circle",
color=style.colors.mid
)
self._cached_icons = {}
self._project_task_types = {}
self._empty_tasks_item = None
self._last_asset_id = None
self._loaded_project_name = None
def _context_is_valid(self):
if self.dbcon.Session.get("AVALON_PROJECT"):
return True
return False
def refresh(self):
self._refresh_task_types()
self.set_asset_id(self._last_asset_id)
def _refresh_task_types(self):
# Get the project configured icons from database
task_types = {}
if self._context_is_valid():
project = self.dbcon.find_one(
{"type": "project"},
{"config.tasks"}
)
task_types = project["config"].get("tasks") or task_types
self._project_task_types = task_types
def _try_get_awesome_icon(self, icon_name):
icon = None
if icon_name:
try:
icon = qtawesome.icon(
"fa.{}".format(icon_name),
color=style.colors.default
)
except Exception:
pass
return icon
def headerData(self, section, orientation, role=None):
if role is None:
role = QtCore.Qt.EditRole
# Show nice labels in the header
if section == 0:
if (
role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole)
and orientation == QtCore.Qt.Horizontal
):
return "Tasks"
return super(TasksModel, self).headerData(section, orientation, role)
def _get_icon(self, task_icon, task_type_icon):
if task_icon in self._cached_icons:
return self._cached_icons[task_icon]
icon = self._try_get_awesome_icon(task_icon)
if icon is not None:
self._cached_icons[task_icon] = icon
return icon
if task_type_icon in self._cached_icons:
icon = self._cached_icons[task_type_icon]
self._cached_icons[task_icon] = icon
return icon
icon = self._try_get_awesome_icon(task_type_icon)
if icon is None:
icon = self._default_icon
self._cached_icons[task_icon] = icon
self._cached_icons[task_type_icon] = icon
return icon
def set_asset_id(self, asset_id):
asset_doc = None
if self._context_is_valid():
asset_doc = self.dbcon.find_one(
{"_id": asset_id},
{"data.tasks": True}
)
self._set_asset(asset_doc)
def _get_empty_task_item(self):
if self._empty_tasks_item is None:
item = QtGui.QStandardItem("No task")
item.setData(self._no_tasks_icon, QtCore.Qt.DecorationRole)
item.setFlags(QtCore.Qt.NoItemFlags)
self._empty_tasks_item = item
return self._empty_tasks_item
def _set_asset(self, asset_doc):
"""Set assets to track by their database id
Arguments:
asset_doc (dict): Asset document from MongoDB.
"""
asset_tasks = {}
self._last_asset_id = None
if asset_doc:
asset_tasks = asset_doc.get("data", {}).get("tasks") or {}
self._last_asset_id = asset_doc["_id"]
root_item = self.invisibleRootItem()
root_item.removeRows(0, root_item.rowCount())
items = []
for task_name, task_info in asset_tasks.items():
task_icon = task_info.get("icon")
task_type = task_info.get("type")
task_order = task_info.get("order")
task_type_info = self._project_task_types.get(task_type) or {}
task_type_icon = task_type_info.get("icon")
icon = self._get_icon(task_icon, task_type_icon)
label = "{} ({})".format(task_name, task_type or "type N/A")
item = QtGui.QStandardItem(label)
item.setData(task_name, TASK_NAME_ROLE)
item.setData(task_type, TASK_TYPE_ROLE)
item.setData(task_order, TASK_ORDER_ROLE)
item.setData(icon, QtCore.Qt.DecorationRole)
item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable)
items.append(item)
if not items:
item = QtGui.QStandardItem("No task")
item.setData(self._no_tasks_icon, QtCore.Qt.DecorationRole)
item.setFlags(QtCore.Qt.NoItemFlags)
items.append(item)
root_item.appendRows(items)
class TasksProxyModel(QtCore.QSortFilterProxyModel):
def lessThan(self, x_index, y_index):
x_order = x_index.data(TASK_ORDER_ROLE)
y_order = y_index.data(TASK_ORDER_ROLE)
if x_order is not None and y_order is not None:
if x_order < y_order:
return True
if x_order > y_order:
return False
elif x_order is None and y_order is not None:
return True
elif y_order is None and x_order is not None:
return False
x_name = x_index.data(QtCore.Qt.DisplayRole)
y_name = y_index.data(QtCore.Qt.DisplayRole)
if x_name == y_name:
return True
if x_name == tuple(sorted((x_name, y_name)))[0]:
return True
return False
class TasksWidget(QtWidgets.QWidget):
"""Widget showing active Tasks"""
task_changed = QtCore.Signal()
def __init__(self, dbcon, parent=None):
super(TasksWidget, self).__init__(parent)
tasks_view = DeselectableTreeView(self)
tasks_view.setIndentation(0)
tasks_view.setSortingEnabled(True)
tasks_view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers)
header_view = tasks_view.header()
header_view.setSortIndicator(0, QtCore.Qt.AscendingOrder)
tasks_model = TasksModel(dbcon)
tasks_proxy = TasksProxyModel()
tasks_proxy.setSourceModel(tasks_model)
tasks_view.setModel(tasks_proxy)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(tasks_view)
selection_model = tasks_view.selectionModel()
selection_model.selectionChanged.connect(self._on_task_change)
self._tasks_model = tasks_model
self._tasks_proxy = tasks_proxy
self._tasks_view = tasks_view
self._last_selected_task_name = None
def refresh(self):
self._tasks_model.refresh()
def set_asset_id(self, asset_id):
# Asset deselected
if asset_id is None:
return
# Try and preserve the last selected task and reselect it
# after switching assets. If there's no currently selected
# asset keep whatever the "last selected" was prior to it.
current = self.get_selected_task_name()
if current:
self._last_selected_task_name = current
self._tasks_model.set_asset_id(asset_id)
if self._last_selected_task_name:
self.select_task_name(self._last_selected_task_name)
# Force a task changed emit.
self.task_changed.emit()
def select_task_name(self, task_name):
"""Select a task by name.
If the task does not exist in the current model then selection is only
cleared.
Args:
task (str): Name of the task to select.
"""
task_view_model = self._tasks_view.model()
if not task_view_model:
return
# Clear selection
selection_model = self._tasks_view.selectionModel()
selection_model.clearSelection()
# Select the task
mode = selection_model.Select | selection_model.Rows
for row in range(task_view_model.rowCount()):
index = task_view_model.index(row, 0)
name = index.data(TASK_NAME_ROLE)
if name == task_name:
selection_model.select(index, mode)
# Set the currently active index
self._tasks_view.setCurrentIndex(index)
break
def get_selected_task_name(self):
"""Return name of task at current index (selected)
Returns:
str: Name of the current task.
"""
index = self._tasks_view.currentIndex()
selection_model = self._tasks_view.selectionModel()
if index.isValid() and selection_model.isSelected(index):
return index.data(TASK_NAME_ROLE)
return None
def get_selected_task_type(self):
index = self._tasks_view.currentIndex()
selection_model = self._tasks_view.selectionModel()
if index.isValid() and selection_model.isSelected(index):
return index.data(TASK_TYPE_ROLE)
return None
def _on_task_change(self):
self.task_changed.emit()

View file

@ -15,16 +15,9 @@ from openpype.tools.utils.lib import (
schedule, qt_app_context
)
from openpype.tools.utils.widgets import AssetWidget
from openpype.tools.utils.tasks_widget import TasksWidget
from openpype.tools.utils.delegates import PrettyTimeDelegate
from openpype.tools.utils.constants import (
TASK_NAME_ROLE,
TASK_TYPE_ROLE
)
from openpype.tools.utils.models import (
TasksModel,
TasksProxyModel
)
from .model import FilesModel
from .view import FilesView
@ -323,110 +316,6 @@ class NameWindow(QtWidgets.QDialog):
)
class TasksWidget(QtWidgets.QWidget):
"""Widget showing active Tasks"""
task_changed = QtCore.Signal()
def __init__(self, dbcon=None, parent=None):
super(TasksWidget, self).__init__(parent)
tasks_view = QtWidgets.QTreeView(self)
tasks_view.setIndentation(0)
tasks_view.setSortingEnabled(True)
if dbcon is None:
dbcon = io
tasks_model = TasksModel(dbcon)
tasks_proxy = TasksProxyModel()
tasks_proxy.setSourceModel(tasks_model)
tasks_view.setModel(tasks_proxy)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(tasks_view)
selection_model = tasks_view.selectionModel()
selection_model.currentChanged.connect(self.task_changed)
self._tasks_model = tasks_model
self._tasks_proxy = tasks_proxy
self._tasks_view = tasks_view
self._last_selected_task = None
def set_asset(self, asset_doc):
# Asset deselected
if asset_doc is None:
return
# Try and preserve the last selected task and reselect it
# after switching assets. If there's no currently selected
# asset keep whatever the "last selected" was prior to it.
current = self.get_current_task_name()
if current:
self._last_selected_task = current
self._tasks_model.set_asset(asset_doc)
self._tasks_proxy.sort(0, QtCore.Qt.AscendingOrder)
if self._last_selected_task:
self.select_task(self._last_selected_task)
# Force a task changed emit.
self.task_changed.emit()
def select_task(self, task_name):
"""Select a task by name.
If the task does not exist in the current model then selection is only
cleared.
Args:
task (str): Name of the task to select.
"""
task_view_model = self._tasks_view.model()
if not task_view_model:
return
# Clear selection
selection_model = self._tasks_view.selectionModel()
selection_model.clearSelection()
# Select the task
mode = selection_model.Select | selection_model.Rows
for row in range(task_view_model.rowCount()):
index = task_view_model.index(row, 0)
name = index.data(TASK_NAME_ROLE)
if name == task_name:
selection_model.select(index, mode)
# Set the currently active index
self._tasks_view.setCurrentIndex(index)
break
def get_current_task_name(self):
"""Return name of task at current index (selected)
Returns:
str: Name of the current task.
"""
index = self._tasks_view.currentIndex()
selection_model = self._tasks_view.selectionModel()
if index.isValid() and selection_model.isSelected(index):
return index.data(TASK_NAME_ROLE)
return None
def get_current_task_type(self):
index = self._tasks_view.currentIndex()
selection_model = self._tasks_view.selectionModel()
if index.isValid() and selection_model.isSelected(index):
return index.data(TASK_TYPE_ROLE)
return None
class FilesWidget(QtWidgets.QWidget):
"""A widget displaying files that allows to save and open files."""
file_selected = QtCore.Signal(str)
@ -1052,7 +941,7 @@ class Window(QtWidgets.QMainWindow):
if asset_docs:
asset_doc = asset_docs[0]
task_name = self.tasks_widget.get_current_task_name()
task_name = self.tasks_widget.get_selected_task_name()
workfile_doc = None
if asset_doc and task_name and filepath:
@ -1082,7 +971,7 @@ class Window(QtWidgets.QMainWindow):
def _get_current_workfile_doc(self, filepath=None):
if filepath is None:
filepath = self.files_widget._get_selected_filepath()
task_name = self.tasks_widget.get_current_task_name()
task_name = self.tasks_widget.get_selected_task_name()
asset_docs = self.assets_widget.get_selected_assets()
if not task_name or not asset_docs or not filepath:
return
@ -1102,7 +991,7 @@ class Window(QtWidgets.QMainWindow):
workdir, filename = os.path.split(filepath)
asset_docs = self.assets_widget.get_selected_assets()
asset_doc = asset_docs[0]
task_name = self.tasks_widget.get_current_task_name()
task_name = self.tasks_widget.get_selected_task_name()
create_workfile_doc(asset_doc, task_name, filename, workdir, io)
def set_context(self, context):
@ -1113,18 +1002,16 @@ class Window(QtWidgets.QMainWindow):
"name": asset,
"type": "asset"
},
{
"data.tasks": 1
}
)
{"_id": 1}
) or {}
# Select the asset
self.assets_widget.select_assets([asset], expand=True)
self.tasks_widget.set_asset(asset_document)
self.tasks_widget.set_asset_id(asset_document.get("_id"))
if "task" in context:
self.tasks_widget.select_task(context["task"])
self.tasks_widget.select_task_name(context["task"])
def refresh(self):
# Refresh asset widget
@ -1134,7 +1021,7 @@ class Window(QtWidgets.QMainWindow):
def _on_asset_changed(self):
asset = self.assets_widget.get_selected_assets() or None
asset_id = None
if not asset:
# Force disable the other widgets if no
# active selection
@ -1142,16 +1029,17 @@ class Window(QtWidgets.QMainWindow):
self.files_widget.setEnabled(False)
else:
asset = asset[0]
asset_id = asset.get("_id")
self.tasks_widget.setEnabled(True)
self.tasks_widget.set_asset(asset)
self.tasks_widget.set_asset_id(asset_id)
def _on_task_changed(self):
asset = self.assets_widget.get_selected_assets() or None
if asset is not None:
asset = asset[0]
task_name = self.tasks_widget.get_current_task_name()
task_type = self.tasks_widget.get_current_task_type()
task_name = self.tasks_widget.get_selected_task_name()
task_type = self.tasks_widget.get_selected_task_type()
self.tasks_widget.setEnabled(bool(asset))

View file

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

View file

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