diff --git a/CHANGELOG.md b/CHANGELOG.md index c7ecbc83bf..145c2e2c1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,854 @@ # Changelog +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.1...3.15.2) + +### **🆕 New features** + + +
+maya gltf texture convertor and validator #4261 + +Continuity of the gltf extractor implementation + +Continuity of the gltf extractor https://github.com/pypeclub/OpenPype/pull/4192UPDATE:**Validator for GLSL Shader**: Validate whether the mesh uses GLSL Shader. If not it will error out. The user can choose to perform the repair action and it will help to assign glsl shader. If the mesh with Stringray PBS, the repair action will also check to see if there is any linked texture such as Color, Occulsion, and Normal Map. If yes, it will help to relink the related textures to the glsl shader.*****If the mesh uses the PBS Shader, + + +___ + +
+ + +
+Unreal: New Publisher #4370 + +Implementation of the new publisher for Unreal. + +The implementation of the new publisher for Unreal. This PR includes the changes for all the existing creators to be compatible with the new publisher.The basic creator has been split in two distinct creators: +- `UnrealAssetCreator`, works with assets in the Content Browser. +- `UnrealActorCreator` that works with actors in the scene. + + +___ + +
+ + +
+Implementation of a new splash screen #4592 + +Implemented a new splash screen widget to reflect a process running in the background. This widget can be used for other tasks than UE. **Also fixed the compilation error of the AssetContainer.cpp when trying to build the plugin in UE 5.0** + + +___ + +
+ + +
+Deadline for 3dsMax #4439 + +Setting up deadline for 3dsmax + +Setting up deadline for 3dsmax by setting render outputs and viewport camera + + +___ + +
+ + +
+Nuke: adding nukeassist #4494 + +Adding support for NukeAssist + +For support of NukeAssist we had to limit some Nuke features since NukeAssist itself Nuke with limitations. We do not support Creator and Publisher. User can only Load versions with version control. User can also set Framerange and Colorspace. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Maya: OP-2630 acescg maya #4340 + +Resolves #2712 + + +___ + +
+ + +
+Default Ftrack Family on RenderLayer #4458 + +With default settings, renderlayers in Maya were not being tagged with the Ftrack family leading to confusion when doing reviews. + + +___ + +
+ + +
+Maya: Maya Playblast Options - OP-3783 #4487 + +Replacement PR for #3912. Adds more options for playblasts to preferences/settings. + +Adds the following as options in generating playblasts, matching viewport settings. +- Use default material +- Wireframe on shaded +- X-ray +- X-ray Joints +- X-ray active component + + +___ + +
+ + +
+Maya: Passing custom attributes to alembic - OP-4111 #4516 + +Passing custom attributes to alembic + +This PR makes it possible to pass all user defined attributes along to the alembic representation. + + +___ + +
+ + +
+Maya: Options for VrayProxy output - OP-2010 #4525 + +Options for output of VrayProxy. + +Client requested more granular control of output from VrayProxy instance. Exposed options on the instance and settings for vrmesh and alembic. + + +___ + +
+ + +
+Maya: Validate missing instance attributes #4559 + +Validate missing instance attributes. + +New attributes can be introduced as new features come in. Old instances will need to be updated with these attributes for the documentation to make sense, and users do not have to recreate the instances. + + +___ + +
+ + +
+Refactored Generation of UE Projects, installation of plugins moved to the engine #4369 + +Improved the way how OpenPype works with generation of UE projects. Also the installation of the plugin has been altered to install into the engine + +OpenPype now uses the appropriate tools to generate UE projects. Unreal Build Tool (UBT) and a "Commandlet Project" is needed to properly generate a BP project, or C++ code in case that `dev_mode = True`, folders, the .uproject file and many other resources.On the plugin's side, it is built seperately with the UnrealAutomationTool (UAT) and then it's contents are moved under the `Engine/Plugins/Marketplace/OpenPype` directory. + + +___ + +
+ + +
+Unreal: Use client functions in Layout loader #4578 + +Use 'get_representations' instead of 'legacy_io' query in layout loader. + +This is removing usage of `find_one` called on `legacy_io` and use rather client functions as preparation for AYON connection. Also all representations are queried at once instead of one by one. + + +___ + +
+ + +
+General: Support for extensions filtering in loaders #4492 + +Added extensions filtering support to loader plugins. + +To avoid possible backwards compatibility break is filtering exactly the same and filtering by extensions is enabled only if class attribute 'extensions' is set. + + +___ + +
+ + +
+Nuke: multiple reformat in baking review profiles #4514 + +Added support for multiple reformat nodes in baking profiles. + +Old settings for single reformat node is supported and prioritised just in case studios are using it and backward compatibility is needed. Warnings in Nuke terminal are notifying users to switch settings to new workflow. Settings are also explaining the migration way. + + +___ + +
+ + +
+Nuke: Add option to use new creating system in workfile template builder #4545 + +Nuke workfile template builder can use new creators instead of legacy creators. + +Modified workfile template builder to have option to say if legacy creators should be used or new creators. Legacy creators are disabled by default, so Maya has changed the value. + + +___ + +
+ + +
+Global, Nuke: Workfile first version with template processing #4579 + +Supporting new template workfile builder with toggle for creation of first version of workfile in case there is none yet. + + +___ + +
+ + +
+Fusion: New Publisher #4523 + +This is an updated PR for @BigRoy 's old PR (https://github.com/ynput/OpenPype/pull/3892).I have merged it with code from OP 3.15.1-nightly.6 and made sure it works as expected.This converts the old publishing system to the new one. It implements Fusion as a new host addon. + + +- Create button removed in OpenPype menu in favor of the new Publisher +- Draft refactor validations to raise PublishValidationError +- Implement Creator for New Publisher +- Implement Fusion as Host addon + + +___ + +
+ + +
+TVPaint: Use Publisher tool #4471 + +Use Publisher tool and new creation system in TVPaint integration. + +Using new creation system makes TVPaint integration a little bit easier to maintain for artists. Removed unneeded tools Creator and Subset Manager tools. Goal is to keep the integration work as close as possible to previous integration. Some changes were made but primarilly because they were not right using previous system.All creators create instance with final family instead of changing the family during extraction. Render passes are not related to group id but to render layer instance. Render layer is still related to group. Workfile, review and scene render instances are created using autocreators instead of auto-collection during publishing. Subset names are fully filled during publishing but instance labels are filled on refresh with the last known right value. Implemented basic of legacy convertor which should convert render layers and render passes. + + +___ + +
+ + +
+TVPaint: Auto-detect render creation #4496 + +Create plugin which will create Render Layer and Render Pass instances based on information in the scene. + +Added new creator that must be triggered by artist. The create plugin will first create Render Layer instances if were not created yet. For variant is used color group name. The creator has option to rename color groups by template defined in settings -> Template may use index of group by it's usage in scene (from bottom to top). After Render Layers will create Render Passes. Render Pass is created for each individual TVPaint layer in any group that had created Render Layer. It's name is used as variant (pass). + + +___ + +
+ + +
+TVPaint: Small enhancements #4501 + +Small enhancements in TVPaint integration which did not get to https://github.com/ynput/OpenPype/pull/4471. + +It was found out that `opacity` returned from `tv_layerinfo` is always empty and is dangerous to add it to layer information. Added information about "current" layer to layers information. Disable review of Render Layer and Render Pass instances by default. In most of productions is used only "scene review". Skip usage of `"enabled"` key from settings in automated layer/pass creation. + + +___ + +
+ + +
+Global: color v3 global oiio transcoder plugin #4291 + +Implements possibility to use `oiiotool` to transcode image sequences from one color space to another(s). + +Uses collected `colorspaceData` information about source color spaces, these information needs to be collected previously in each DCC interested in color management.Uses profiles configured in Settings to create single or multiple new representations (and file extensions) with different color spaces.New representations might replace existing one, each new representation might contain different tags and custom tags to control its integration step. + + +___ + +
+ + +
+Deadline: Added support for multiple install dirs in Deadline #4451 + +SearchDirectoryList returns FIRST existing so if you would have multiple OP install dirs, it won't search for appropriate version in later ones. + + +___ + +
+ + +
+Ftrack: Upload reviewables with original name #4483 + +Ftrack can integrate reviewables with original filenames. + +As ftrack have restrictions about names of components the only way how to achieve the result was to upload the same file twice, one with required name and one with origin name. + + +___ + +
+ + +
+TVPaint: Ignore transparency in Render Pass #4499 + +It is possible to ignore layers transparency during Render Pass extraction. + +Render pass extraction does not respect opacity of TVPaint layers set in scene during extraction. It can be enabled/disabled in settings. + + +___ + +
+ + +
+Anatomy: Preparation for different root overrides #4521 + +Prepare Anatomy to handle only 'studio' site override on it's own. + +Change how Anatomy fill root overrides based on requested site name. The logic which decide what is active site was moved to sync server addon and the same for receiving root overrides of local site. The Anatomy resolve only studio site overrides anything else is handled by sync server. BaseAnatomy only expect root overrides value and does not need site name. Validation of site name happens in sync server same as resolving if site name is local or not. + + +___ + +
+ + +
+Nuke | Global: colormanaged plugin in collection #4556 + +Colormanaged extractor had changed to Mixin class so it can be added to any stage of publishing rather then just to Exctracting.Nuke is no collecting colorspaceData to representation collected on already rendered images. + +Mixin class can no be used as secondary parent in publishing plugins. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+look publishing and srgb colorspace in maya #4276 + +Check the OCIO color management is enabled before doing linearize colorspace for converting the texture maps into tx files. + +Check whether the OCIO color management is enabled before the condition of converting the texture to tx extension. + + +___ + +
+ + +
+Maya: extract Thumbnail "No active model panel found" - OP-3849 #4421 + +Error when extracting playblast with no model panel. + +If `project_settings/maya/publish/ExtractPlayblast/capture_preset/Viewport Options/override_viewport_options` were off and publishing without showing any model panel, the extraction would fail. + + +___ + +
+ + +
+Maya: Fix setting scene fps with float input #4488 + +Returned value of float fps on integer values would return float. + +This PR fixes the case when switching between integer fps values for example 24 > 25. Issue was when setting the scene fps, the original float value was used which makes it unpredictable whether the value is float or integer when mapping the fps values. + + +___ + +
+ + +
+Maya: Multipart fix #4497 + +Fix multipart logic in render products. + +Each renderer has a different way of defining whether output images is multipart, so we need to define it for each renderer. Also before the `multipart` class variable was defined multiple times in several places, which made it tricky to debug where `multipart` was defined. Now its created on initialization and referenced as `self.multipart` + + +___ + +
+ + +
+Maya: Set pool on tile assembly - OP-2012 #4520 + +Set pool on tile assembly + +Pool for publishing and tiling jobs, need to use the settings (`project_settings/deadline/publish/ProcessSubmittedJobOnFarm/deadline_pool`) else fallback on primary pool (`project_settings/deadline/publish/CollectDeadlinePools/primary_pool`) + + +___ + +
+ + +
+Maya: Extract review with handles #4527 + +Review was not extracting properly with/without handles. + +Review instance was not created properly resulting in the frame range on the instance including handles. + + +___ + +
+ + +
+Maya: Fix broken lib. #4529 + +Fix broken lib. + +This commit from this PR broke the Maya lib module. + + +___ + +
+ + +
+Maya: Validate model name - OP-4983 #4539 + +Validate model name issues. + +Couple of issues with validate model name; +- missing platform extraction from settings +- map function should be list comprehension +- code cosmetics + + +___ + +
+ + +
+Maya: SkeletalMesh family loadable as reference #4573 + +In Maya, fix the SkeletalMesh family not loadable as reference. + + +___ + +
+ + +
+Unreal: fix loaders because of missing AssetContainer #4536 + +Fixing Unreal loaders, where changes in OpenPype Unreal integration plugin deleted AssetContainer. + +`AssetContainer` and `AssetContainerFactory` are still used to mark loaded instances. Because of optimizations in Integration plugin we've accidentally removed them but that broke loader. + + +___ + +
+ + +
+3dsmax unable to delete loaded asset in the scene inventory #4507 + +Fix the bug of being unable to delete loaded asset in the Scene Inventory + +Fix the bug of being unable to delete loaded asset in the Scene Inventory + + +___ + +
+ + +
+Hiero/Nuke: originalBasename editorial publishing and loading #4453 + +Publishing and loading `originalBasename` is working as expected + +Frame-ranges on version document is now correctly defined to fit original media frame range which is published. It means loading is now correctly identifying frame start and end on clip loader in Nuke. + + +___ + +
+ + +
+Nuke: Fix workfile template placeholder creation #4512 + +Template placeholder creation was erroring out in Nuke due to the Workfile template builder not being able to find any of the plugins for the Nuke host. + +Move `get_workfile_build_placeholder_plugins` function to NukeHost class as workfile template builder expects. + + +___ + +
+ + +
+Nuke: creator farm attributes from deadline submit plugin settings #4519 + +Defaults in farm attributes are sourced from settings. + +Settings for deadline nuke submitter are now used during nuke render and prerender creator plugins. + + +___ + +
+ + +
+Nuke: fix clip sequence loading #4574 + +Nuke is loading correctly clip from image sequence created without "{originalBasename}" token in anatomy template. + + +___ + +
+ + +
+Fusion: Fix files collection and small bug-fixes #4423 + +Fixed Fusion review-representation and small bug-fixes + +This fixes the problem with review-file generation that stopped the publishing on second publish before the fix.The problem was that Fusion simply looked at all the files in the render-folder instead of only gathering the needed frames for the review.Also includes a fix to get the handle start/end that before throw an error if the data didn't exist (like from a kitsu sync). + + +___ + +
+ + +
+Fusion: Updated render_local.py to not only process the first instance #4522 + +Moved the `__hasRun` to `render_once()` so the check only happens with the rendering. Currently only the first render node gets the representations added.Critical PR + + +___ + +
+ + +
+Fusion: Load sequence fix filepath resolving from representation #4580 + +Resolves issue mentioned on discord by @movalex:The loader was incorrectly trying to find the file in the publish folder which resulted in just picking 'any first file'. + +This gets the filepath from representation instead of taking the first file from listing files from publish folder. + + +___ + +
+ + +
+Fusion: Fix review burnin start and end frame #4590 + +Fix the burnin start and end frame for reviews. Without this the asset document's start and end handle would've been added to the _burnin_ frame range even though that would've been incorrect since the handles are based on the comp saver's render range instead. + + +___ + +
+ + +
+Harmony: missing set of frame range when opening scene #4485 + +Frame range gets set from DB everytime scene is opened. + +Added also check for not up-to-date loaded containers. + + +___ + +
+ + +
+Photoshop: context is not changed in publisher #4570 + +When PS is already open and artists launch new task, it should keep only opened PS open, but change context. + +Problem were occurring in Workfile app where under new task files from old task were shown. This fixes this and adds opening of last workfile for new context if workfile exists. + + +___ + +
+ + +
+hiero: fix effect item node class #4543 + +Collected effect name after renaming is saving correct class name. + + +___ + +
+ + +
+Bugfix/OP-4616 vray multipart #4297 + +This fixes a bug where multipart vray renders would not make a review in Ftrack. + + +___ + +
+ + +
+Maya: Fix changed location of reset_frame_range #4491 + +Location in commands caused cyclic import + + +___ + +
+ + +
+global: source template fixed frame duplication #4503 + +Duplication is not happening. + +Template is using `originalBasename` which already assume all necessary elements are part of the file name so there was no need for additional optional name elements. + + +___ + +
+ + +
+Deadline: Hint to use Python 3 #4518 + +Added shebank to give deadline hint which python should be used. + +Deadline has issues with Python 2 (especially with `os.scandir`). When a shebank is added to file header deadline will use python 3 mode instead of python 2 which fix the issue. + + +___ + +
+ + +
+Publisher: Prevent access to create tab after publish start #4528 + +Prevent access to create tab after publish start. + +Disable create button in instance view on publish start and enable it again on reset. Even with that make sure that it is not possible to go to create tab if the tab is disabled. + + +___ + +
+ + +
+Color Transcoding: store target_colorspace as new colorspace #4544 + +When transcoding into new colorspace, representation must carry this information instead original color space. + + +___ + +
+ + +
+Deadline: fix submit_publish_job #4552 + +Fix submit_publish_job + +Resolves #4541 + + +___ + +
+ + +
+Kitsu: Fix task itteration in update-op-with-zou #4577 + +From the last PR (https://github.com/ynput/OpenPype/pull/4425) a comment-commit last second messed up the code and resulted in two lines being the same, crashing the script. This PR fixes that. +___ + +
+ + +
+AttrDefs: Fix type for PySide6 #4584 + +Use right type in signal emit for value change of attribute definitions. + +Changed `UUID` type to `str`. This is not an issue with PySide2 but it is with PySide6. + + +___ + +
+ +### **🔀 Refactored code** + + +
+Scene Inventory: Avoid using ObjectId #4524 + +Avoid using conversion to ObjectId type in scene inventory tool. + +Preparation for AYON compatibility where ObjectId won't be used for ids. Representation ids from loaded containers are not converted to ObjectId but kept as strings which also required some changes when working with representation documents. + + +___ + +
+ +### **Merged pull requests** + + +
+SiteSync: host dirmap is not working properly #4563 + +If artists uses SiteSync with real remote (gdrive, dropbox, sftp) drive, Local Settings were throwing error `string indices must be integers`. + +Logic was reworked to provide only `local_drive` values to be overrriden by Local Settings. If remote site is `gdrive` etc. mapping to `studio` is provided as it is expected that workfiles will have imported from `studio` location and not from `gdrive` folder.Also Nuke dirmap was reworked to be less verbose and much faster. + + +___ + +
+ + +
+General: Input representation ids are not ObjectIds #4576 + +Don't use `ObjectId` as representation ids during publishing. + +Representation ids are kept as strings during publishing instead of converting them to `ObjectId`. This change is pre-requirement for AYON connection.Inputs are used for integration of links and for farm publishing (or at least it looks like). + + +___ + +
+ + +
+Shotgrid: Fixes on Deadline submissions #4498 + +A few other bug fixes for getting Nuke submission to Deadline work smoothly using Shotgrid integration. + +Continuing on the work done on this other PR this fixes a few other bugs I came across with further tests. + + +___ + +
+ + +
+Fusion: New Publisher #3892 + +This converts the old publishing system to the new one. It implements Fusion as a new host addon. + + +- Create button removed in OpenPype menu in favor of the new Publisher +- Draft refactor validations to raise `PublishValidationError` +- Implement Creator for New Publisher +- Implement Fusion as Host addon + + +___ + +
+ + +
+Make Kitsu work with Tray Publisher, added kitsureview tag, fixed sync-problems. #4425 + +Make Kitsu work with Tray Publisher, added kitsureview tag, fixed sync-problems. + +This PR updates the way the module gather info for the current publish so it now works with Tray Publisher.It fixes the data that gets synced from Kitsu to OP so all needed data gets registered even if it doesn't exist on Kitsus side.It also adds the tag "Add review to Kitsu" and adds it to Burn In so previews gets generated by default to Kitsu. + + +___ + +
+ + +
+Maya: V-Ray Set Image Format from settings #4566 + +Resolves #4565 + +Set V-Ray Image Format using settings. + + +___ + +
+ + + + ## [3.15.1](https://github.com/ynput/OpenPype/tree/3.15.1) [Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.0...3.15.1) diff --git a/openpype/cli.py b/openpype/cli.py index 5c47088a44..a650a9fdcc 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -367,11 +367,15 @@ def run(script): "--timeout", help="Provide specific timeout value for test case", default=None) +@click.option("-so", + "--setup_only", + help="Only create dbs, do not run tests", + default=None) def runtests(folder, mark, pyargs, test_data_folder, persist, app_variant, - timeout): + timeout, setup_only): """Run all automatic tests after proper initialization via start.py""" PypeCommands().run_tests(folder, mark, pyargs, test_data_folder, - persist, app_variant, timeout) + persist, app_variant, timeout, setup_only) @main.command() diff --git a/openpype/host/dirmap.py b/openpype/host/dirmap.py index 347c5fbf85..1d084cccad 100644 --- a/openpype/host/dirmap.py +++ b/openpype/host/dirmap.py @@ -39,7 +39,6 @@ class HostDirmap(object): self._project_settings = project_settings self._sync_module = sync_module # to limit reinit of Modules self._log = None - self._mapping = None # cache mapping @property def sync_module(self): @@ -70,29 +69,28 @@ class HostDirmap(object): """Run host dependent remapping from source_path to destination_path""" pass - def process_dirmap(self): + def process_dirmap(self, mapping=None): # type: (dict) -> None """Go through all paths in Settings and set them using `dirmap`. If artists has Site Sync enabled, take dirmap mapping directly from Local Settings when artist is syncing workfile locally. - Args: - project_settings (dict): Settings for current project. """ - if not self._mapping: - self._mapping = self.get_mappings(self.project_settings) - if not self._mapping: + if not mapping: + mapping = self.get_mappings() + if not mapping: return - self.log.info("Processing directory mapping ...") self.on_enable_dirmap() - self.log.info("mapping:: {}".format(self._mapping)) - for k, sp in enumerate(self._mapping["source-path"]): - dst = self._mapping["destination-path"][k] + for k, sp in enumerate(mapping["source-path"]): + dst = mapping["destination-path"][k] try: + # add trailing slash if missing + sp = os.path.join(sp, '') + dst = os.path.join(dst, '') print("{} -> {}".format(sp, dst)) self.dirmap_routine(sp, dst) except IndexError: @@ -110,28 +108,24 @@ class HostDirmap(object): ) continue - def get_mappings(self, project_settings): + def get_mappings(self): """Get translation from source-path to destination-path. It checks if Site Sync is enabled and user chose to use local site, in that case configuration in Local Settings takes precedence """ - local_mapping = self._get_local_sync_dirmap(project_settings) dirmap_label = "{}-dirmap".format(self.host_name) - if ( - not self.project_settings[self.host_name].get(dirmap_label) - and not local_mapping - ): - return {} - mapping_settings = self.project_settings[self.host_name][dirmap_label] - mapping_enabled = mapping_settings["enabled"] or bool(local_mapping) + mapping_sett = self.project_settings[self.host_name].get(dirmap_label, + {}) + local_mapping = self._get_local_sync_dirmap() + mapping_enabled = mapping_sett.get("enabled") or bool(local_mapping) if not mapping_enabled: return {} mapping = ( local_mapping - or mapping_settings["paths"] + or mapping_sett["paths"] or {} ) @@ -141,28 +135,27 @@ class HostDirmap(object): or not mapping.get("source-path") ): return {} + self.log.info("Processing directory mapping ...") + self.log.info("mapping:: {}".format(mapping)) return mapping - def _get_local_sync_dirmap(self, project_settings): + def _get_local_sync_dirmap(self): """ Returns dirmap if synch to local project is enabled. Only valid mapping is from roots of remote site to local site set in Local Settings. - Args: - project_settings (dict) Returns: dict : { "source-path": [XXX], "destination-path": [YYYY]} """ + project_name = os.getenv("AVALON_PROJECT") mapping = {} - - if not project_settings["global"]["sync_server"]["enabled"]: + if (not self.sync_module.enabled or + project_name not in self.sync_module.get_enabled_projects()): return mapping - project_name = os.getenv("AVALON_PROJECT") - active_site = self.sync_module.get_local_normalized_site( self.sync_module.get_active_site(project_name)) remote_site = self.sync_module.get_local_normalized_site( @@ -171,11 +164,7 @@ class HostDirmap(object): "active {} - remote {}".format(active_site, remote_site) ) - if ( - active_site == "local" - and project_name in self.sync_module.get_enabled_projects() - and active_site != remote_site - ): + if active_site == "local" and active_site != remote_site: sync_settings = self.sync_module.get_sync_project_setting( project_name, exclude_locals=False, @@ -188,7 +177,15 @@ class HostDirmap(object): self.log.debug("local overrides {}".format(active_overrides)) self.log.debug("remote overrides {}".format(remote_overrides)) + current_platform = platform.system().lower() + remote_provider = self.sync_module.get_provider_for_site( + project_name, remote_site + ) + # dirmap has sense only with regular disk provider, in the workfile + # wont be root on cloud or sftp provider + if remote_provider != "local_drive": + remote_site = "studio" for root_name, active_site_dir in active_overrides.items(): remote_site_dir = ( remote_overrides.get(root_name) diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py index c6d7a73a04..fbd7606cd7 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py +++ b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py @@ -39,3 +39,5 @@ class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): context.data["frameEnd"] = int(end) context.data["frameStartHandle"] = int(global_start) context.data["frameEndHandle"] = int(global_end) + context.data["handleStart"] = int(start) - int(global_start) + context.data["handleEnd"] = int(global_end) - int(end) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 1e6d095cc2..af227f03db 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -39,6 +39,8 @@ class CollectInstanceData(pyblish.api.InstancePlugin): "frameEnd": context.data["frameEnd"], "frameStartHandle": context.data["frameStartHandle"], "frameEndHandle": context.data["frameStartHandle"], + "handleStart": context.data["handleStart"], + "handleEnd": context.data["handleEnd"], "fps": context.data["fps"], }) diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml index c08114b71b..8e24ce3b99 100644 --- a/openpype/hosts/houdini/startup/MainMenuCommon.xml +++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml @@ -10,7 +10,7 @@ import hou from openpype.tools.utils import host_tools parent = hou.qt.mainWindow() -host_tools.show_creator(parent) +host_tools.show_publisher(parent, tab="create") ]]> @@ -30,7 +30,7 @@ host_tools.show_loader(parent=parent, use_context=True) import hou from openpype.tools.utils import host_tools parent = hou.qt.mainWindow() -host_tools.show_publisher(parent) +host_tools.show_publisher(parent, tab="publish") ]]> diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 954576f02e..a670737b66 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3576,6 +3576,65 @@ def get_color_management_output_transform(): return colorspace +def image_info(file_path): + # type: (str) -> dict + """Based on tha texture path, get its bit depth and format information. + Take reference from makeTx.py in Arnold: + ImageInfo(filename): Get Image Information for colorspace + AiTextureGetFormat(filename): Get Texture Format + AiTextureGetBitDepth(filename): Get Texture bit depth + Args: + file_path (str): Path to the texture file. + Returns: + dict: Dictionary with the information about the texture file. + """ + from arnold import ( + AiTextureGetBitDepth, + AiTextureGetFormat + ) + # Get Texture Information + img_info = {'filename': file_path} + if os.path.isfile(file_path): + img_info['bit_depth'] = AiTextureGetBitDepth(file_path) # noqa + img_info['format'] = AiTextureGetFormat(file_path) # noqa + else: + img_info['bit_depth'] = 8 + img_info['format'] = "unknown" + return img_info + + +def guess_colorspace(img_info): + # type: (dict) -> str + """Guess the colorspace of the input image filename. + Note: + Reference from makeTx.py + Args: + img_info (dict): Image info generated by :func:`image_info` + Returns: + str: color space name use in the `--colorconvert` + option of maketx. + """ + from arnold import ( + AiTextureInvalidate, + # types + AI_TYPE_BYTE, + AI_TYPE_INT, + AI_TYPE_UINT + ) + try: + if img_info['bit_depth'] <= 16: + if img_info['format'] in (AI_TYPE_BYTE, AI_TYPE_INT, AI_TYPE_UINT): # noqa + return 'sRGB' + else: + return 'linear' + # now discard the image file as AiTextureGetFormat has loaded it + AiTextureInvalidate(img_info['filename']) # noqa + except ValueError: + print(("[maketx] Error: Could not guess" + "colorspace for {}").format(img_info["filename"])) + return "linear" + + def len_flattened(components): """Return the length of the list as if it was flattened. diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index f19deb0351..7d44d4eaa6 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -336,7 +336,8 @@ class RenderSettings(object): ) # Set render file format to exr - cmds.setAttr("{}.imageFormatStr".format(node), "exr", type="string") + ext = vray_render_presets["image_format"] + cmds.setAttr("{}.imageFormatStr".format(node), ext, type="string") # animType cmds.setAttr("{}.animType".format(node), 1) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index df07a674dc..efeddcfbe4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -16,6 +16,7 @@ import pyblish.api from openpype.lib import source_hash, run_subprocess from openpype.pipeline import legacy_io, publish from openpype.hosts.maya.api import lib +from openpype.hosts.maya.api.lib import image_info, guess_colorspace # Modes for transfer COPY = 1 @@ -367,16 +368,25 @@ class ExtractLook(publish.Extractor): for filepath in files_metadata: linearize = False - if do_maketx and files_metadata[filepath]["color_space"].lower() == "srgb": # noqa: E501 - linearize = True - # set its file node to 'raw' as tx will be linearized - files_metadata[filepath]["color_space"] = "Raw" + # if OCIO color management enabled + # it won't take the condition of the files_metadata + + ocio_maya = cmds.colorManagementPrefs(q=True, + cmConfigFileEnabled=True, + cmEnabled=True) + + if do_maketx and not ocio_maya: + if files_metadata[filepath]["color_space"].lower() == "srgb": # noqa: E501 + linearize = True + # set its file node to 'raw' as tx will be linearized + files_metadata[filepath]["color_space"] = "Raw" # if do_maketx: # color_space = "Raw" source, mode, texture_hash = self._process_texture( filepath, + resource, do_maketx, staging=staging_dir, linearize=linearize, @@ -482,7 +492,8 @@ class ExtractLook(publish.Extractor): resources_dir, basename + ext ) - def _process_texture(self, filepath, do_maketx, staging, linearize, force): + def _process_texture(self, filepath, resource, + do_maketx, staging, linearize, force): """Process a single texture file on disk for publishing. This will: 1. Check whether it's already published, if so it will do hardlink @@ -524,10 +535,47 @@ class ExtractLook(publish.Extractor): texture_hash ] if linearize: - self.log.info("tx: converting sRGB -> linear") - additional_args.extend(["--colorconvert", "sRGB", "linear"]) + if cmds.colorManagementPrefs(query=True, cmEnabled=True): + render_colorspace = cmds.colorManagementPrefs(query=True, + renderingSpaceName=True) # noqa + config_path = cmds.colorManagementPrefs(query=True, + configFilePath=True) # noqa + if not os.path.exists(config_path): + raise RuntimeError("No OCIO config path found!") + + color_space_attr = resource["node"] + ".colorSpace" + try: + color_space = cmds.getAttr(color_space_attr) + except ValueError: + # node doesn't have color space attribute + if cmds.loadPlugin("mtoa", quiet=True): + img_info = image_info(filepath) + color_space = guess_colorspace(img_info) + else: + color_space = "Raw" + self.log.info("tx: converting {0} -> {1}".format(color_space, render_colorspace)) # noqa + + additional_args.extend(["--colorconvert", + color_space, + render_colorspace]) + else: + + if cmds.loadPlugin("mtoa", quiet=True): + img_info = image_info(filepath) + color_space = guess_colorspace(img_info) + if color_space == "sRGB": + self.log.info("tx: converting sRGB -> linear") + additional_args.extend(["--colorconvert", + "sRGB", + "Raw"]) + else: + self.log.info("tx: texture's colorspace " + "is already linear") + else: + self.log.warning("cannot guess the colorspace" + "color conversion won't be available!") # noqa + - config_path = get_ocio_config_path("nuke-default") additional_args.extend(["--colorconfig", config_path]) # Ensure folder exists if not os.path.exists(os.path.dirname(converted)): diff --git a/openpype/hosts/maya/plugins/publish/validate_look_color_space.py b/openpype/hosts/maya/plugins/publish/validate_look_color_space.py new file mode 100644 index 0000000000..b1bdeb7541 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_look_color_space.py @@ -0,0 +1,26 @@ +from maya import cmds + +import pyblish.api +from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline import PublishValidationError + + +class ValidateMayaColorSpace(pyblish.api.InstancePlugin): + """ + Check if the OCIO Color Management and maketx options + enabled at the same time + """ + + order = ValidateContentsOrder + families = ['look'] + hosts = ['maya'] + label = 'Color Management with maketx' + + def process(self, instance): + ocio_maya = cmds.colorManagementPrefs(q=True, + cmConfigFileEnabled=True, + cmEnabled=True) + maketx = instance.data["maketx"] + + if ocio_maya and maketx: + raise PublishValidationError("Maya is color managed and maketx option is on. OpenPype doesn't support this combination yet.") # noqa diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index a5a631cc70..2a14096f0e 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2861,10 +2861,10 @@ class NukeDirmap(HostDirmap): pass def dirmap_routine(self, source_path, destination_path): - log.debug("{}: {}->{}".format(self.file_name, - source_path, destination_path)) source_path = source_path.lower().replace(os.sep, '/') destination_path = destination_path.lower().replace(os.sep, '/') + log.debug("Map: {} with: {}->{}".format(self.file_name, + source_path, destination_path)) if platform.system().lower() == "windows": self.file_name = self.file_name.lower().replace( source_path, destination_path) @@ -2878,6 +2878,7 @@ class DirmapCache: _project_name = None _project_settings = None _sync_module = None + _mapping = None @classmethod def project_name(cls): @@ -2897,6 +2898,36 @@ class DirmapCache: cls._sync_module = ModulesManager().modules_by_name["sync_server"] return cls._sync_module + @classmethod + def mapping(cls): + return cls._mapping + + @classmethod + def set_mapping(cls, mapping): + cls._mapping = mapping + + +def dirmap_file_name_filter(file_name): + """Nuke callback function with single full path argument. + + Checks project settings for potential mapping from source to dest. + """ + + dirmap_processor = NukeDirmap( + file_name, + "nuke", + DirmapCache.project_name(), + DirmapCache.project_settings(), + DirmapCache.sync_module(), + ) + if not DirmapCache.mapping(): + DirmapCache.set_mapping(dirmap_processor.get_mappings()) + + dirmap_processor.process_dirmap(DirmapCache.mapping()) + if os.path.exists(dirmap_processor.file_name): + return dirmap_processor.file_name + return file_name + @contextlib.contextmanager def node_tempfile(): @@ -2942,25 +2973,6 @@ def duplicate_node(node): return dupli_node -def dirmap_file_name_filter(file_name): - """Nuke callback function with single full path argument. - - Checks project settings for potential mapping from source to dest. - """ - - dirmap_processor = NukeDirmap( - file_name, - "nuke", - DirmapCache.project_name(), - DirmapCache.project_settings(), - DirmapCache.sync_module(), - ) - dirmap_processor.process_dirmap() - if os.path.exists(dirmap_processor.file_name): - return dirmap_processor.file_name - return file_name - - def get_group_io_nodes(nodes): """Get the input and the output of a group of nodes.""" diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 4c9f8258f5..da12bc75de 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -3,7 +3,14 @@ import os import copy from pathlib import Path +from openpype.widgets.splash_screen import SplashScreen +from qtpy import QtCore +from openpype.hosts.unreal.ue_workers import ( + UEProjectGenerationWorker, + UEPluginInstallWorker +) +from openpype import resources from openpype.lib import ( PreLaunchHook, ApplicationLaunchFailed, @@ -22,6 +29,7 @@ class UnrealPrelaunchHook(PreLaunchHook): shell script. """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -58,6 +66,78 @@ class UnrealPrelaunchHook(PreLaunchHook): # Return filename return filled_anatomy[workfile_template_key]["file"] + def exec_plugin_install(self, engine_path: Path, env: dict = None): + # set up the QThread and worker with necessary signals + env = env or os.environ + q_thread = QtCore.QThread() + ue_plugin_worker = UEPluginInstallWorker() + + q_thread.started.connect(ue_plugin_worker.run) + ue_plugin_worker.setup(engine_path, env) + ue_plugin_worker.moveToThread(q_thread) + + splash_screen = SplashScreen( + "Installing plugin", + resources.get_resource("app_icons", "ue4.png") + ) + + # set up the splash screen with necessary triggers + ue_plugin_worker.installing.connect( + splash_screen.update_top_label_text + ) + ue_plugin_worker.progress.connect(splash_screen.update_progress) + ue_plugin_worker.log.connect(splash_screen.append_log) + ue_plugin_worker.finished.connect(splash_screen.quit_and_close) + ue_plugin_worker.failed.connect(splash_screen.fail) + + splash_screen.start_thread(q_thread) + splash_screen.show_ui() + + if not splash_screen.was_proc_successful(): + raise ApplicationLaunchFailed("Couldn't run the application! " + "Plugin failed to install!") + + def exec_ue_project_gen(self, + engine_version: str, + unreal_project_name: str, + engine_path: Path, + project_dir: Path): + self.log.info(( + f"{self.signature} Creating unreal " + f"project [ {unreal_project_name} ]" + )) + + q_thread = QtCore.QThread() + ue_project_worker = UEProjectGenerationWorker() + ue_project_worker.setup( + engine_version, + unreal_project_name, + engine_path, + project_dir + ) + ue_project_worker.moveToThread(q_thread) + q_thread.started.connect(ue_project_worker.run) + + splash_screen = SplashScreen( + "Initializing UE project", + resources.get_resource("app_icons", "ue4.png") + ) + + ue_project_worker.stage_begin.connect( + splash_screen.update_top_label_text + ) + ue_project_worker.progress.connect(splash_screen.update_progress) + ue_project_worker.log.connect(splash_screen.append_log) + ue_project_worker.finished.connect(splash_screen.quit_and_close) + ue_project_worker.failed.connect(splash_screen.fail) + + splash_screen.start_thread(q_thread) + splash_screen.show_ui() + + if not splash_screen.was_proc_successful(): + raise ApplicationLaunchFailed("Couldn't run the application! " + "Failed to generate the project!") + def execute(self): """Hook entry method.""" workdir = self.launch_context.env["AVALON_WORKDIR"] @@ -137,23 +217,18 @@ class UnrealPrelaunchHook(PreLaunchHook): if self.launch_context.env.get(env_key): os.environ[env_key] = self.launch_context.env[env_key] - engine_path = detected[engine_version] + engine_path: Path = Path(detected[engine_version]) - unreal_lib.try_installing_plugin(Path(engine_path), os.environ) + if not unreal_lib.check_plugin_existence(engine_path): + self.exec_plugin_install(engine_path) project_file = project_path / unreal_project_filename - if not project_file.is_file(): - self.log.info(( - f"{self.signature} creating unreal " - f"project [ {unreal_project_name} ]" - )) - unreal_lib.create_unreal_project( - unreal_project_name, - engine_version, - project_path, - engine_path=Path(engine_path) - ) + if not project_file.is_file(): + self.exec_ue_project_gen(engine_version, + unreal_project_name, + engine_path, + project_path) self.launch_context.env["OPENPYPE_UNREAL_VERSION"] = engine_version # Append project file to launch arguments diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainer.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainer.cpp index 0bea9e3d78..06dcd67808 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainer.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainer.cpp @@ -30,7 +30,7 @@ void UAssetContainer::OnAssetAdded(const FAssetData& AssetData) // get asset path and class FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.AssetClassPath.ToString(); + FString assetFName = AssetData.ObjectPath.ToString(); UE_LOG(LogTemp, Log, TEXT("asset name %s"), *assetFName); // split path assetPath.ParseIntoArray(split, TEXT(" "), true); @@ -60,7 +60,7 @@ void UAssetContainer::OnAssetRemoved(const FAssetData& AssetData) // get asset path and class FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.AssetClassPath.ToString(); + FString assetFName = AssetData.ObjectPath.ToString(); // split path assetPath.ParseIntoArray(split, TEXT(" "), true); @@ -93,7 +93,7 @@ void UAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& // get asset path and class FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.AssetClassPath.ToString(); + FString assetFName = AssetData.ObjectPath.ToString(); // split path assetPath.ParseIntoArray(split, TEXT(" "), true); diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 28a5106042..86ce0bb033 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -252,7 +252,7 @@ def create_unreal_project(project_name: str, with open(project_file.as_posix(), mode="r+") as pf: pf_json = json.load(pf) - pf_json["EngineAssociation"] = _get_build_id(engine_path, ue_version) + pf_json["EngineAssociation"] = get_build_id(engine_path, ue_version) pf.seek(0) json.dump(pf_json, pf, indent=4) pf.truncate() @@ -338,7 +338,7 @@ def get_path_to_ubt(engine_path: Path, ue_version: str) -> Path: return Path(u_build_tool_path) -def _get_build_id(engine_path: Path, ue_version: str) -> str: +def get_build_id(engine_path: Path, ue_version: str) -> str: ue_modules = Path() if platform.system().lower() == "windows": ue_modules_path = engine_path / "Engine/Binaries/Win64" @@ -365,6 +365,26 @@ def _get_build_id(engine_path: Path, ue_version: str) -> str: return "{" + loaded_modules.get("BuildId") + "}" +def check_plugin_existence(engine_path: Path, env: dict = None) -> bool: + env = env or os.environ + integration_plugin_path: Path = Path(env.get("OPENPYPE_UNREAL_PLUGIN", "")) + + if not os.path.isdir(integration_plugin_path): + raise RuntimeError("Path to the integration plugin is null!") + + # Create a path to the plugin in the engine + op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/OpenPype" + + if not op_plugin_path.is_dir(): + return False + + if not (op_plugin_path / "Binaries").is_dir() \ + or not (op_plugin_path / "Intermediate").is_dir(): + return False + + return True + + def try_installing_plugin(engine_path: Path, env: dict = None) -> None: env = env or os.environ @@ -377,7 +397,6 @@ def try_installing_plugin(engine_path: Path, env: dict = None) -> None: op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/OpenPype" if not op_plugin_path.is_dir(): - print("--- OpenPype Plugin is not present. Installing ...") op_plugin_path.mkdir(parents=True, exist_ok=True) engine_plugin_config_path: Path = op_plugin_path / "Config" @@ -387,7 +406,6 @@ def try_installing_plugin(engine_path: Path, env: dict = None) -> None: if not (op_plugin_path / "Binaries").is_dir() \ or not (op_plugin_path / "Intermediate").is_dir(): - print("--- Binaries are not present. Building the plugin ...") _build_and_move_plugin(engine_path, op_plugin_path, env) diff --git a/openpype/hosts/unreal/ue_workers.py b/openpype/hosts/unreal/ue_workers.py new file mode 100644 index 0000000000..00f83a7d7a --- /dev/null +++ b/openpype/hosts/unreal/ue_workers.py @@ -0,0 +1,335 @@ +import json +import os +import platform +import re +import subprocess +from distutils import dir_util +from pathlib import Path +from typing import List + +import openpype.hosts.unreal.lib as ue_lib + +from qtpy import QtCore + + +def parse_comp_progress(line: str, progress_signal: QtCore.Signal(int)) -> int: + match = re.search('\[[1-9]+/[0-9]+\]', line) + if match is not None: + split: list[str] = match.group().split('/') + curr: float = float(split[0][1:]) + total: float = float(split[1][:-1]) + progress_signal.emit(int((curr / total) * 100.0)) + + +def parse_prj_progress(line: str, progress_signal: QtCore.Signal(int)) -> int: + match = re.search('@progress', line) + if match is not None: + percent_match = re.search('\d{1,3}', line) + progress_signal.emit(int(percent_match.group())) + + +class UEProjectGenerationWorker(QtCore.QObject): + finished = QtCore.Signal(str) + failed = QtCore.Signal(str) + progress = QtCore.Signal(int) + log = QtCore.Signal(str) + stage_begin = QtCore.Signal(str) + + ue_version: str = None + project_name: str = None + env = None + engine_path: Path = None + project_dir: Path = None + dev_mode = False + + def setup(self, ue_version: str, + project_name, + engine_path: Path, + project_dir: Path, + dev_mode: bool = False, + env: dict = None): + + self.ue_version = ue_version + self.project_dir = project_dir + self.env = env or os.environ + + preset = ue_lib.get_project_settings( + project_name + )["unreal"]["project_setup"] + + if dev_mode or preset["dev_mode"]: + self.dev_mode = True + + self.project_name = project_name + self.engine_path = engine_path + + def run(self): + # engine_path should be the location of UE_X.X folder + + ue_editor_exe = ue_lib.get_editor_exe_path(self.engine_path, + self.ue_version) + cmdlet_project = ue_lib.get_path_to_cmdlet_project(self.ue_version) + project_file = self.project_dir / f"{self.project_name}.uproject" + + print("--- Generating a new project ...") + # 1st stage + stage_count = 2 + if self.dev_mode: + stage_count = 4 + + self.stage_begin.emit(f'Generating a new UE project ... 1 out of ' + f'{stage_count}') + + commandlet_cmd = [f'{ue_editor_exe.as_posix()}', + f'{cmdlet_project.as_posix()}', + f'-run=OPGenerateProject', + f'{project_file.resolve().as_posix()}'] + + if self.dev_mode: + commandlet_cmd.append('-GenerateCode') + + gen_process = subprocess.Popen(commandlet_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + for line in gen_process.stdout: + decoded_line = line.decode(errors="replace") + print(decoded_line, end='') + self.log.emit(decoded_line) + gen_process.stdout.close() + return_code = gen_process.wait() + + if return_code and return_code != 0: + msg = 'Failed to generate ' + self.project_name \ + + f' project! Exited with return code {return_code}' + self.failed.emit(msg, return_code) + raise RuntimeError(msg) + + print("--- Project has been generated successfully.") + self.stage_begin.emit(f'Writing the Engine ID of the build UE ... 1' + f' out of {stage_count}') + + if not project_file.is_file(): + msg = "Failed to write the Engine ID into .uproject file! Can " \ + "not read!" + self.failed.emit(msg) + raise RuntimeError(msg) + + with open(project_file.as_posix(), mode="r+") as pf: + pf_json = json.load(pf) + pf_json["EngineAssociation"] = ue_lib.get_build_id( + self.engine_path, + self.ue_version + ) + print(pf_json["EngineAssociation"]) + pf.seek(0) + json.dump(pf_json, pf, indent=4) + pf.truncate() + print(f'--- Engine ID has been written into the project file') + + self.progress.emit(90) + if self.dev_mode: + # 2nd stage + self.stage_begin.emit(f'Generating project files ... 2 out of ' + f'{stage_count}') + + self.progress.emit(0) + ubt_path = ue_lib.get_path_to_ubt(self.engine_path, + self.ue_version) + + arch = "Win64" + if platform.system().lower() == "windows": + arch = "Win64" + elif platform.system().lower() == "linux": + arch = "Linux" + elif platform.system().lower() == "darwin": + # we need to test this out + arch = "Mac" + + gen_prj_files_cmd = [ubt_path.as_posix(), + "-projectfiles", + f"-project={project_file}", + "-progress"] + gen_proc = subprocess.Popen(gen_prj_files_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + for line in gen_proc.stdout: + decoded_line: str = line.decode(errors='replace') + print(decoded_line, end='') + self.log.emit(decoded_line) + parse_prj_progress(decoded_line, self.progress) + + gen_proc.stdout.close() + return_code = gen_proc.wait() + + if return_code and return_code != 0: + msg = 'Failed to generate project files! ' \ + f'Exited with return code {return_code}' + self.failed.emit(msg, return_code) + raise RuntimeError(msg) + + self.stage_begin.emit(f'Building the project ... 3 out of ' + f'{stage_count}') + self.progress.emit(0) + # 3rd stage + build_prj_cmd = [ubt_path.as_posix(), + f"-ModuleWithSuffix={self.project_name},3555", + arch, + "Development", + "-TargetType=Editor", + f'-Project={project_file}', + f'{project_file}', + "-IgnoreJunk"] + + build_prj_proc = subprocess.Popen(build_prj_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + for line in build_prj_proc.stdout: + decoded_line: str = line.decode(errors='replace') + print(decoded_line, end='') + self.log.emit(decoded_line) + parse_comp_progress(decoded_line, self.progress) + + build_prj_proc.stdout.close() + return_code = build_prj_proc.wait() + + if return_code and return_code != 0: + msg = 'Failed to build project! ' \ + f'Exited with return code {return_code}' + self.failed.emit(msg, return_code) + raise RuntimeError(msg) + + # ensure we have PySide2 installed in engine + + self.progress.emit(0) + self.stage_begin.emit(f'Checking PySide2 installation... {stage_count}' + f' out of {stage_count}') + python_path = None + if platform.system().lower() == "windows": + python_path = self.engine_path / ("Engine/Binaries/ThirdParty/" + "Python3/Win64/python.exe") + + if platform.system().lower() == "linux": + python_path = self.engine_path / ("Engine/Binaries/ThirdParty/" + "Python3/Linux/bin/python3") + + if platform.system().lower() == "darwin": + python_path = self.engine_path / ("Engine/Binaries/ThirdParty/" + "Python3/Mac/bin/python3") + + if not python_path: + msg = "Unsupported platform" + self.failed.emit(msg, 1) + raise NotImplementedError(msg) + if not python_path.exists(): + msg = f"Unreal Python not found at {python_path}" + self.failed.emit(msg, 1) + raise RuntimeError(msg) + subprocess.check_call( + [python_path.as_posix(), "-m", "pip", "install", "pyside2"] + ) + self.progress.emit(100) + self.finished.emit("Project successfully built!") + + +class UEPluginInstallWorker(QtCore.QObject): + finished = QtCore.Signal(str) + installing = QtCore.Signal(str) + failed = QtCore.Signal(str, int) + progress = QtCore.Signal(int) + log = QtCore.Signal(str) + + engine_path: Path = None + env = None + + def setup(self, engine_path: Path, env: dict = None, ): + self.engine_path = engine_path + self.env = env or os.environ + + def _build_and_move_plugin(self, plugin_build_path: Path): + uat_path: Path = ue_lib.get_path_to_uat(self.engine_path) + src_plugin_dir = Path(self.env.get("OPENPYPE_UNREAL_PLUGIN", "")) + + if not os.path.isdir(src_plugin_dir): + msg = "Path to the integration plugin is null!" + self.failed.emit(msg, 1) + raise RuntimeError(msg) + + if not uat_path.is_file(): + msg = "Building failed! Path to UAT is invalid!" + self.failed.emit(msg, 1) + raise RuntimeError(msg) + + temp_dir: Path = src_plugin_dir.parent / "Temp" + temp_dir.mkdir(exist_ok=True) + uplugin_path: Path = src_plugin_dir / "OpenPype.uplugin" + + # in order to successfully build the plugin, + # It must be built outside the Engine directory and then moved + build_plugin_cmd: List[str] = [f'{uat_path.as_posix()}', + 'BuildPlugin', + f'-Plugin={uplugin_path.as_posix()}', + f'-Package={temp_dir.as_posix()}'] + + build_proc = subprocess.Popen(build_plugin_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + for line in build_proc.stdout: + decoded_line: str = line.decode(errors='replace') + print(decoded_line, end='') + self.log.emit(decoded_line) + parse_comp_progress(decoded_line, self.progress) + + build_proc.stdout.close() + return_code = build_proc.wait() + + if return_code and return_code != 0: + msg = 'Failed to build plugin' \ + f' project! Exited with return code {return_code}' + self.failed.emit(msg, return_code) + raise RuntimeError(msg) + + # Copy the contents of the 'Temp' dir into the + # 'OpenPype' directory in the engine + dir_util.copy_tree(temp_dir.as_posix(), + plugin_build_path.as_posix()) + + # We need to also copy the config folder. + # The UAT doesn't include the Config folder in the build + plugin_install_config_path: Path = plugin_build_path / "Config" + src_plugin_config_path = src_plugin_dir / "Config" + + dir_util.copy_tree(src_plugin_config_path.as_posix(), + plugin_install_config_path.as_posix()) + + dir_util.remove_tree(temp_dir.as_posix()) + + def run(self): + src_plugin_dir = Path(self.env.get("OPENPYPE_UNREAL_PLUGIN", "")) + + if not os.path.isdir(src_plugin_dir): + msg = "Path to the integration plugin is null!" + self.failed.emit(msg, 1) + raise RuntimeError(msg) + + # Create a path to the plugin in the engine + op_plugin_path = self.engine_path / "Engine/Plugins/Marketplace" \ + "/OpenPype" + + if not op_plugin_path.is_dir(): + self.installing.emit("Installing and building the plugin ...") + op_plugin_path.mkdir(parents=True, exist_ok=True) + + engine_plugin_config_path = op_plugin_path / "Config" + engine_plugin_config_path.mkdir(exist_ok=True) + + dir_util._path_created = {} + + if not (op_plugin_path / "Binaries").is_dir() \ + or not (op_plugin_path / "Intermediate").is_dir(): + self.installing.emit("Building the plugin ...") + print("--- Building the plugin...") + + self._build_and_move_plugin(op_plugin_path) + + self.finished.emit("Plugin successfully installed") diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 28863c091a..5a4fa07e98 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -1472,13 +1472,15 @@ class SyncServerModule(OpenPypeModule, ITrayModule): return sync_settings - def get_all_site_configs(self, project_name=None): + def get_all_site_configs(self, project_name=None, + local_editable_only=False): """ Returns (dict) with all sites configured system wide. Args: project_name (str)(optional): if present, check if not disabled - + local_editable_only (bool)(opt): if True return only Local + Setting configurable (for LS UI) Returns: (dict): {'studio': {'provider':'local_drive'...}, 'MY_LOCAL': {'provider':....}} @@ -1499,9 +1501,21 @@ class SyncServerModule(OpenPypeModule, ITrayModule): if site_settings: detail.update(site_settings) system_sites[site] = detail - system_sites.update(self._get_default_site_configs(sync_enabled, project_name)) + if local_editable_only: + local_schema = SyncServerModule.get_local_settings_schema() + editable_keys = {} + for provider_code, editables in local_schema.items(): + editable_keys[provider_code] = ["enabled", "provider"] + for editable_item in editables: + editable_keys[provider_code].append(editable_item["key"]) + + for _, site in system_sites.items(): + provider = site["provider"] + for site_config_key in list(site.keys()): + if site_config_key not in editable_keys[provider]: + site.pop(site_config_key, None) return system_sites diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 932fdc9be4..dc5b3d63c3 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -270,7 +270,7 @@ class PypeCommands: pass def run_tests(self, folder, mark, pyargs, - test_data_folder, persist, app_variant, timeout): + test_data_folder, persist, app_variant, timeout, setup_only): """ Runs tests from 'folder' @@ -311,6 +311,9 @@ class PypeCommands: if timeout: args.extend(["--timeout", timeout]) + if setup_only: + args.extend(["--setup_only", setup_only]) + print("run_tests args: {}".format(args)) import pytest pytest.main(args) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index dca0b95293..63ba4542f3 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -330,6 +330,11 @@ "optional": true, "active": true }, + "ValidateMayaColorSpace": { + "enabled": true, + "optional": true, + "active": true + }, "ValidateAttributes": { "enabled": false, "attributes": {} diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 994e2d0032..3484f42f6b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -144,6 +144,10 @@ { "key": "ValidateShadingEngine", "label": "Validate Look Shading Engine Naming" + }, + { + "key": "ValidateMayaColorSpace", + "label": "ValidateMayaColorSpace" } ] }, diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 98ac9c871f..b3aa381d14 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -295,10 +295,10 @@ class SubsetWidget(QtWidgets.QWidget): self.model.set_grouping(state) def _subset_changed(self, text): - if hasattr(self.proxy, "setFilterRegularExpression"): - self.proxy.setFilterRegularExpression(text) - else: + if hasattr(self.proxy, "setFilterRegExp"): self.proxy.setFilterRegExp(text) + else: + self.proxy.setFilterRegularExpression(text) self.view.expandAll() def set_loading_state(self, loading, empty): diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index 3398743aec..680dfd5a51 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -482,10 +482,10 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel): return True # Filter by regex - if hasattr(self, "filterRegularExpression"): - regex = self.filterRegularExpression() - else: + if hasattr(self, "filterRegExp"): regex = self.filterRegExp() + else: + regex = self.filterRegularExpression() pattern = regex.pattern() if pattern: pattern = re.escape(pattern) diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 8a6e43f796..89424fd746 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -160,10 +160,10 @@ class SceneInventoryWindow(QtWidgets.QDialog): self._model.set_hierarchy_view(enabled) def _on_text_filter_change(self, text_filter): - if hasattr(self._proxy, "setFilterRegularExpression"): - self._proxy.setFilterRegularExpression(text_filter) - else: + if hasattr(self._proxy, "setFilterRegExp"): self._proxy.setFilterRegExp(text_filter) + else: + self._proxy.setFilterRegularExpression(text_filter) def _on_outdated_state_change(self): self._proxy.set_filter_outdated( diff --git a/openpype/tools/settings/local_settings/projects_widget.py b/openpype/tools/settings/local_settings/projects_widget.py index bdf291524c..4a4148d7cd 100644 --- a/openpype/tools/settings/local_settings/projects_widget.py +++ b/openpype/tools/settings/local_settings/projects_widget.py @@ -272,7 +272,7 @@ class SitesWidget(QtWidgets.QWidget): ) site_configs = sync_server_module.get_all_site_configs( - self._project_name) + self._project_name, local_editable_only=True) roots_entity = ( self.project_settings[PROJECT_ANATOMY_KEY][LOCAL_ROOTS_KEY] diff --git a/openpype/tools/settings/settings/search_dialog.py b/openpype/tools/settings/settings/search_dialog.py index 33a4d16e98..59750c02e1 100644 --- a/openpype/tools/settings/settings/search_dialog.py +++ b/openpype/tools/settings/settings/search_dialog.py @@ -27,10 +27,10 @@ class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel): if not parent.isValid(): return False - if hasattr(self, "filterRegularExpression"): - regex = self.filterRegularExpression() - else: + if hasattr(self, "filterRegExp"): regex = self.filterRegExp() + else: + regex = self.filterRegularExpression() pattern = regex.pattern() if pattern and regex.isValid(): @@ -111,10 +111,10 @@ class SearchEntitiesDialog(QtWidgets.QDialog): def _on_filter_timer(self): text = self._filter_edit.text() - if hasattr(self._proxy, "setFilterRegularExpression"): - self._proxy.setFilterRegularExpression(text) - else: + if hasattr(self._proxy, "setFilterRegExp"): self._proxy.setFilterRegExp(text) + else: + self._proxy.setFilterRegularExpression(text) # WARNING This expanding and resizing is relatively slow. self._view.expandAll() diff --git a/openpype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py b/openpype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py index 5c72e2049b..602faaa489 100644 --- a/openpype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py +++ b/openpype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py @@ -5,10 +5,10 @@ from qtpy import QtCore class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel): """Filters to the regex if any of the children matches allow parent""" def filterAcceptsRow(self, row, parent): - if hasattr(self, "filterRegularExpression"): - regex = self.filterRegularExpression() - else: + if hasattr(self, "filterRegExp"): regex = self.filterRegExp() + else: + regex = self.filterRegularExpression() pattern = regex.pattern() if pattern: model = self.sourceModel() diff --git a/openpype/tools/utils/models.py b/openpype/tools/utils/models.py index 270e00b2ef..94645af110 100644 --- a/openpype/tools/utils/models.py +++ b/openpype/tools/utils/models.py @@ -202,11 +202,20 @@ class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel): Use case: Filtering by string - parent won't be filtered if does not match the filter string but first checks if any children does. """ + + def __init__(self, *args, **kwargs): + super(RecursiveSortFilterProxyModel, self).__init__(*args, **kwargs) + recursive_enabled = False + if hasattr(self, "setRecursiveFilteringEnabled"): + self.setRecursiveFilteringEnabled(True) + recursive_enabled = True + self._recursive_enabled = recursive_enabled + def filterAcceptsRow(self, row, parent_index): - if hasattr(self, "filterRegularExpression"): - regex = self.filterRegularExpression() - else: + if hasattr(self, "filterRegExp"): regex = self.filterRegExp() + else: + regex = self.filterRegularExpression() pattern = regex.pattern() if pattern: @@ -219,8 +228,9 @@ class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel): # Check current index itself value = model.data(source_index, self.filterRole()) - if re.search(pattern, value, re.IGNORECASE): - return True + matched = bool(re.search(pattern, value, re.IGNORECASE)) + if matched or self._recursive_enabled: + return matched rows = model.rowCount(source_index) for idx in range(rows): diff --git a/openpype/version.py b/openpype/version.py index c7a5e9bea5..6ab03c2121 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.2-nightly.5" +__version__ = "3.15.2" diff --git a/openpype/widgets/README.md b/openpype/widgets/README.md new file mode 100644 index 0000000000..cda83a95d3 --- /dev/null +++ b/openpype/widgets/README.md @@ -0,0 +1,102 @@ +# Widgets + +## Splash Screen + +This widget is used for executing a monitoring progress of a process which has been executed on a different thread. + +To properly use this widget certain preparation has to be done in order to correctly execute the process and show the +splash screen. + +### Prerequisites + +In order to run a function or an operation on another thread, a `QtCore.QObject` class needs to be created with the +desired code. The class has to have a method as an entry point for the thread to execute the code. + +For utilizing the functionalities of the splash screen, certain signals need to be declared to let it know what is +happening in the thread and how is it progressing. It is also recommended to have a function to set up certain variables +which are needed in the worker's code + +For example: +```python +from qtpy import QtCore + +class ExampleWorker(QtCore.QObject): + + finished = QtCore.Signal() + failed = QtCore.Signal(str) + progress = QtCore.Signal(int) + log = QtCore.Signal(str) + stage_begin = QtCore.Signal(str) + + foo = None + bar = None + + def run(self): + # The code goes here + print("Hello world!") + self.finished.emit() + + def setup(self, + foo: str, + bar: str,): + self.foo = foo + self.bar = bar +``` + +### Creating the splash screen + +```python +import os +from qtpy import QtCore +from pathlib import Path +from openpype.widgets.splash_screen import SplashScreen +from openpype import resources + + +def exec_plugin_install( engine_path: Path, env: dict = None): + env = env or os.environ + q_thread = QtCore.QThread() + example_worker = ExampleWorker() + + q_thread.started.connect(example_worker.run) + example_worker.setup(engine_path, env) + example_worker.moveToThread(q_thread) + + splash_screen = SplashScreen("Executing process ...", + resources.get_openpype_icon_filepath()) + + # set up the splash screen with necessary events + example_worker.installing.connect(splash_screen.update_top_label_text) + example_worker.progress.connect(splash_screen.update_progress) + example_worker.log.connect(splash_screen.append_log) + example_worker.finished.connect(splash_screen.quit_and_close) + example_worker.failed.connect(splash_screen.fail) + + splash_screen.start_thread(q_thread) + splash_screen.show_ui() +``` + +In this example code, before executing the process the worker needs to be instantiated and moved onto a newly created +`QtCore.QThread` object. After this, needed signals have to be connected to the desired slots to make full use of +the splash screen. Finally, the `start_thread` and `show_ui` is called. + +**Note that when the `show_ui` function is called the thread is blocked until the splash screen quits automatically, or +it is closed by the user in case the process fails! The `start_thread` method in that case must be called before +showing the UI!** + +The most important signals are +```python +q_thread.started.connect(example_worker.run) +``` + and +```python +example_worker.finished.connect(splash_screen.quit_and_close) +``` + +These ensure that when the `start_thread` method is called (which takes as a parameter the `QtCore.QThread` object and +saves it as a reference), the `QThread` object starts and signals the worker to +start executing its own code. Once the worker is done and emits a signal that it has finished with the `quit_and_close` +slot, the splash screen quits the `QtCore.QThread` and closes itself. + +It is highly recommended to also use the `fail` slot in case an exception or other error occurs during the execution of +the worker's code (You would use in this case the `failed` signal in the `ExampleWorker`). diff --git a/openpype/widgets/splash_screen.py b/openpype/widgets/splash_screen.py new file mode 100644 index 0000000000..fffe143ea5 --- /dev/null +++ b/openpype/widgets/splash_screen.py @@ -0,0 +1,258 @@ +from qtpy import QtWidgets, QtCore, QtGui +from openpype import style, resources +from igniter.nice_progress_bar import NiceProgressBar + + +class SplashScreen(QtWidgets.QDialog): + """Splash screen for executing a process on another thread. It is able + to inform about the progress of the process and log given information. + """ + + splash_icon = None + top_label = None + show_log_btn: QtWidgets.QLabel = None + progress_bar = None + log_text: QtWidgets.QLabel = None + scroll_area: QtWidgets.QScrollArea = None + close_btn: QtWidgets.QPushButton = None + scroll_bar: QtWidgets.QScrollBar = None + + is_log_visible = False + is_scroll_auto = True + + thread_return_code = None + q_thread: QtCore.QThread = None + + def __init__(self, + window_title: str, + splash_icon=None, + window_icon=None): + """ + Args: + window_title (str): String which sets the window title + splash_icon (str | bytes | None): A resource (pic) which is used + for the splash icon + window_icon (str | bytes | None: A resource (pic) which is used for + the window's icon + """ + super(SplashScreen, self).__init__() + + if splash_icon is None: + splash_icon = resources.get_openpype_icon_filepath() + + if window_icon is None: + window_icon = resources.get_openpype_icon_filepath() + + self.splash_icon = splash_icon + self.setWindowIcon(QtGui.QIcon(window_icon)) + self.setWindowTitle(window_title) + self.init_ui() + + def was_proc_successful(self) -> bool: + if self.thread_return_code == 0: + return True + return False + + def start_thread(self, q_thread: QtCore.QThread): + """Saves the reference to this thread and starts it. + + Args: + q_thread (QtCore.QThread): A QThread containing a given worker + (QtCore.QObject) + + Returns: + None + """ + if not q_thread: + raise RuntimeError("Failed to run a worker thread! " + "The thread is null!") + + self.q_thread = q_thread + self.q_thread.start() + + @QtCore.Slot() + def quit_and_close(self): + """Quits the thread and closes the splash screen. Note that this means + the thread has exited with the return code 0! + + Returns: + None + """ + self.thread_return_code = 0 + self.q_thread.quit() + self.close() + + @QtCore.Slot() + def toggle_log(self): + if self.is_log_visible: + self.scroll_area.hide() + width = self.width() + self.adjustSize() + self.resize(width, self.height()) + else: + self.scroll_area.show() + self.scroll_bar.setValue(self.scroll_bar.maximum()) + self.resize(self.width(), 300) + + self.is_log_visible = not self.is_log_visible + + def show_ui(self): + """Shows the splash screen. BEWARE THAT THIS FUNCTION IS BLOCKING + (The execution of code can not proceed further beyond this function + until the splash screen is closed!) + + Returns: + None + """ + self.show() + self.exec_() + + def init_ui(self): + self.resize(450, 100) + self.setMinimumWidth(250) + self.setStyleSheet(style.load_stylesheet()) + + # Top Section + self.top_label = QtWidgets.QLabel(self) + self.top_label.setText("Starting process ...") + self.top_label.setWordWrap(True) + + icon = QtWidgets.QLabel(self) + icon.setPixmap(QtGui.QPixmap(self.splash_icon)) + icon.setFixedHeight(45) + icon.setFixedWidth(45) + icon.setScaledContents(True) + + self.close_btn = QtWidgets.QPushButton(self) + self.close_btn.setText("Quit") + self.close_btn.clicked.connect(self.close) + self.close_btn.setFixedWidth(80) + self.close_btn.hide() + + self.show_log_btn = QtWidgets.QPushButton(self) + self.show_log_btn.setText("Show log") + self.show_log_btn.setFixedWidth(80) + self.show_log_btn.clicked.connect(self.toggle_log) + + button_layout = QtWidgets.QVBoxLayout() + button_layout.addWidget(self.show_log_btn) + button_layout.addWidget(self.close_btn) + + # Progress Bar + self.progress_bar = NiceProgressBar() + self.progress_bar.setValue(0) + self.progress_bar.setAlignment(QtCore.Qt.AlignTop) + + # Log Content + self.scroll_area = QtWidgets.QScrollArea(self) + self.scroll_area.hide() + log_widget = QtWidgets.QWidget(self.scroll_area) + self.scroll_area.setWidgetResizable(True) + self.scroll_area.setHorizontalScrollBarPolicy( + QtCore.Qt.ScrollBarAlwaysOn + ) + self.scroll_area.setVerticalScrollBarPolicy( + QtCore.Qt.ScrollBarAlwaysOn + ) + self.scroll_area.setWidget(log_widget) + + self.scroll_bar = self.scroll_area.verticalScrollBar() + self.scroll_bar.sliderMoved.connect(self.on_scroll) + + self.log_text = QtWidgets.QLabel(self) + self.log_text.setText('') + self.log_text.setAlignment(QtCore.Qt.AlignTop) + + log_layout = QtWidgets.QVBoxLayout(log_widget) + log_layout.addWidget(self.log_text) + + top_layout = QtWidgets.QHBoxLayout() + top_layout.setAlignment(QtCore.Qt.AlignTop) + top_layout.addWidget(icon) + top_layout.addSpacing(10) + top_layout.addWidget(self.top_label) + top_layout.addSpacing(10) + top_layout.addLayout(button_layout) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addLayout(top_layout) + main_layout.addSpacing(10) + main_layout.addWidget(self.progress_bar) + main_layout.addSpacing(10) + main_layout.addWidget(self.scroll_area) + + self.setWindowFlags( + QtCore.Qt.Window + | QtCore.Qt.CustomizeWindowHint + | QtCore.Qt.WindowTitleHint + | QtCore.Qt.WindowMinimizeButtonHint + ) + + desktop_rect = QtWidgets.QApplication.desktop().availableGeometry(self) + center = desktop_rect.center() + self.move( + center.x() - (self.width() * 0.5), + center.y() - (self.height() * 0.5) + ) + + @QtCore.Slot(int) + def update_progress(self, value: int): + self.progress_bar.setValue(value) + + @QtCore.Slot(str) + def update_top_label_text(self, text: str): + self.top_label.setText(text) + + @QtCore.Slot(str, str) + def append_log(self, text: str, end: str = ''): + """A slot used for receiving log info and appending it to scroll area's + content. + Args: + text (str): A log text that will append to the current one in the + scroll area. + end (str): end string which can be appended to the end of the given + line (for ex. a line break). + + Returns: + None + """ + self.log_text.setText(self.log_text.text() + text + end) + if self.is_scroll_auto: + self.scroll_bar.setValue(self.scroll_bar.maximum()) + + @QtCore.Slot(int) + def on_scroll(self, position: int): + """ + A slot for the vertical scroll bar's movement. This ensures the + auto-scrolling feature of the scroll area when the scroll bar is at its + maximum value. + + Args: + position (int): Position value of the scroll bar. + + Returns: + None + """ + if self.scroll_bar.maximum() == position: + self.is_scroll_auto = True + return + + self.is_scroll_auto = False + + @QtCore.Slot(str, int) + def fail(self, text: str, return_code: int = 1): + """ + A slot used for signals which can emit when a worker (process) has + failed. at this moment the splash screen doesn't close by itself. + it has to be closed by the user. + + Args: + text (str): A text which can be set to the top label. + + Returns: + return_code (int): Return code of the thread's code + """ + self.top_label.setText(text) + self.close_btn.show() + self.thread_return_code = return_code + self.q_thread.exit(return_code) diff --git a/pyproject.toml b/pyproject.toml index 2fc4f6fe39..02370a4f10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.1" # OpenPype +version = "3.15.2" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" diff --git a/tests/conftest.py b/tests/conftest.py index 7b58b0314d..4f7c17244b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,6 +24,11 @@ def pytest_addoption(parser): help="Overwrite default timeout" ) + parser.addoption( + "--setup_only", action="store", default=None, + help="True - only setup test, do not run any tests" + ) + @pytest.fixture(scope="module") def test_data_folder(request): @@ -45,6 +50,11 @@ def timeout(request): return request.config.getoption("--timeout") +@pytest.fixture(scope="module") +def setup_only(request): + return request.config.getoption("--setup_only") + + @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): # execute all other hooks to obtain the report object diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 2bafa16971..300024dc98 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -243,6 +243,8 @@ class PublishTest(ModuleUnitTest): PERSIST = True # True - keep test_db, test_openpype, outputted test files TEST_DATA_FOLDER = None # use specific folder of unzipped test file + SETUP_ONLY = False + @pytest.fixture(scope="module") def app_name(self, app_variant): """Returns calculated value for ApplicationManager. Eg.(nuke/12-2)""" @@ -286,8 +288,13 @@ class PublishTest(ModuleUnitTest): @pytest.fixture(scope="module") def launched_app(self, dbcon, download_test_data, last_workfile_path, - startup_scripts, app_args, app_name, output_folder_url): + startup_scripts, app_args, app_name, output_folder_url, + setup_only): """Launch host app""" + if setup_only or self.SETUP_ONLY: + print("Creating only setup for test, not launching app") + yield + return # set schema - for integrate_new from openpype import PACKAGE_DIR # Path to OpenPype's schema @@ -316,8 +323,12 @@ class PublishTest(ModuleUnitTest): @pytest.fixture(scope="module") def publish_finished(self, dbcon, launched_app, download_test_data, - timeout): + timeout, setup_only): """Dummy fixture waiting for publish to finish""" + if setup_only or self.SETUP_ONLY: + print("Creating only setup for test, not launching app") + yield False + return import time time_start = time.time() timeout = timeout or self.TIMEOUT @@ -334,11 +345,16 @@ class PublishTest(ModuleUnitTest): def test_folder_structure_same(self, dbcon, publish_finished, download_test_data, output_folder_url, - skip_compare_folders): + skip_compare_folders, + setup_only): """Check if expected and published subfolders contain same files. Compares only presence, not size nor content! """ + if setup_only or self.SETUP_ONLY: + print("Creating only setup for test, not launching app") + return + published_dir_base = output_folder_url expected_dir_base = os.path.join(download_test_data, "expected") diff --git a/website/yarn.lock b/website/yarn.lock index 559c58f931..d250e48b9d 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -1669,9 +1669,9 @@ "@hapi/hoek" "^9.0.0" "@sideway/formula@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c" - integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== "@sideway/pinpoint@^2.0.0": version "2.0.0"