diff --git a/.github_changelog_generator b/.github_changelog_generator new file mode 100644 index 0000000000..cd09ebcbfa --- /dev/null +++ b/.github_changelog_generator @@ -0,0 +1,7 @@ +pr-wo-labels=False +exclude-labels=duplicate,question,invalid,wontfix,weekly-digest +author=False +unreleased=False +since-tag=2.11.0 +release-branch=master +enhancement-label=**Enhancements:** diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..ba86b85eec --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,565 @@ +# Changelog + +## [2.12.0](https://github.com/pypeclub/pype/tree/2.12.0) (2020-09-09) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.8...2.12.0) + +**Enhancements:** + +- Less mongo connections [\#509](https://github.com/pypeclub/pype/pull/509) +- Nuke: adding image loader [\#499](https://github.com/pypeclub/pype/pull/499) +- Move launcher window to top if launcher action is clicked [\#450](https://github.com/pypeclub/pype/pull/450) +- Maya: better tile rendering support in Pype [\#446](https://github.com/pypeclub/pype/pull/446) +- Implementation of non QML launcher [\#443](https://github.com/pypeclub/pype/pull/443) +- Optional skip review on renders. [\#441](https://github.com/pypeclub/pype/pull/441) +- Ftrack: Option to push status from task to latest version [\#440](https://github.com/pypeclub/pype/pull/440) +- Properly containerize image plane loads. [\#434](https://github.com/pypeclub/pype/pull/434) +- Option to keep the review files. [\#426](https://github.com/pypeclub/pype/pull/426) +- Isolate view on instance members. [\#425](https://github.com/pypeclub/pype/pull/425) +- ftrack group is bcw compatible [\#418](https://github.com/pypeclub/pype/pull/418) +- Maya: Publishing of tile renderings on Deadline [\#398](https://github.com/pypeclub/pype/pull/398) +- Feature/little bit better logging gui [\#383](https://github.com/pypeclub/pype/pull/383) + +**Fixed bugs:** + +- Maya: Fix tile order for Draft Tile Assembler [\#511](https://github.com/pypeclub/pype/pull/511) +- Remove extra dash [\#501](https://github.com/pypeclub/pype/pull/501) +- Fix: strip dot from repre names in single frame renders [\#498](https://github.com/pypeclub/pype/pull/498) +- Better handling of destination during integrating [\#485](https://github.com/pypeclub/pype/pull/485) +- Fix: allow thumbnail creation for single frame renders [\#460](https://github.com/pypeclub/pype/pull/460) +- added missing argument to launch\_application in ftrack app handler [\#453](https://github.com/pypeclub/pype/pull/453) +- Burnins: Copy bit rate of input video to match quality. [\#448](https://github.com/pypeclub/pype/pull/448) +- Standalone publisher is now independent from tray [\#442](https://github.com/pypeclub/pype/pull/442) +- Bugfix/empty enumerator attributes [\#436](https://github.com/pypeclub/pype/pull/436) +- Fixed wrong order of "other" category collapssing in publisher [\#435](https://github.com/pypeclub/pype/pull/435) +- Multiple reviews where being overwritten to one. [\#424](https://github.com/pypeclub/pype/pull/424) +- Cleanup plugin fail on instances without staging dir [\#420](https://github.com/pypeclub/pype/pull/420) +- deprecated -intra parameter in ffmpeg to new `-g` [\#417](https://github.com/pypeclub/pype/pull/417) +- Delivery action can now work with entered path [\#397](https://github.com/pypeclub/pype/pull/397) + +**Merged pull requests:** + +- Review on instance.data [\#473](https://github.com/pypeclub/pype/pull/473) + +## [2.11.8](https://github.com/pypeclub/pype/tree/2.11.8) (2020-08-27) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.7...2.11.8) + +**Enhancements:** + +- DWAA support for Maya [\#382](https://github.com/pypeclub/pype/issues/382) +- Isolate View on Playblast [\#367](https://github.com/pypeclub/pype/issues/367) +- Maya: Tile rendering [\#297](https://github.com/pypeclub/pype/issues/297) +- single pype instance running [\#47](https://github.com/pypeclub/pype/issues/47) +- PYPE-649: projects don't guarantee backwards compatible environment [\#8](https://github.com/pypeclub/pype/issues/8) +- PYPE-663: separate venv for each deployed version [\#7](https://github.com/pypeclub/pype/issues/7) + +**Fixed bugs:** + +- pyblish pype - other group is collapsed before plugins are done [\#431](https://github.com/pypeclub/pype/issues/431) +- Alpha white edges in harmony on PNGs [\#412](https://github.com/pypeclub/pype/issues/412) +- harmony image loader picks wrong representations [\#404](https://github.com/pypeclub/pype/issues/404) +- Clockify crash when response contain symbol not allowed by UTF-8 [\#81](https://github.com/pypeclub/pype/issues/81) + +## [2.11.7](https://github.com/pypeclub/pype/tree/2.11.7) (2020-08-21) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.6...2.11.7) + +**Fixed bugs:** + +- Clean Up Baked Movie [\#369](https://github.com/pypeclub/pype/issues/369) +- celaction last workfile [\#459](https://github.com/pypeclub/pype/pull/459) + +## [2.11.6](https://github.com/pypeclub/pype/tree/2.11.6) (2020-08-18) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.5...2.11.6) + +**Enhancements:** + +- publisher app [\#56](https://github.com/pypeclub/pype/issues/56) + +## [2.11.5](https://github.com/pypeclub/pype/tree/2.11.5) (2020-08-13) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.4...2.11.5) + +**Enhancements:** + +- Switch from master to equivalent [\#220](https://github.com/pypeclub/pype/issues/220) +- Standalone publisher now only groups sequence if the extension is known [\#439](https://github.com/pypeclub/pype/pull/439) + +**Fixed bugs:** + +- Logs have been disable for editorial by default to speed up publishing [\#433](https://github.com/pypeclub/pype/pull/433) +- additional fixes for celaction [\#430](https://github.com/pypeclub/pype/pull/430) +- Harmony: invalid variable scope in validate scene settings [\#428](https://github.com/pypeclub/pype/pull/428) +- new representation name for audio was not accepted [\#427](https://github.com/pypeclub/pype/pull/427) + +## [2.11.4](https://github.com/pypeclub/pype/tree/2.11.4) (2020-08-10) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.3...2.11.4) + +**Enhancements:** + +- WebSocket server [\#135](https://github.com/pypeclub/pype/issues/135) +- standalonepublisher: editorial family features expansion \[master branch\] [\#411](https://github.com/pypeclub/pype/pull/411) + +## [2.11.3](https://github.com/pypeclub/pype/tree/2.11.3) (2020-08-04) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.2...2.11.3) + +**Fixed bugs:** + +- Harmony: publishing performance issues [\#408](https://github.com/pypeclub/pype/pull/408) + +## [2.11.2](https://github.com/pypeclub/pype/tree/2.11.2) (2020-07-31) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.1...2.11.2) + +**Fixed bugs:** + +- Ftrack to Avalon bug [\#406](https://github.com/pypeclub/pype/issues/406) + +## [2.11.1](https://github.com/pypeclub/pype/tree/2.11.1) (2020-07-29) + +[Full Changelog](https://github.com/pypeclub/pype/compare/2.11.0...2.11.1) + +**Merged pull requests:** + +- Celaction: metadata json folder fixes on path [\#393](https://github.com/pypeclub/pype/pull/393) +- CelAction - version up method taken fro pype.lib [\#391](https://github.com/pypeclub/pype/pull/391) + + +## 2.11.0 ## + +_**release date:** 27 July 2020_ + +**new:** +- _(blender)_ namespace support [\#341](https://github.com/pypeclub/pype/pull/341) +- _(blender)_ start end frames [\#330](https://github.com/pypeclub/pype/pull/330) +- _(blender)_ camera asset [\#322](https://github.com/pypeclub/pype/pull/322) +- _(pype)_ toggle instances per family in pyblish GUI [\#320](https://github.com/pypeclub/pype/pull/320) +- _(pype)_ current release version is now shown in the tray menu [#379](https://github.com/pypeclub/pype/pull/379) + + +**improved:** +- _(resolve)_ tagging for publish [\#239](https://github.com/pypeclub/pype/issues/239) +- _(pype)_ Support publishing a subset of shots with standalone editorial [\#336](https://github.com/pypeclub/pype/pull/336) +- _(harmony)_ Basic support for palettes [\#324](https://github.com/pypeclub/pype/pull/324) +- _(photoshop)_ Flag outdated containers on startup and publish. [\#309](https://github.com/pypeclub/pype/pull/309) +- _(harmony)_ Flag Outdated containers [\#302](https://github.com/pypeclub/pype/pull/302) +- _(photoshop)_ Publish review [\#298](https://github.com/pypeclub/pype/pull/298) +- _(pype)_ Optional Last workfile launch [\#365](https://github.com/pypeclub/pype/pull/365) + + +**fixed:** +- _(premiere)_ workflow fixes [\#346](https://github.com/pypeclub/pype/pull/346) +- _(pype)_ pype-setup does not work with space in path [\#327](https://github.com/pypeclub/pype/issues/327) +- _(ftrack)_ Ftrack delete action cause circular error [\#206](https://github.com/pypeclub/pype/issues/206) +- _(nuke)_ Priority was forced to 50 [\#345](https://github.com/pypeclub/pype/pull/345) +- _(nuke)_ Fix ValidateNukeWriteKnobs [\#340](https://github.com/pypeclub/pype/pull/340) +- _(maya)_ If camera attributes are connected, we can ignore them. [\#339](https://github.com/pypeclub/pype/pull/339) +- _(pype)_ stop appending of tools environment to existing env [\#337](https://github.com/pypeclub/pype/pull/337) +- _(ftrack)_ Ftrack timeout needs to look at AVALON\_TIMEOUT [\#325](https://github.com/pypeclub/pype/pull/325) +- _(harmony)_ Only zip files are supported. [\#310](https://github.com/pypeclub/pype/pull/310) +- _(pype)_ hotfix/Fix event server mongo uri [\#305](https://github.com/pypeclub/pype/pull/305) +- _(photoshop)_ Subset was not named or validated correctly. [\#304](https://github.com/pypeclub/pype/pull/304) + + + + +## 2.10.0 ## + +_**release date:** 17 June 2020_ + +**new:** +- _(harmony)_ **Toon Boom Harmony** has been greatly extended to support rigging, scene build, animation and rendering workflows. [#270](https://github.com/pypeclub/pype/issues/270) [#271](https://github.com/pypeclub/pype/issues/271) [#190](https://github.com/pypeclub/pype/issues/190) [#191](https://github.com/pypeclub/pype/issues/191) [#172](https://github.com/pypeclub/pype/issues/172) [#168](https://github.com/pypeclub/pype/issues/168) +- _(pype)_ Added support for rudimentary **edl publishing** into individual shots. [#265](https://github.com/pypeclub/pype/issues/265) +- _(celaction)_ Simple **Celaction** integration has been added with support for workfiles and rendering. [#255](https://github.com/pypeclub/pype/issues/255) +- _(maya)_ Support for multiple job types when submitting to the farm. We can now render Maya or Standalone render jobs for Vray and Arnold (limited support for arnold) [#204](https://github.com/pypeclub/pype/issues/204) +- _(photoshop)_ Added initial support for Photoshop [#232](https://github.com/pypeclub/pype/issues/232) + +**improved:** +- _(blender)_ Updated support for rigs and added support Layout family [#233](https://github.com/pypeclub/pype/issues/233) [#226](https://github.com/pypeclub/pype/issues/226) +- _(premiere)_ It is now possible to choose different storage root for workfiles of different task types. [#255](https://github.com/pypeclub/pype/issues/255) +- _(maya)_ Support for unmerged AOVs in Redshift multipart EXRs [#197](https://github.com/pypeclub/pype/issues/197) +- _(pype)_ Pype repository has been refactored in preparation for 3.0 release [#169](https://github.com/pypeclub/pype/issues/169) +- _(deadline)_ All file dependencies are now passed to deadline from maya to prevent premature start of rendering if caches or textures haven't been coppied over yet. [#195](https://github.com/pypeclub/pype/issues/195) +- _(nuke)_ Script validation can now be made optional. [#194](https://github.com/pypeclub/pype/issues/194) +- _(pype)_ Publishing can now be stopped at any time. [#194](https://github.com/pypeclub/pype/issues/194) + +**fix:** +- _(pype)_ Pyblish-lite has been integrated into pype repository, plus various publishing GUI fixes. [#274](https://github.com/pypeclub/pype/issues/274) [#275](https://github.com/pypeclub/pype/issues/275) [#268](https://github.com/pypeclub/pype/issues/268) [#227](https://github.com/pypeclub/pype/issues/227) [#238](https://github.com/pypeclub/pype/issues/238) +- _(maya)_ Alembic extractor was getting wrong frame range type in certain scenarios [#254](https://github.com/pypeclub/pype/issues/254) +- _(maya)_ Attaching a render to subset in maya was not passing validation in certain scenarios [#256](https://github.com/pypeclub/pype/issues/256) +- _(ftrack)_ Various small fixes to ftrack sync [#263](https://github.com/pypeclub/pype/issues/263) [#259](https://github.com/pypeclub/pype/issues/259) +- _(maya)_ Look extraction is now able to skp invalid connections in shaders [#207](https://github.com/pypeclub/pype/issues/207) + + + + +## 2.9.0 ## + +_**release date:** 25 May 2020_ + +**new:** +- _(pype)_ Support for **Multiroot projects**. You can now store project data on multiple physical or virtual storages and target individual publishes to these locations. For instance render can be stored on a faster storage than the rest of the project. [#145](https://github.com/pypeclub/pype/issues/145), [#38](https://github.com/pypeclub/pype/issues/38) +- _(harmony)_ Basic implementation of **Toon Boom Harmony** has been added. [#142](https://github.com/pypeclub/pype/issues/142) +- _(pype)_ OSX support is in public beta now. There are issues to be expected, but the main implementation should be functional. [#141](https://github.com/pypeclub/pype/issues/141) + + +**improved:** + +- _(pype)_ **Review extractor** has been completely rebuilt. It now supports granular filtering so you can create **multiple outputs** for different tasks, families or hosts. [#103](https://github.com/pypeclub/pype/issues/103), [#166](https://github.com/pypeclub/pype/issues/166), [#165](https://github.com/pypeclub/pype/issues/165) +- _(pype)_ **Burnin** generation had been extended to **support same multi-output filtering** as review extractor [#103](https://github.com/pypeclub/pype/issues/103) +- _(pype)_ Publishing file templates can now be specified in config for each individual family [#114](https://github.com/pypeclub/pype/issues/114) +- _(pype)_ Studio specific plugins can now be appended to pype standard publishing plugins. [#112](https://github.com/pypeclub/pype/issues/112) +- _(nukestudio)_ Reviewable clips no longer need to be previously cut, exported and re-imported to timeline. **Pype can now dynamically cut reviewable quicktimes** from continuous offline footage during publishing. [#23](https://github.com/pypeclub/pype/issues/23) +- _(deadline)_ Deadline can now correctly differentiate between staging and production pype. [#154](https://github.com/pypeclub/pype/issues/154) +- _(deadline)_ `PYPE_PYTHON_EXE` env variable can now be used to direct publishing to explicit python installation. [#120](https://github.com/pypeclub/pype/issues/120) +- _(nuke)_ Nuke now check for new version of loaded data on file open. [#140](https://github.com/pypeclub/pype/issues/140) +- _(nuke)_ frame range and limit checkboxes are now exposed on write node. [#119](https://github.com/pypeclub/pype/issues/119) + + + +**fix:** + +- _(nukestudio)_ Project Location was using backslashes which was breaking nukestudio native exporting in certains configurations [#82](https://github.com/pypeclub/pype/issues/82) +- _(nukestudio)_ Duplicity in hierarchy tags was prone to throwing publishing error [#130](https://github.com/pypeclub/pype/issues/130), [#144](https://github.com/pypeclub/pype/issues/144) +- _(ftrack)_ multiple stability improvements [#157](https://github.com/pypeclub/pype/issues/157), [#159](https://github.com/pypeclub/pype/issues/159), [#128](https://github.com/pypeclub/pype/issues/128), [#118](https://github.com/pypeclub/pype/issues/118), [#127](https://github.com/pypeclub/pype/issues/127) +- _(deadline)_ multipart EXRs were stopping review publishing on the farm. They are still not supported for automatic review generation, but the publish will go through correctly without the quicktime. [#155](https://github.com/pypeclub/pype/issues/155) +- _(deadline)_ If deadline is non-responsive it will no longer freeze host when publishing [#149](https://github.com/pypeclub/pype/issues/149) +- _(deadline)_ Sometimes deadline was trying to launch render before all the source data was coppied over. [#137](https://github.com/pypeclub/pype/issues/137) _(harmony)_ Basic implementation of **Toon Boom Harmony** has been added. [#142](https://github.com/pypeclub/pype/issues/142) +- _(nuke)_ Filepath knob wasn't updated properly. [#131](https://github.com/pypeclub/pype/issues/131) +- _(maya)_ When extracting animation, the "Write Color Set" options on the instance were not respected. [#108](https://github.com/pypeclub/pype/issues/108) +- _(maya)_ Attribute overrides for AOV only worked for the legacy render layers. Now it works for new render setup as well [#132](https://github.com/pypeclub/pype/issues/132) +- _(maya)_ Stability and usability improvements in yeti workflow [#104](https://github.com/pypeclub/pype/issues/104) + + + + +## 2.8.0 ## + +_**release date:** 20 April 2020_ + +**new:** + +- _(pype)_ Option to generate slates from json templates. [PYPE-628] [#26](https://github.com/pypeclub/pype/issues/26) +- _(pype)_ It is now possible to automate loading of published subsets into any scene. Documentation will follow :). [PYPE-611] [#24](https://github.com/pypeclub/pype/issues/24) + +**fix:** + +- _(maya)_ Some Redshift render tokens could break publishing. [PYPE-778] [#33](https://github.com/pypeclub/pype/issues/33) +- _(maya)_ Publish was not preserving maya file extension. [#39](https://github.com/pypeclub/pype/issues/39) +- _(maya)_ Rig output validator was failing on nodes without shapes. [#40](https://github.com/pypeclub/pype/issues/40) +- _(maya)_ Yeti caches can now be properly versioned up in the scene inventory. [#40](https://github.com/pypeclub/pype/issues/40) +- _(nuke)_ Build first workfiles was not accepting jpeg sequences. [#34](https://github.com/pypeclub/pype/issues/34) +- _(deadline)_ Trying to generate ffmpeg review from multipart EXRs no longer crashes publishing. [PYPE-781] +- _(deadline)_ Render publishing is more stable in multiplatform environments. [PYPE-775] + + + + +## 2.7.0 ## + +_**release date:** 30 March 2020_ + +**new:** + +- _(maya)_ Artist can now choose to load multiple references of the same subset at once [PYPE-646, PYPS-81] +- _(nuke)_ Option to use named OCIO colorspaces for review colour baking. [PYPS-82] +- _(pype)_ Pype can now work with `master` versions for publishing and loading. These are non-versioned publishes that are overwritten with the latest version during publish. These are now supported in all the GUIs, but their publishing is deactivated by default. [PYPE-653] +- _(blender)_ Added support for basic blender workflow. We currently support `rig`, `model` and `animation` families. [PYPE-768] +- _(pype)_ Source timecode can now be used in burn-ins. [PYPE-777] +- _(pype)_ Review outputs profiles can now specify delivery resolution different than project setting [PYPE-759] +- _(nuke)_ Bookmark to current context is now added automatically to all nuke browser windows. [PYPE-712] + +**change:** + +- _(maya)_ It is now possible to publish camera without. baking. Keep in mind that unbaked cameras can't be guaranteed to work in other hosts. [PYPE-595] +- _(maya)_ All the renders from maya are now grouped in the loader by their Layer name. [PYPE-482] +- _(nuke/hiero)_ Any publishes from nuke and hiero can now be versioned independently of the workfile. [PYPE-728] + + +**fix:** + +- _(nuke)_ Mixed slashes caused issues in ocio config path. +- _(pype)_ Intent field in pyblish GUI was passing label instead of value to ftrack. [PYPE-733] +- _(nuke)_ Publishing of pre-renders was inconsistent. [PYPE-766] +- _(maya)_ Handles and frame ranges were inconsistent in various places during publishing. +- _(nuke)_ Nuke was crashing if it ran into certain missing knobs. For example DPX output missing `autocrop` [PYPE-774] +- _(deadline)_ Project overrides were not working properly with farm render publishing. +- _(hiero)_ Problems with single frame plates publishing. +- _(maya)_ Redshift RenderPass token were breaking render publishing. [PYPE-778] +- _(nuke)_ Build first workfile was not accepting jpeg sequences. +- _(maya)_ Multipart (Multilayer) EXRs were breaking review publishing due to FFMPEG incompatiblity [PYPE-781] + + + +## 2.6.0 ## + +_**release date:** 9 March 2020_ + +**change:** +- _(maya)_ render publishing has been simplified and made more robust. Render setup layers are now automatically added to publishing subsets and `render globals` family has been replaced with simple `render` [PYPE-570] +- _(avalon)_ change context and workfiles apps, have been merged into one, that allows both actions to be performed at the same time. [PYPE-747] +- _(pype)_ thumbnails are now automatically propagate to asset from the last published subset in the loader +- _(ftrack)_ publishing comment and intent are now being published to ftrack note as well as describtion. [PYPE-727] +- _(pype)_ when overriding existing version new old representations are now overriden, instead of the new ones just being appended. (to allow this behaviour, the version validator need to be disabled. [PYPE-690]) +- _(pype)_ burnin preset has been significantly simplified. It now doesn't require passing function to each field, but only need the actual text template. to use this, all the current burnin PRESETS MUST BE UPDATED for all the projects. +- _(ftrack)_ credentials are now stored on a per server basis, so it's possible to switch between ftrack servers without having to log in and out. [PYPE-723] + + +**new:** +- _(pype)_ production and development deployments now have different colour of the tray icon. Orange for Dev and Green for production [PYPE-718] +- _(maya)_ renders can now be attached to a publishable subset rather than creating their own subset. For example it is possible to create a reviewable `look` or `model` render and have it correctly attached as a representation of the subsets [PYPE-451] +- _(maya)_ after saving current scene into a new context (as a new shot for instance), all the scene publishing subsets data gets re-generated automatically to match the new context [PYPE-532] +- _(pype)_ we now support project specific publish, load and create plugins [PYPE-740] +- _(ftrack)_ new action that allow archiving/deleting old published versions. User can keep how many of the latest version to keep when the action is ran. [PYPE-748, PYPE-715] +- _(ftrack)_ it is now possible to monitor and restart ftrack event server using ftrack action. [PYPE-658] +- _(pype)_ validator that prevent accidental overwrites of previously published versions. [PYPE-680] +- _(avalon)_ avalon core updated to version 5.6.0 +- _(maya)_ added validator to make sure that relative paths are used when publishing arnold standins. +- _(nukestudio)_ it is now possible to extract and publish audio family from clip in nuke studio [PYPE-682] + +**fix**: +- _(maya)_ maya set framerange button was ignoring handles [PYPE-719] +- _(ftrack)_ sync to avalon was sometime crashing when ran on empty project +- _(nukestudio)_ publishing same shots after they've been previously archived/deleted would result in a crash. [PYPE-737] +- _(nuke)_ slate workflow was breaking in certain scenarios. [PYPE-730] +- _(pype)_ rendering publish workflow has been significantly improved to prevent error resulting from implicit render collection. [PYPE-665, PYPE-746] +- _(pype)_ launching application on a non-synced project resulted in obscure [PYPE-528] +- _(pype)_ missing keys in burnins no longer result in an error. [PYPE-706] +- _(ftrack)_ create folder structure action was sometimes failing for project managers due to wrong permissions. +- _(Nukestudio)_ using `source` in the start frame tag could result in wrong frame range calculation +- _(ftrack)_ sync to avalon action and event have been improved by catching more edge cases and provessing them properly. + + + +## 2.5.0 ## + +_**release date:** 11 Feb 2020_ + +**change:** +- _(pype)_ added many logs for easier debugging +- _(pype)_ review presets can now be separated between 2d and 3d renders [PYPE-693] +- _(pype)_ anatomy module has been greatly improved to allow for more dynamic pulblishing and faster debugging [PYPE-685] +- _(pype)_ avalon schemas have been moved from `pype-config` to `pype` repository, for simplification. [PYPE-670] +- _(ftrack)_ updated to latest ftrack API +- _(ftrack)_ publishing comments now appear in ftrack also as a note on version with customisable category [PYPE-645] +- _(ftrack)_ delete asset/subset action had been improved. It is now able to remove multiple entities and descendants of the selected entities [PYPE-361, PYPS-72] +- _(workfiles)_ added date field to workfiles app [PYPE-603] +- _(maya)_ old deprecated loader have been removed in favour of a single unified reference loader (old scenes will upgrade automatically to the new loader upon opening) [PYPE-633, PYPE-697] +- _(avalon)_ core updated to 5.5.15 [PYPE-671] +- _(nuke)_ library loader is now available in nuke [PYPE-698] + + +**new:** +- _(pype)_ added pype render wrapper to allow rendering on mixed platform farms. [PYPE-634] +- _(pype)_ added `pype launch` command. It let's admin run applications with dynamically built environment based on the given context. [PYPE-634] +- _(pype)_ added support for extracting review sequences with burnins [PYPE-657] +- _(publish)_ users can now set intent next to a comment when publishing. This will then be reflected on an attribute in ftrack. [PYPE-632] +- _(burnin)_ timecode can now be added to burnin +- _(burnin)_ datetime keys can now be added to burnin and anatomy [PYPE-651] +- _(burnin)_ anatomy templates can now be used in burnins. [PYPE=626] +- _(nuke)_ new validator for render resolution +- _(nuke)_ support for attach slate to nuke renders [PYPE-630] +- _(nuke)_ png sequences were added to loaders +- _(maya)_ added maya 2020 compatibility [PYPE-677] +- _(maya)_ ability to publish and load .ASS standin sequences [PYPS-54] +- _(pype)_ thumbnails can now be published and are visible in the loader. `AVALON_THUMBNAIL_ROOT` environment variable needs to be set for this to work [PYPE-573, PYPE-132] +- _(blender)_ base implementation of blender was added with publishing and loading of .blend files [PYPE-612] +- _(ftrack)_ new action for preparing deliveries [PYPE-639] + + +**fix**: +- _(burnin)_ more robust way of finding ffmpeg for burnins. +- _(pype)_ improved UNC paths remapping when sending to farm. +- _(pype)_ float frames sometimes made their way to representation context in database, breaking loaders [PYPE-668] +- _(pype)_ `pype install --force` was failing sometimes [PYPE-600] +- _(pype)_ padding in published files got calculated wrongly sometimes. It is now instead being always read from project anatomy. [PYPE-667] +- _(publish)_ comment publishing was failing in certain situations +- _(ftrack)_ multiple edge case scenario fixes in auto sync and sync-to-avalon action +- _(ftrack)_ sync to avalon now works on empty projects +- _(ftrack)_ thumbnail update event was failing when deleting entities [PYPE-561] +- _(nuke)_ loader applies proper colorspaces from Presets +- _(nuke)_ publishing handles didn't always work correctly [PYPE-686] +- _(maya)_ assembly publishing and loading wasn't working correctly + + + + + +## 2.4.0 ## + +_**release date:** 9 Dec 2019_ + +**change:** +- _(ftrack)_ version to status ftrack event can now be configured from Presets + - based on preset `presets/ftracc/ftrack_config.json["status_version_to_task"]` +- _(ftrack)_ sync to avalon event has been completely re-written. It now supports most of the project management situations on ftrack including moving, renaming and deleting entities, updating attributes and working with tasks. +- _(ftrack)_ sync to avalon action has been also re-writen. It is now much faster (up to 100 times depending on a project structure), has much better logging and reporting on encountered problems, and is able to handle much more complex situations. +- _(ftrack)_ sync to avalon trigger by checking `auto-sync` toggle on ftrack [PYPE-504] +- _(pype)_ various new features in the REST api +- _(pype)_ new visual identity used across pype +- _(pype)_ started moving all requirements to pip installation rather than vendorising them in pype repository. Due to a few yet unreleased packages, this means that pype can temporarily be only installed in the offline mode. + +**new:** +- _(nuke)_ support for publishing gizmos and loading them as viewer processes +- _(nuke)_ support for publishing nuke nodes from backdrops and loading them back +- _(pype)_ burnins can now work with start and end frames as keys + - use keys `{frame_start}`, `{frame_end}` and `{current_frame}` in burnin preset to use them. [PYPS-44,PYPS-73, PYPE-602] +- _(pype)_ option to filter logs by user and level in loggin GUI +- _(pype)_ image family added to standalone publisher [PYPE-574] +- _(pype)_ matchmove family added to standalone publisher [PYPE-574] +- _(nuke)_ validator for comparing arbitrary knobs with values from presets +- _(maya)_ option to force maya to copy textures in the new look publish rather than hardlinking them +- _(pype)_ comments from pyblish GUI are now being added to ftrack version +- _(maya)_ validator for checking outdated containers in the scene +- _(maya)_ option to publish and load arnold standin sequence [PYPE-579, PYPS-54] + +**fix**: +- _(pype)_ burnins were not respecting codec of the input video +- _(nuke)_ lot's of various nuke and nuke studio fixes across the board [PYPS-45] +- _(pype)_ workfiles app is not launching with the start of the app by default [PYPE-569] +- _(ftrack)_ ftrack integration during publishing was failing under certain situations [PYPS-66] +- _(pype)_ minor fixes in REST api +- _(ftrack)_ status change event was crashing when the target status was missing [PYPS-68] +- _(ftrack)_ actions will try to reconnect if they fail for some reason +- _(maya)_ problems with fps mapping when using float FPS values +- _(deadline)_ overall improvements to deadline publishing +- _(setup)_ environment variables are now remapped on the fly based on the platform pype is running on. This fixes many issues in mixed platform environments. + + + +## 2.3.6 # + +_**release date:** 27 Nov 2019_ + +**hotfix**: +- _(ftrack)_ was hiding important debug logo +- _(nuke)_ crashes during workfile publishing +- _(ftrack)_ event server crashes because of signal problems +- _(muster)_ problems with muster render submissions +- _(ftrack)_ thumbnail update event syntax errors + + +## 2.3.0 ## +_release date: 6 Oct 2019_ + +**new**: +- _(maya)_ support for yeti rigs and yeti caches +- _(maya)_ validator for comparing arbitrary attributes against ftrack +- _(pype)_ burnins can now show current date and time +- _(muster)_ pools can now be set in render globals in maya +- _(pype)_ Rest API has been implemented in beta stage +- _(nuke)_ LUT loader has been added +- _(pype)_ rudimentary user module has been added as preparation for user management +- _(pype)_ a simple logging GUI has been added to pype tray +- _(nuke)_ nuke can now bake input process into mov +- _(maya)_ imported models now have selection handle displayed by defaulting +- _(avalon)_ it's is now possible to load multiple assets at once using loader +- _(maya)_ added ability to automatically connect yeti rig to a mesh upon loading + +**changed**: +- _(ftrack)_ event server now runs two parallel processes and is able to keep queue of events to process. +- _(nuke)_ task name is now added to all rendered subsets +- _(pype)_ adding more families to standalone publisher +- _(pype)_ standalone publisher now uses pyblish-lite +- _(pype)_ standalone publisher can now create review quicktimes +- _(ftrack)_ queries to ftrack were sped up +- _(ftrack)_ multiple ftrack action have been deprecated +- _(avalon)_ avalon upstream has been updated to 5.5.0 +- _(nukestudio)_ published transforms can now be animated +- + +**fix**: +- _(maya)_ fps popup button didn't work in some cases +- _(maya)_ geometry instances and references in maya were losing shader assignments +- _(muster)_ muster rendering templates were not working correctly +- _(maya)_ arnold tx texture conversion wasn't respecting colorspace set by the artist +- _(pype)_ problems with avalon db sync +- _(maya)_ ftrack was rounding FPS making it inconsistent +- _(pype)_ wrong icon names in Creator +- _(maya)_ scene inventory wasn't showing anything if representation was removed from database after it's been loaded to the scene +- _(nukestudio)_ multiple bugs squashed +- _(loader)_ loader was taking long time to show all the loading action when first launcher in maya + +## 2.2.0 ## +_release date: 8 Sept 2019_ + +**new**: +- _(pype)_ add customisable workflow for creating quicktimes from renders or playblasts +- _(nuke)_ option to choose deadline chunk size on write nodes +- _(nukestudio)_ added option to publish soft effects (subTrackItems) from NukeStudio as subsets including LUT files. these can then be loaded in nuke or NukeStudio +- _(nuke)_ option to build nuke script from previously published latest versions of plate and render subsets. +- _(nuke)_ nuke writes now have deadline tab. +- _(ftrack)_ Prepare Project action can now be used for creating the base folder structure on disk and in ftrack, setting up all the initial project attributes and it automatically prepares `pype_project_config` folder for the given project. +- _(clockify)_ Added support for time tracking in clockify. This currently in addition to ftrack time logs, but does not completely replace them. +- _(pype)_ any attributes in Creator and Loader plugins can now be customised using pype preset system + +**changed**: +- nukestudio now uses workio API for workfiles +- _(maya)_ "FIX FPS" prompt in maya now appears in the middle of the screen +- _(muster)_ can now be configured with custom templates +- _(pype)_ global publishing plugins can now be configured using presets as well as host specific ones + + +**fix**: +- wrong version retrieval from path in certain scenarios +- nuke reset resolution wasn't working in certain scenarios + +## 2.1.0 ## +_release date: 6 Aug 2019_ + +A large cleanup release. Most of the change are under the hood. + +**new**: +- _(pype)_ add customisable workflow for creating quicktimes from renders or playblasts +- _(pype)_ Added configurable option to add burnins to any generated quicktimes +- _(ftrack)_ Action that identifies what machines pype is running on. +- _(system)_ unify subprocess calls +- _(maya)_ add audio to review quicktimes +- _(nuke)_ add crop before write node to prevent overscan problems in ffmpeg +- **Nuke Studio** publishing and workfiles support +- **Muster** render manager support +- _(nuke)_ Framerange, FPS and Resolution are set automatically at startup +- _(maya)_ Ability to load published sequences as image planes +- _(system)_ Ftrack event that sets asset folder permissions based on task assignees in ftrack. +- _(maya)_ Pyblish plugin that allow validation of maya attributes +- _(system)_ added better startup logging to tray debug, including basic connection information +- _(avalon)_ option to group published subsets to groups in the loader +- _(avalon)_ loader family filters are working now + +**changed**: +- change multiple key attributes to unify their behaviour across the pipeline + - `frameRate` to `fps` + - `startFrame` to `frameStart` + - `endFrame` to `frameEnd` + - `fstart` to `frameStart` + - `fend` to `frameEnd` + - `handle_start` to `handleStart` + - `handle_end` to `handleEnd` + - `resolution_width` to `resolutionWidth` + - `resolution_height` to `resolutionHeight` + - `pixel_aspect` to `pixelAspect` + +- _(nuke)_ write nodes are now created inside group with only some attributes editable by the artist +- rendered frames are now deleted from temporary location after their publishing is finished. +- _(ftrack)_ RV action can now be launched from any entity +- after publishing only refresh button is now available in pyblish UI +- added context instance pyblish-lite so that artist knows if context plugin fails +- _(avalon)_ allow opening selected files using enter key +- _(avalon)_ core updated to v5.2.9 with our forked changes on top + +**fix**: +- faster hierarchy retrieval from db +- _(nuke)_ A lot of stability enhancements +- _(nuke studio)_ A lot of stability enhancements +- _(nuke)_ now only renders a single write node on farm +- _(ftrack)_ pype would crash when launcher project level task +- work directory was sometimes not being created correctly +- major pype.lib cleanup. Removing of unused functions, merging those that were doing the same and general house cleaning. +- _(avalon)_ subsets in maya 2019 weren't behaving correctly in the outliner + + +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 0000000000..d60bd7b0c7 --- /dev/null +++ b/HISTORY.md @@ -0,0 +1,432 @@ + +## 2.11.0 ## + +_**release date:** 27 July 2020_ + +**new:** +- _(blender)_ namespace support [\#341](https://github.com/pypeclub/pype/pull/341) +- _(blender)_ start end frames [\#330](https://github.com/pypeclub/pype/pull/330) +- _(blender)_ camera asset [\#322](https://github.com/pypeclub/pype/pull/322) +- _(pype)_ toggle instances per family in pyblish GUI [\#320](https://github.com/pypeclub/pype/pull/320) +- _(pype)_ current release version is now shown in the tray menu [#379](https://github.com/pypeclub/pype/pull/379) + + +**improved:** +- _(resolve)_ tagging for publish [\#239](https://github.com/pypeclub/pype/issues/239) +- _(pype)_ Support publishing a subset of shots with standalone editorial [\#336](https://github.com/pypeclub/pype/pull/336) +- _(harmony)_ Basic support for palettes [\#324](https://github.com/pypeclub/pype/pull/324) +- _(photoshop)_ Flag outdated containers on startup and publish. [\#309](https://github.com/pypeclub/pype/pull/309) +- _(harmony)_ Flag Outdated containers [\#302](https://github.com/pypeclub/pype/pull/302) +- _(photoshop)_ Publish review [\#298](https://github.com/pypeclub/pype/pull/298) +- _(pype)_ Optional Last workfile launch [\#365](https://github.com/pypeclub/pype/pull/365) + + +**fixed:** +- _(premiere)_ workflow fixes [\#346](https://github.com/pypeclub/pype/pull/346) +- _(pype)_ pype-setup does not work with space in path [\#327](https://github.com/pypeclub/pype/issues/327) +- _(ftrack)_ Ftrack delete action cause circular error [\#206](https://github.com/pypeclub/pype/issues/206) +- _(nuke)_ Priority was forced to 50 [\#345](https://github.com/pypeclub/pype/pull/345) +- _(nuke)_ Fix ValidateNukeWriteKnobs [\#340](https://github.com/pypeclub/pype/pull/340) +- _(maya)_ If camera attributes are connected, we can ignore them. [\#339](https://github.com/pypeclub/pype/pull/339) +- _(pype)_ stop appending of tools environment to existing env [\#337](https://github.com/pypeclub/pype/pull/337) +- _(ftrack)_ Ftrack timeout needs to look at AVALON\_TIMEOUT [\#325](https://github.com/pypeclub/pype/pull/325) +- _(harmony)_ Only zip files are supported. [\#310](https://github.com/pypeclub/pype/pull/310) +- _(pype)_ hotfix/Fix event server mongo uri [\#305](https://github.com/pypeclub/pype/pull/305) +- _(photoshop)_ Subset was not named or validated correctly. [\#304](https://github.com/pypeclub/pype/pull/304) + + + + +## 2.10.0 ## + +_**release date:** 17 June 2020_ + +**new:** +- _(harmony)_ **Toon Boom Harmony** has been greatly extended to support rigging, scene build, animation and rendering workflows. [#270](https://github.com/pypeclub/pype/issues/270) [#271](https://github.com/pypeclub/pype/issues/271) [#190](https://github.com/pypeclub/pype/issues/190) [#191](https://github.com/pypeclub/pype/issues/191) [#172](https://github.com/pypeclub/pype/issues/172) [#168](https://github.com/pypeclub/pype/issues/168) +- _(pype)_ Added support for rudimentary **edl publishing** into individual shots. [#265](https://github.com/pypeclub/pype/issues/265) +- _(celaction)_ Simple **Celaction** integration has been added with support for workfiles and rendering. [#255](https://github.com/pypeclub/pype/issues/255) +- _(maya)_ Support for multiple job types when submitting to the farm. We can now render Maya or Standalone render jobs for Vray and Arnold (limited support for arnold) [#204](https://github.com/pypeclub/pype/issues/204) +- _(photoshop)_ Added initial support for Photoshop [#232](https://github.com/pypeclub/pype/issues/232) + +**improved:** +- _(blender)_ Updated support for rigs and added support Layout family [#233](https://github.com/pypeclub/pype/issues/233) [#226](https://github.com/pypeclub/pype/issues/226) +- _(premiere)_ It is now possible to choose different storage root for workfiles of different task types. [#255](https://github.com/pypeclub/pype/issues/255) +- _(maya)_ Support for unmerged AOVs in Redshift multipart EXRs [#197](https://github.com/pypeclub/pype/issues/197) +- _(pype)_ Pype repository has been refactored in preparation for 3.0 release [#169](https://github.com/pypeclub/pype/issues/169) +- _(deadline)_ All file dependencies are now passed to deadline from maya to prevent premature start of rendering if caches or textures haven't been coppied over yet. [#195](https://github.com/pypeclub/pype/issues/195) +- _(nuke)_ Script validation can now be made optional. [#194](https://github.com/pypeclub/pype/issues/194) +- _(pype)_ Publishing can now be stopped at any time. [#194](https://github.com/pypeclub/pype/issues/194) + +**fix:** +- _(pype)_ Pyblish-lite has been integrated into pype repository, plus various publishing GUI fixes. [#274](https://github.com/pypeclub/pype/issues/274) [#275](https://github.com/pypeclub/pype/issues/275) [#268](https://github.com/pypeclub/pype/issues/268) [#227](https://github.com/pypeclub/pype/issues/227) [#238](https://github.com/pypeclub/pype/issues/238) +- _(maya)_ Alembic extractor was getting wrong frame range type in certain scenarios [#254](https://github.com/pypeclub/pype/issues/254) +- _(maya)_ Attaching a render to subset in maya was not passing validation in certain scenarios [#256](https://github.com/pypeclub/pype/issues/256) +- _(ftrack)_ Various small fixes to ftrack sync [#263](https://github.com/pypeclub/pype/issues/263) [#259](https://github.com/pypeclub/pype/issues/259) +- _(maya)_ Look extraction is now able to skp invalid connections in shaders [#207](https://github.com/pypeclub/pype/issues/207) + + + + +## 2.9.0 ## + +_**release date:** 25 May 2020_ + +**new:** +- _(pype)_ Support for **Multiroot projects**. You can now store project data on multiple physical or virtual storages and target individual publishes to these locations. For instance render can be stored on a faster storage than the rest of the project. [#145](https://github.com/pypeclub/pype/issues/145), [#38](https://github.com/pypeclub/pype/issues/38) +- _(harmony)_ Basic implementation of **Toon Boom Harmony** has been added. [#142](https://github.com/pypeclub/pype/issues/142) +- _(pype)_ OSX support is in public beta now. There are issues to be expected, but the main implementation should be functional. [#141](https://github.com/pypeclub/pype/issues/141) + + +**improved:** + +- _(pype)_ **Review extractor** has been completely rebuilt. It now supports granular filtering so you can create **multiple outputs** for different tasks, families or hosts. [#103](https://github.com/pypeclub/pype/issues/103), [#166](https://github.com/pypeclub/pype/issues/166), [#165](https://github.com/pypeclub/pype/issues/165) +- _(pype)_ **Burnin** generation had been extended to **support same multi-output filtering** as review extractor [#103](https://github.com/pypeclub/pype/issues/103) +- _(pype)_ Publishing file templates can now be specified in config for each individual family [#114](https://github.com/pypeclub/pype/issues/114) +- _(pype)_ Studio specific plugins can now be appended to pype standard publishing plugins. [#112](https://github.com/pypeclub/pype/issues/112) +- _(nukestudio)_ Reviewable clips no longer need to be previously cut, exported and re-imported to timeline. **Pype can now dynamically cut reviewable quicktimes** from continuous offline footage during publishing. [#23](https://github.com/pypeclub/pype/issues/23) +- _(deadline)_ Deadline can now correctly differentiate between staging and production pype. [#154](https://github.com/pypeclub/pype/issues/154) +- _(deadline)_ `PYPE_PYTHON_EXE` env variable can now be used to direct publishing to explicit python installation. [#120](https://github.com/pypeclub/pype/issues/120) +- _(nuke)_ Nuke now check for new version of loaded data on file open. [#140](https://github.com/pypeclub/pype/issues/140) +- _(nuke)_ frame range and limit checkboxes are now exposed on write node. [#119](https://github.com/pypeclub/pype/issues/119) + + + +**fix:** + +- _(nukestudio)_ Project Location was using backslashes which was breaking nukestudio native exporting in certains configurations [#82](https://github.com/pypeclub/pype/issues/82) +- _(nukestudio)_ Duplicity in hierarchy tags was prone to throwing publishing error [#130](https://github.com/pypeclub/pype/issues/130), [#144](https://github.com/pypeclub/pype/issues/144) +- _(ftrack)_ multiple stability improvements [#157](https://github.com/pypeclub/pype/issues/157), [#159](https://github.com/pypeclub/pype/issues/159), [#128](https://github.com/pypeclub/pype/issues/128), [#118](https://github.com/pypeclub/pype/issues/118), [#127](https://github.com/pypeclub/pype/issues/127) +- _(deadline)_ multipart EXRs were stopping review publishing on the farm. They are still not supported for automatic review generation, but the publish will go through correctly without the quicktime. [#155](https://github.com/pypeclub/pype/issues/155) +- _(deadline)_ If deadline is non-responsive it will no longer freeze host when publishing [#149](https://github.com/pypeclub/pype/issues/149) +- _(deadline)_ Sometimes deadline was trying to launch render before all the source data was coppied over. [#137](https://github.com/pypeclub/pype/issues/137) _(harmony)_ Basic implementation of **Toon Boom Harmony** has been added. [#142](https://github.com/pypeclub/pype/issues/142) +- _(nuke)_ Filepath knob wasn't updated properly. [#131](https://github.com/pypeclub/pype/issues/131) +- _(maya)_ When extracting animation, the "Write Color Set" options on the instance were not respected. [#108](https://github.com/pypeclub/pype/issues/108) +- _(maya)_ Attribute overrides for AOV only worked for the legacy render layers. Now it works for new render setup as well [#132](https://github.com/pypeclub/pype/issues/132) +- _(maya)_ Stability and usability improvements in yeti workflow [#104](https://github.com/pypeclub/pype/issues/104) + + + + +## 2.8.0 ## + +_**release date:** 20 April 2020_ + +**new:** + +- _(pype)_ Option to generate slates from json templates. [PYPE-628] [#26](https://github.com/pypeclub/pype/issues/26) +- _(pype)_ It is now possible to automate loading of published subsets into any scene. Documentation will follow :). [PYPE-611] [#24](https://github.com/pypeclub/pype/issues/24) + +**fix:** + +- _(maya)_ Some Redshift render tokens could break publishing. [PYPE-778] [#33](https://github.com/pypeclub/pype/issues/33) +- _(maya)_ Publish was not preserving maya file extension. [#39](https://github.com/pypeclub/pype/issues/39) +- _(maya)_ Rig output validator was failing on nodes without shapes. [#40](https://github.com/pypeclub/pype/issues/40) +- _(maya)_ Yeti caches can now be properly versioned up in the scene inventory. [#40](https://github.com/pypeclub/pype/issues/40) +- _(nuke)_ Build first workfiles was not accepting jpeg sequences. [#34](https://github.com/pypeclub/pype/issues/34) +- _(deadline)_ Trying to generate ffmpeg review from multipart EXRs no longer crashes publishing. [PYPE-781] +- _(deadline)_ Render publishing is more stable in multiplatform environments. [PYPE-775] + + + + +## 2.7.0 ## + +_**release date:** 30 March 2020_ + +**new:** + +- _(maya)_ Artist can now choose to load multiple references of the same subset at once [PYPE-646, PYPS-81] +- _(nuke)_ Option to use named OCIO colorspaces for review colour baking. [PYPS-82] +- _(pype)_ Pype can now work with `master` versions for publishing and loading. These are non-versioned publishes that are overwritten with the latest version during publish. These are now supported in all the GUIs, but their publishing is deactivated by default. [PYPE-653] +- _(blender)_ Added support for basic blender workflow. We currently support `rig`, `model` and `animation` families. [PYPE-768] +- _(pype)_ Source timecode can now be used in burn-ins. [PYPE-777] +- _(pype)_ Review outputs profiles can now specify delivery resolution different than project setting [PYPE-759] +- _(nuke)_ Bookmark to current context is now added automatically to all nuke browser windows. [PYPE-712] + +**change:** + +- _(maya)_ It is now possible to publish camera without. baking. Keep in mind that unbaked cameras can't be guaranteed to work in other hosts. [PYPE-595] +- _(maya)_ All the renders from maya are now grouped in the loader by their Layer name. [PYPE-482] +- _(nuke/hiero)_ Any publishes from nuke and hiero can now be versioned independently of the workfile. [PYPE-728] + + +**fix:** + +- _(nuke)_ Mixed slashes caused issues in ocio config path. +- _(pype)_ Intent field in pyblish GUI was passing label instead of value to ftrack. [PYPE-733] +- _(nuke)_ Publishing of pre-renders was inconsistent. [PYPE-766] +- _(maya)_ Handles and frame ranges were inconsistent in various places during publishing. +- _(nuke)_ Nuke was crashing if it ran into certain missing knobs. For example DPX output missing `autocrop` [PYPE-774] +- _(deadline)_ Project overrides were not working properly with farm render publishing. +- _(hiero)_ Problems with single frame plates publishing. +- _(maya)_ Redshift RenderPass token were breaking render publishing. [PYPE-778] +- _(nuke)_ Build first workfile was not accepting jpeg sequences. +- _(maya)_ Multipart (Multilayer) EXRs were breaking review publishing due to FFMPEG incompatiblity [PYPE-781] + + + +## 2.6.0 ## + +_**release date:** 9 March 2020_ + +**change:** +- _(maya)_ render publishing has been simplified and made more robust. Render setup layers are now automatically added to publishing subsets and `render globals` family has been replaced with simple `render` [PYPE-570] +- _(avalon)_ change context and workfiles apps, have been merged into one, that allows both actions to be performed at the same time. [PYPE-747] +- _(pype)_ thumbnails are now automatically propagate to asset from the last published subset in the loader +- _(ftrack)_ publishing comment and intent are now being published to ftrack note as well as describtion. [PYPE-727] +- _(pype)_ when overriding existing version new old representations are now overriden, instead of the new ones just being appended. (to allow this behaviour, the version validator need to be disabled. [PYPE-690]) +- _(pype)_ burnin preset has been significantly simplified. It now doesn't require passing function to each field, but only need the actual text template. to use this, all the current burnin PRESETS MUST BE UPDATED for all the projects. +- _(ftrack)_ credentials are now stored on a per server basis, so it's possible to switch between ftrack servers without having to log in and out. [PYPE-723] + + +**new:** +- _(pype)_ production and development deployments now have different colour of the tray icon. Orange for Dev and Green for production [PYPE-718] +- _(maya)_ renders can now be attached to a publishable subset rather than creating their own subset. For example it is possible to create a reviewable `look` or `model` render and have it correctly attached as a representation of the subsets [PYPE-451] +- _(maya)_ after saving current scene into a new context (as a new shot for instance), all the scene publishing subsets data gets re-generated automatically to match the new context [PYPE-532] +- _(pype)_ we now support project specific publish, load and create plugins [PYPE-740] +- _(ftrack)_ new action that allow archiving/deleting old published versions. User can keep how many of the latest version to keep when the action is ran. [PYPE-748, PYPE-715] +- _(ftrack)_ it is now possible to monitor and restart ftrack event server using ftrack action. [PYPE-658] +- _(pype)_ validator that prevent accidental overwrites of previously published versions. [PYPE-680] +- _(avalon)_ avalon core updated to version 5.6.0 +- _(maya)_ added validator to make sure that relative paths are used when publishing arnold standins. +- _(nukestudio)_ it is now possible to extract and publish audio family from clip in nuke studio [PYPE-682] + +**fix**: +- _(maya)_ maya set framerange button was ignoring handles [PYPE-719] +- _(ftrack)_ sync to avalon was sometime crashing when ran on empty project +- _(nukestudio)_ publishing same shots after they've been previously archived/deleted would result in a crash. [PYPE-737] +- _(nuke)_ slate workflow was breaking in certain scenarios. [PYPE-730] +- _(pype)_ rendering publish workflow has been significantly improved to prevent error resulting from implicit render collection. [PYPE-665, PYPE-746] +- _(pype)_ launching application on a non-synced project resulted in obscure [PYPE-528] +- _(pype)_ missing keys in burnins no longer result in an error. [PYPE-706] +- _(ftrack)_ create folder structure action was sometimes failing for project managers due to wrong permissions. +- _(Nukestudio)_ using `source` in the start frame tag could result in wrong frame range calculation +- _(ftrack)_ sync to avalon action and event have been improved by catching more edge cases and provessing them properly. + + + +## 2.5.0 ## + +_**release date:** 11 Feb 2020_ + +**change:** +- _(pype)_ added many logs for easier debugging +- _(pype)_ review presets can now be separated between 2d and 3d renders [PYPE-693] +- _(pype)_ anatomy module has been greatly improved to allow for more dynamic pulblishing and faster debugging [PYPE-685] +- _(pype)_ avalon schemas have been moved from `pype-config` to `pype` repository, for simplification. [PYPE-670] +- _(ftrack)_ updated to latest ftrack API +- _(ftrack)_ publishing comments now appear in ftrack also as a note on version with customisable category [PYPE-645] +- _(ftrack)_ delete asset/subset action had been improved. It is now able to remove multiple entities and descendants of the selected entities [PYPE-361, PYPS-72] +- _(workfiles)_ added date field to workfiles app [PYPE-603] +- _(maya)_ old deprecated loader have been removed in favour of a single unified reference loader (old scenes will upgrade automatically to the new loader upon opening) [PYPE-633, PYPE-697] +- _(avalon)_ core updated to 5.5.15 [PYPE-671] +- _(nuke)_ library loader is now available in nuke [PYPE-698] + + +**new:** +- _(pype)_ added pype render wrapper to allow rendering on mixed platform farms. [PYPE-634] +- _(pype)_ added `pype launch` command. It let's admin run applications with dynamically built environment based on the given context. [PYPE-634] +- _(pype)_ added support for extracting review sequences with burnins [PYPE-657] +- _(publish)_ users can now set intent next to a comment when publishing. This will then be reflected on an attribute in ftrack. [PYPE-632] +- _(burnin)_ timecode can now be added to burnin +- _(burnin)_ datetime keys can now be added to burnin and anatomy [PYPE-651] +- _(burnin)_ anatomy templates can now be used in burnins. [PYPE=626] +- _(nuke)_ new validator for render resolution +- _(nuke)_ support for attach slate to nuke renders [PYPE-630] +- _(nuke)_ png sequences were added to loaders +- _(maya)_ added maya 2020 compatibility [PYPE-677] +- _(maya)_ ability to publish and load .ASS standin sequences [PYPS-54] +- _(pype)_ thumbnails can now be published and are visible in the loader. `AVALON_THUMBNAIL_ROOT` environment variable needs to be set for this to work [PYPE-573, PYPE-132] +- _(blender)_ base implementation of blender was added with publishing and loading of .blend files [PYPE-612] +- _(ftrack)_ new action for preparing deliveries [PYPE-639] + + +**fix**: +- _(burnin)_ more robust way of finding ffmpeg for burnins. +- _(pype)_ improved UNC paths remapping when sending to farm. +- _(pype)_ float frames sometimes made their way to representation context in database, breaking loaders [PYPE-668] +- _(pype)_ `pype install --force` was failing sometimes [PYPE-600] +- _(pype)_ padding in published files got calculated wrongly sometimes. It is now instead being always read from project anatomy. [PYPE-667] +- _(publish)_ comment publishing was failing in certain situations +- _(ftrack)_ multiple edge case scenario fixes in auto sync and sync-to-avalon action +- _(ftrack)_ sync to avalon now works on empty projects +- _(ftrack)_ thumbnail update event was failing when deleting entities [PYPE-561] +- _(nuke)_ loader applies proper colorspaces from Presets +- _(nuke)_ publishing handles didn't always work correctly [PYPE-686] +- _(maya)_ assembly publishing and loading wasn't working correctly + + + + + +## 2.4.0 ## + +_**release date:** 9 Dec 2019_ + +**change:** +- _(ftrack)_ version to status ftrack event can now be configured from Presets + - based on preset `presets/ftracc/ftrack_config.json["status_version_to_task"]` +- _(ftrack)_ sync to avalon event has been completely re-written. It now supports most of the project management situations on ftrack including moving, renaming and deleting entities, updating attributes and working with tasks. +- _(ftrack)_ sync to avalon action has been also re-writen. It is now much faster (up to 100 times depending on a project structure), has much better logging and reporting on encountered problems, and is able to handle much more complex situations. +- _(ftrack)_ sync to avalon trigger by checking `auto-sync` toggle on ftrack [PYPE-504] +- _(pype)_ various new features in the REST api +- _(pype)_ new visual identity used across pype +- _(pype)_ started moving all requirements to pip installation rather than vendorising them in pype repository. Due to a few yet unreleased packages, this means that pype can temporarily be only installed in the offline mode. + +**new:** +- _(nuke)_ support for publishing gizmos and loading them as viewer processes +- _(nuke)_ support for publishing nuke nodes from backdrops and loading them back +- _(pype)_ burnins can now work with start and end frames as keys + - use keys `{frame_start}`, `{frame_end}` and `{current_frame}` in burnin preset to use them. [PYPS-44,PYPS-73, PYPE-602] +- _(pype)_ option to filter logs by user and level in loggin GUI +- _(pype)_ image family added to standalone publisher [PYPE-574] +- _(pype)_ matchmove family added to standalone publisher [PYPE-574] +- _(nuke)_ validator for comparing arbitrary knobs with values from presets +- _(maya)_ option to force maya to copy textures in the new look publish rather than hardlinking them +- _(pype)_ comments from pyblish GUI are now being added to ftrack version +- _(maya)_ validator for checking outdated containers in the scene +- _(maya)_ option to publish and load arnold standin sequence [PYPE-579, PYPS-54] + +**fix**: +- _(pype)_ burnins were not respecting codec of the input video +- _(nuke)_ lot's of various nuke and nuke studio fixes across the board [PYPS-45] +- _(pype)_ workfiles app is not launching with the start of the app by default [PYPE-569] +- _(ftrack)_ ftrack integration during publishing was failing under certain situations [PYPS-66] +- _(pype)_ minor fixes in REST api +- _(ftrack)_ status change event was crashing when the target status was missing [PYPS-68] +- _(ftrack)_ actions will try to reconnect if they fail for some reason +- _(maya)_ problems with fps mapping when using float FPS values +- _(deadline)_ overall improvements to deadline publishing +- _(setup)_ environment variables are now remapped on the fly based on the platform pype is running on. This fixes many issues in mixed platform environments. + + + +## 2.3.6 # + +_**release date:** 27 Nov 2019_ + +**hotfix**: +- _(ftrack)_ was hiding important debug logo +- _(nuke)_ crashes during workfile publishing +- _(ftrack)_ event server crashes because of signal problems +- _(muster)_ problems with muster render submissions +- _(ftrack)_ thumbnail update event syntax errors + + +## 2.3.0 ## +_release date: 6 Oct 2019_ + +**new**: +- _(maya)_ support for yeti rigs and yeti caches +- _(maya)_ validator for comparing arbitrary attributes against ftrack +- _(pype)_ burnins can now show current date and time +- _(muster)_ pools can now be set in render globals in maya +- _(pype)_ Rest API has been implemented in beta stage +- _(nuke)_ LUT loader has been added +- _(pype)_ rudimentary user module has been added as preparation for user management +- _(pype)_ a simple logging GUI has been added to pype tray +- _(nuke)_ nuke can now bake input process into mov +- _(maya)_ imported models now have selection handle displayed by defaulting +- _(avalon)_ it's is now possible to load multiple assets at once using loader +- _(maya)_ added ability to automatically connect yeti rig to a mesh upon loading + +**changed**: +- _(ftrack)_ event server now runs two parallel processes and is able to keep queue of events to process. +- _(nuke)_ task name is now added to all rendered subsets +- _(pype)_ adding more families to standalone publisher +- _(pype)_ standalone publisher now uses pyblish-lite +- _(pype)_ standalone publisher can now create review quicktimes +- _(ftrack)_ queries to ftrack were sped up +- _(ftrack)_ multiple ftrack action have been deprecated +- _(avalon)_ avalon upstream has been updated to 5.5.0 +- _(nukestudio)_ published transforms can now be animated +- + +**fix**: +- _(maya)_ fps popup button didn't work in some cases +- _(maya)_ geometry instances and references in maya were losing shader assignments +- _(muster)_ muster rendering templates were not working correctly +- _(maya)_ arnold tx texture conversion wasn't respecting colorspace set by the artist +- _(pype)_ problems with avalon db sync +- _(maya)_ ftrack was rounding FPS making it inconsistent +- _(pype)_ wrong icon names in Creator +- _(maya)_ scene inventory wasn't showing anything if representation was removed from database after it's been loaded to the scene +- _(nukestudio)_ multiple bugs squashed +- _(loader)_ loader was taking long time to show all the loading action when first launcher in maya + +## 2.2.0 ## +_release date: 8 Sept 2019_ + +**new**: +- _(pype)_ add customisable workflow for creating quicktimes from renders or playblasts +- _(nuke)_ option to choose deadline chunk size on write nodes +- _(nukestudio)_ added option to publish soft effects (subTrackItems) from NukeStudio as subsets including LUT files. these can then be loaded in nuke or NukeStudio +- _(nuke)_ option to build nuke script from previously published latest versions of plate and render subsets. +- _(nuke)_ nuke writes now have deadline tab. +- _(ftrack)_ Prepare Project action can now be used for creating the base folder structure on disk and in ftrack, setting up all the initial project attributes and it automatically prepares `pype_project_config` folder for the given project. +- _(clockify)_ Added support for time tracking in clockify. This currently in addition to ftrack time logs, but does not completely replace them. +- _(pype)_ any attributes in Creator and Loader plugins can now be customised using pype preset system + +**changed**: +- nukestudio now uses workio API for workfiles +- _(maya)_ "FIX FPS" prompt in maya now appears in the middle of the screen +- _(muster)_ can now be configured with custom templates +- _(pype)_ global publishing plugins can now be configured using presets as well as host specific ones + + +**fix**: +- wrong version retrieval from path in certain scenarios +- nuke reset resolution wasn't working in certain scenarios + +## 2.1.0 ## +_release date: 6 Aug 2019_ + +A large cleanup release. Most of the change are under the hood. + +**new**: +- _(pype)_ add customisable workflow for creating quicktimes from renders or playblasts +- _(pype)_ Added configurable option to add burnins to any generated quicktimes +- _(ftrack)_ Action that identifies what machines pype is running on. +- _(system)_ unify subprocess calls +- _(maya)_ add audio to review quicktimes +- _(nuke)_ add crop before write node to prevent overscan problems in ffmpeg +- **Nuke Studio** publishing and workfiles support +- **Muster** render manager support +- _(nuke)_ Framerange, FPS and Resolution are set automatically at startup +- _(maya)_ Ability to load published sequences as image planes +- _(system)_ Ftrack event that sets asset folder permissions based on task assignees in ftrack. +- _(maya)_ Pyblish plugin that allow validation of maya attributes +- _(system)_ added better startup logging to tray debug, including basic connection information +- _(avalon)_ option to group published subsets to groups in the loader +- _(avalon)_ loader family filters are working now + +**changed**: +- change multiple key attributes to unify their behaviour across the pipeline + - `frameRate` to `fps` + - `startFrame` to `frameStart` + - `endFrame` to `frameEnd` + - `fstart` to `frameStart` + - `fend` to `frameEnd` + - `handle_start` to `handleStart` + - `handle_end` to `handleEnd` + - `resolution_width` to `resolutionWidth` + - `resolution_height` to `resolutionHeight` + - `pixel_aspect` to `pixelAspect` + +- _(nuke)_ write nodes are now created inside group with only some attributes editable by the artist +- rendered frames are now deleted from temporary location after their publishing is finished. +- _(ftrack)_ RV action can now be launched from any entity +- after publishing only refresh button is now available in pyblish UI +- added context instance pyblish-lite so that artist knows if context plugin fails +- _(avalon)_ allow opening selected files using enter key +- _(avalon)_ core updated to v5.2.9 with our forked changes on top + +**fix**: +- faster hierarchy retrieval from db +- _(nuke)_ A lot of stability enhancements +- _(nuke studio)_ A lot of stability enhancements +- _(nuke)_ now only renders a single write node on farm +- _(ftrack)_ pype would crash when launcher project level task +- work directory was sometimes not being created correctly +- major pype.lib cleanup. Removing of unused functions, merging those that were doing the same and general house cleaning. +- _(avalon)_ subsets in maya 2019 weren't behaving correctly in the outliner diff --git a/changelog.md b/changelog.md deleted file mode 100644 index bdee041615..0000000000 --- a/changelog.md +++ /dev/null @@ -1,120 +0,0 @@ -# Pype changelog # -Welcome to pype changelog - -## 2.3.0 ## -_release date: 6 Oct 2019_ - -**new**: -- _(maya)_ support for yeti rigs and yeti caches -- _(maya)_ validator for comparing arbitrary attributes against ftrack -- _(pype)_ burnins can now show current date and time -- _(muster)_ pools can now be set in render globals in maya -- _(pype)_ Rest API has been implemented in beta stage -- _(nuke)_ LUT loader has been added -- _(pype)_ rudimentary user module has been added as preparation for user management -- _(pype)_ a simple logging GUI has been added to pype tray -- _(nuke)_ nuke can now bake input process into mov -- _(maya)_ imported models now have selection handle displayed by defaulting -- _(avalon)_ it's is now possible to load multiple assets at once using loader -- _(maya)_ added ability to automatically connect yeti rig to a mesh upon loading - -**changed**: -- _(ftrack)_ event server now runs two parallel processes and is able to keep queue of events to process. -- _(nuke)_ task name is now added to all rendered subsets -- _(pype)_ adding more families to standalone publisher -- _(pype)_ standalone publisher now uses pyblish-lite -- _(pype)_ standalone publisher can now create review quicktimes -- _(ftrack)_ queries to ftrack were sped up -- _(ftrack)_ multiple ftrack action have been deprecated -- _(avalon)_ avalon upstream has been updated to 5.5.0 -- _(nukestudio)_ published transforms can now be animated -- - -**fix**: -- _(maya)_ fps popup button didn't work in some cases -- _(maya)_ geometry instances and references in maya were losing shader assignments -- _(muster)_ muster rendering templates were not working correctly -- _(maya)_ arnold tx texture conversion wasn't respecting colorspace set by the artist -- _(pype)_ problems with avalon db sync -- _(maya)_ ftrack was rounding FPS making it inconsistent -- _(pype)_ wrong icon names in Creator -- _(maya)_ scene inventory wasn't showing anything if representation was removed from database after it's been loaded to the scene -- _(nukestudio)_ multiple bugs squashed -- _(loader)_ loader was taking long time to show all the loading action when first launcher in maya - -## 2.2.0 ## -_release date: 8 Sept 2019_ - -**new**: -- _(pype)_ add customisable workflow for creating quicktimes from renders or playblasts -- _(nuke)_ option to choose deadline chunk size on write nodes -- _(nukestudio)_ added option to publish soft effects (subTrackItems) from NukeStudio as subsets including LUT files. these can then be loaded in nuke or NukeStudio -- _(nuke)_ option to build nuke script from previously published latest versions of plate and render subsets. -- _(nuke)_ nuke writes now have deadline tab. -- _(ftrack)_ Prepare Project action can now be used for creating the base folder structure on disk and in ftrack, setting up all the initial project attributes and it automatically prepares `pype_project_config` folder for the given project. -- _(clockify)_ Added support for time tracking in clockify. This currently in addition to ftrack time logs, but does not completely replace them. -- _(pype)_ any attributes in Creator and Loader plugins can now be customised using pype preset system - -**changed**: -- nukestudio now uses workio API for workfiles -- _(maya)_ "FIX FPS" prompt in maya now appears in the middle of the screen -- _(muster)_ can now be configured with custom templates -- _(pype)_ global publishing plugins can now be configured using presets as well as host specific ones - - -**fix**: -- wrong version retrieval from path in certain scenarios -- nuke reset resolution wasn't working in certain scenarios - -## 2.1.0 ## -_release date: 6 Aug 2019_ - -A large cleanup release. Most of the change are under the hood. - -**new**: -- _(pype)_ add customisable workflow for creating quicktimes from renders or playblasts -- _(pype)_ Added configurable option to add burnins to any generated quicktimes -- _(ftrack)_ Action that identifies what machines pype is running on. -- _(system)_ unify subprocess calls -- _(maya)_ add audio to review quicktimes -- _(nuke)_ add crop before write node to prevent overscan problems in ffmpeg -- **Nuke Studio** publishing and workfiles support -- **Muster** render manager support -- _(nuke)_ Framerange, FPS and Resolution are set automatically at startup -- _(maya)_ Ability to load published sequences as image planes -- _(system)_ Ftrack event that sets asset folder permissions based on task assignees in ftrack. -- _(maya)_ Pyblish plugin that allow validation of maya attributes -- _(system)_ added better startup logging to tray debug, including basic connection information -- _(avalon)_ option to group published subsets to groups in the loader -- _(avalon)_ loader family filters are working now - -**changed**: -- change multiple key attributes to unify their behaviour across the pipeline - - `frameRate` to `fps` - - `startFrame` to `frameStart` - - `endFrame` to `frameEnd` - - `fstart` to `frameStart` - - `fend` to `frameEnd` - - `handle_start` to `handleStart` - - `handle_end` to `handleEnd` - - `resolution_width` to `resolutionWidth` - - `resolution_height` to `resolutionHeight` - - `pixel_aspect` to `pixelAspect` - -- _(nuke)_ write nodes are now created inside group with only some attributes editable by the artist -- rendered frames are now deleted from temporary location after their publishing is finished. -- _(ftrack)_ RV action can now be launched from any entity -- after publishing only refresh button is now available in pyblish UI -- added context instance pyblish-lite so that artist knows if context plugin fails -- _(avalon)_ allow opening selected files using enter key -- _(avalon)_ core updated to v5.2.9 with our forked changes on top - -**fix**: -- faster hierarchy retrieval from db -- _(nuke)_ A lot of stability enhancements -- _(nuke studio)_ A lot of stability enhancements -- _(nuke)_ now only renders a single write node on farm -- _(ftrack)_ pype would crash when launcher project level task -- work directory was sometimes not being created correctly -- major pype.lib cleanup. Removing of unused functions, merging those that were doing the same and general house cleaning. -- _(avalon)_ subsets in maya 2019 weren't behaving correctly in the outliner diff --git a/pype/api.py b/pype/api.py index c722757a3c..021080b4d5 100644 --- a/pype/api.py +++ b/pype/api.py @@ -1,3 +1,7 @@ +from .settings import ( + system_settings, + project_settings +) from pypeapp import ( Logger, Anatomy, @@ -49,6 +53,9 @@ from .lib import ( from .lib import _subprocess as subprocess __all__ = [ + "system_settings", + "project_settings", + "Logger", "Anatomy", "project_overrides_dir_path", diff --git a/pype/hooks/resolve/prelaunch.py b/pype/hooks/resolve/prelaunch.py index bddeccf4a3..a122b87868 100644 --- a/pype/hooks/resolve/prelaunch.py +++ b/pype/hooks/resolve/prelaunch.py @@ -46,13 +46,14 @@ class ResolvePrelaunch(PypeHook): "`RESOLVE_UTILITY_SCRIPTS_DIR` or reinstall DaVinci Resolve. \n" f"RESOLVE_UTILITY_SCRIPTS_DIR: `{us_dir}`" ) + self.log.debug(f"-- us_dir: `{us_dir}`") # correctly format path for pre python script pre_py_sc = os.path.normpath(env.get("PRE_PYTHON_SCRIPT", "")) env["PRE_PYTHON_SCRIPT"] = pre_py_sc - + self.log.debug(f"-- pre_py_sc: `{pre_py_sc}`...") try: - __import__("pype.resolve") + __import__("pype.hosts.resolve") __import__("pyblish") except ImportError as e: @@ -62,6 +63,7 @@ class ResolvePrelaunch(PypeHook): else: # Resolve Setup integration importlib.reload(utils) + self.log.debug(f"-- utils.__file__: `{utils.__file__}`") utils.setup(env) return True diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py index d4b7d91fdb..7310e91e9b 100644 --- a/pype/hosts/harmony/__init__.py +++ b/pype/hosts/harmony/__init__.py @@ -18,12 +18,7 @@ def set_scene_settings(settings): if (args[0]["frameStart"] && args[0]["frameEnd"]) { var duration = args[0]["frameEnd"] - args[0]["frameStart"] + 1 - if (frame.numberOf() > duration) - { - frame.remove( - duration, frame.numberOf() - duration - ); - } + if (frame.numberOf() < duration) { frame.insert( diff --git a/pype/hosts/maya/plugin.py b/pype/hosts/maya/plugin.py index ed244d56df..a5c57f1ab8 100644 --- a/pype/hosts/maya/plugin.py +++ b/pype/hosts/maya/plugin.py @@ -174,6 +174,25 @@ class ReferenceLoader(api.Loader): assert os.path.exists(path), "%s does not exist." % path + # Need to save alembic settings and reapply, cause referencing resets + # them to incoming data. + alembic_attrs = ["speed", "offset", "cycleType"] + alembic_data = {} + if representation["name"] == "abc": + alembic_nodes = cmds.ls( + "{}:*".format(members[0].split(":")[0]), type="AlembicNode" + ) + if alembic_nodes: + for attr in alembic_attrs: + node_attr = "{}.{}".format(alembic_nodes[0], attr) + alembic_data[attr] = cmds.getAttr(node_attr) + else: + cmds.warning( + "No alembic nodes found in {}".format( + cmds.ls("{}:*".format(members[0].split(":")[0])) + ) + ) + try: content = cmds.file(path, loadReference=reference_node, @@ -195,6 +214,16 @@ class ReferenceLoader(api.Loader): self.log.warning("Ignoring file read error:\n%s", exc) + # Reapply alembic settings. + if representation["name"] == "abc": + alembic_nodes = cmds.ls( + "{}:*".format(members[0].split(":")[0]), type="AlembicNode" + ) + if alembic_nodes: + for attr in alembic_attrs: + value = alembic_data[attr] + cmds.setAttr("{}.{}".format(alembic_nodes[0], attr), value) + # Fix PLN-40 for older containers created with Avalon that had the # `.verticesOnlySet` set to True. if cmds.getAttr("{}.verticesOnlySet".format(node)): diff --git a/pype/hosts/nuke/lib.py b/pype/hosts/nuke/lib.py index 8c0e37b15d..19a0784327 100644 --- a/pype/hosts/nuke/lib.py +++ b/pype/hosts/nuke/lib.py @@ -1,7 +1,6 @@ import os import re import sys -import getpass from collections import OrderedDict from avalon import api, io, lib @@ -1060,310 +1059,6 @@ def get_write_node_template_attr(node): return avalon.nuke.lib.fix_data_for_node_create(correct_data) -class BuildWorkfile(WorkfileSettings): - """ - Building first version of workfile. - - Settings are taken from presets and db. It will add all subsets - in last version for defined representaions - - Arguments: - variable (type): description - - """ - xpos = 0 - ypos = 0 - xpos_size = 80 - ypos_size = 90 - xpos_gap = 50 - ypos_gap = 50 - pos_layer = 10 - - def __init__(self, - root_path=None, - root_node=None, - nodes=None, - to_script=None, - **kwargs): - """ - A short description. - - A bit longer description. - - Argumetns: - root_path (str): description - root_node (nuke.Node): description - nodes (list): list of nuke.Node - nodes_effects (dict): dictionary with subsets - - Example: - nodes_effects = { - "plateMain": { - "nodes": [ - [("Class", "Reformat"), - ("resize", "distort"), - ("flip", True)], - - [("Class", "Grade"), - ("blackpoint", 0.5), - ("multiply", 0.4)] - ] - }, - } - - """ - - WorkfileSettings.__init__(self, - root_node=root_node, - nodes=nodes, - **kwargs) - self.to_script = to_script - # collect data for formating - self.data_tmp = { - "project": {"name": self._project["name"], - "code": self._project["data"].get("code", "")}, - "asset": self._asset or os.environ["AVALON_ASSET"], - "task": kwargs.get("task") or api.Session["AVALON_TASK"], - "hierarchy": kwargs.get("hierarchy") or pype.get_hierarchy(), - "version": kwargs.get("version", {}).get("name", 1), - "user": getpass.getuser(), - "comment": "firstBuild", - "ext": "nk" - } - - # get presets from anatomy - anatomy = get_anatomy() - # format anatomy - anatomy_filled = anatomy.format(self.data_tmp) - - # get dir and file for workfile - self.work_dir = anatomy_filled["work"]["folder"] - self.work_file = anatomy_filled["work"]["file"] - - def save_script_as(self, path=None): - # first clear anything in open window - nuke.scriptClear() - - if not path: - dir = self.work_dir - path = os.path.join( - self.work_dir, - self.work_file).replace("\\", "/") - else: - dir = os.path.dirname(path) - - # check if folder is created - if not os.path.exists(dir): - os.makedirs(dir) - - # save script to path - nuke.scriptSaveAs(path) - - def process(self, - regex_filter=None, - version=None, - representations=["exr", "dpx", "lutJson", "mov", - "preview", "png", "jpeg", "jpg"]): - """ - A short description. - - A bit longer description. - - Args: - regex_filter (raw string): regex pattern to filter out subsets - version (int): define a particular version, None gets last - representations (list): - - Returns: - type: description - - Raises: - Exception: description - - """ - - if not self.to_script: - # save the script - self.save_script_as() - - # create viewer and reset frame range - viewer = self.get_nodes(nodes_filter=["Viewer"]) - if not viewer: - vn = nuke.createNode("Viewer") - vn["xpos"].setValue(self.xpos) - vn["ypos"].setValue(self.ypos) - else: - vn = viewer[-1] - - # move position - self.position_up() - - wn = self.write_create() - wn["xpos"].setValue(self.xpos) - wn["ypos"].setValue(self.ypos) - wn["render"].setValue(True) - vn.setInput(0, wn) - - # adding backdrop under write - self.create_backdrop(label="Render write \n\n\n\nOUTPUT", - color='0xcc1102ff', layer=-1, - nodes=[wn]) - - # move position - self.position_up(4) - - # set frame range for new viewer - self.reset_frame_range_handles() - - # get all available representations - subsets = pype.get_subsets(self._asset, - regex_filter=regex_filter, - version=version, - representations=representations) - - for name, subset in subsets.items(): - log.debug("___________________") - log.debug(name) - log.debug(subset["version"]) - - nodes_backdrop = list() - for name, subset in subsets.items(): - if "lut" in name: - continue - log.info("Building Loader to: `{}`".format(name)) - version = subset["version"] - log.info("Version to: `{}`".format(version["name"])) - representations = subset["representaions"] - for repr in representations: - rn = self.read_loader(repr) - rn["xpos"].setValue(self.xpos) - rn["ypos"].setValue(self.ypos) - wn.setInput(0, rn) - - # get editional nodes - lut_subset = [s for n, s in subsets.items() - if "lut{}".format(name.lower()) in n.lower()] - log.debug(">> lut_subset: `{}`".format(lut_subset)) - - if len(lut_subset) > 0: - lsub = lut_subset[0] - fxn = self.effect_loader(lsub["representaions"][-1]) - fxn_ypos = fxn["ypos"].value() - fxn["ypos"].setValue(fxn_ypos - 100) - nodes_backdrop.append(fxn) - - nodes_backdrop.append(rn) - # move position - self.position_right() - - # adding backdrop under all read nodes - self.create_backdrop(label="Loaded Reads", - color='0x2d7702ff', layer=-1, - nodes=nodes_backdrop) - - def read_loader(self, representation): - """ - Gets Loader plugin for image sequence or mov - - Arguments: - representation (dict): avalon db entity - - """ - context = representation["context"] - - loader_name = "LoadSequence" - if "mov" in context["representation"]: - loader_name = "LoadMov" - - loader_plugin = None - for Loader in api.discover(api.Loader): - if Loader.__name__ != loader_name: - continue - - loader_plugin = Loader - - return api.load(Loader=loader_plugin, - representation=representation["_id"]) - - def effect_loader(self, representation): - """ - Gets Loader plugin for effects - - Arguments: - representation (dict): avalon db entity - - """ - loader_name = "LoadLuts" - - loader_plugin = None - for Loader in api.discover(api.Loader): - if Loader.__name__ != loader_name: - continue - - loader_plugin = Loader - - return api.load(Loader=loader_plugin, - representation=representation["_id"]) - - def write_create(self): - """ - Create render write - - Arguments: - representation (dict): avalon db entity - - """ - task = self.data_tmp["task"] - sanitized_task = re.sub('[^0-9a-zA-Z]+', '', task) - subset_name = "render{}Main".format( - sanitized_task.capitalize()) - - Create_name = "CreateWriteRender" - - creator_plugin = None - for Creator in api.discover(api.Creator): - if Creator.__name__ != Create_name: - continue - - creator_plugin = Creator - - # return api.create() - return creator_plugin(subset_name, self._asset).process() - - def create_backdrop(self, label="", color=None, layer=0, - nodes=None): - """ - Create Backdrop node - - Arguments: - color (str): nuke compatible string with color code - layer (int): layer of node usually used (self.pos_layer - 1) - label (str): the message - nodes (list): list of nodes to be wrapped into backdrop - - """ - assert isinstance(nodes, list), "`nodes` should be a list of nodes" - layer = self.pos_layer + layer - - create_backdrop(label=label, color=color, layer=layer, nodes=nodes) - - def position_reset(self, xpos=0, ypos=0): - self.xpos = xpos - self.ypos = ypos - - def position_right(self, multiply=1): - self.xpos += (self.xpos_size * multiply) + self.xpos_gap - - def position_left(self, multiply=1): - self.xpos -= (self.xpos_size * multiply) + self.xpos_gap - - def position_down(self, multiply=1): - self.ypos -= (self.ypos_size * multiply) + self.ypos_gap - - def position_up(self, multiply=1): - self.ypos -= (self.ypos_size * multiply) + self.ypos_gap - - class ExporterReview: """ Base class object for generating review data from Nuke diff --git a/pype/hosts/nuke/menu.py b/pype/hosts/nuke/menu.py index 7306add9fe..b1ef7f47c4 100644 --- a/pype/hosts/nuke/menu.py +++ b/pype/hosts/nuke/menu.py @@ -2,10 +2,12 @@ import nuke from avalon.api import Session from pype.hosts.nuke import lib +from ...lib import BuildWorkfile from pype.api import Logger log = Logger().get_logger(__name__, "nuke") + def install(): menubar = nuke.menu("Nuke") menu = menubar.findItem(Session["AVALON_LABEL"]) @@ -20,7 +22,11 @@ def install(): log.debug("Changing Item: {}".format(rm_item)) # rm_item[1].setEnabled(False) menu.removeItem(rm_item[1].name()) - menu.addCommand(new_name, lambda: workfile_settings().reset_resolution(), index=(rm_item[0])) + menu.addCommand( + new_name, + lambda: workfile_settings().reset_resolution(), + index=(rm_item[0]) + ) # replace reset frame range from avalon core to pype's name = "Reset Frame Range" @@ -31,33 +37,38 @@ def install(): log.debug("Changing Item: {}".format(rm_item)) # rm_item[1].setEnabled(False) menu.removeItem(rm_item[1].name()) - menu.addCommand(new_name, lambda: workfile_settings().reset_frame_range_handles(), index=(rm_item[0])) + menu.addCommand( + new_name, + lambda: workfile_settings().reset_frame_range_handles(), + index=(rm_item[0]) + ) # add colorspace menu item - name = "Set colorspace" + name = "Set Colorspace" menu.addCommand( name, lambda: workfile_settings().set_colorspace(), - index=(rm_item[0]+2) + index=(rm_item[0] + 2) ) log.debug("Adding menu item: {}".format(name)) # add workfile builder menu item - name = "Build First Workfile.." + name = "Build Workfile" menu.addCommand( - name, lambda: lib.BuildWorkfile().process(), - index=(rm_item[0]+7) + name, lambda: BuildWorkfile().process(), + index=(rm_item[0] + 7) ) log.debug("Adding menu item: {}".format(name)) # add item that applies all setting above - name = "Apply all settings" + name = "Apply All Settings" menu.addCommand( - name, lambda: workfile_settings().set_context_settings(), index=(rm_item[0]+3) + name, + lambda: workfile_settings().set_context_settings(), + index=(rm_item[0] + 3) ) log.debug("Adding menu item: {}".format(name)) - def uninstall(): menubar = nuke.menu("Nuke") diff --git a/pype/hosts/resolve/__init__.py b/pype/hosts/resolve/__init__.py index 72d6314b5e..c8f45259ff 100644 --- a/pype/hosts/resolve/__init__.py +++ b/pype/hosts/resolve/__init__.py @@ -1,17 +1,34 @@ +from .utils import ( + setup, + get_resolve_module +) + from .pipeline import ( install, uninstall, ls, containerise, publish, - launch_workfiles_app + launch_workfiles_app, + maintained_selection ) -from .utils import ( - setup, - get_resolve_module +from .lib import ( + get_project_manager, + get_current_project, + get_current_sequence, + get_current_track_items, + create_current_sequence_media_bin, + create_compound_clip, + swap_clips, + get_pype_clip_metadata, + set_project_manager_to_folder_name ) +from .menu import launch_pype_menu + +from .plugin import Creator + from .workio import ( open_file, save_file, @@ -21,12 +38,8 @@ from .workio import ( work_root ) -from .lib import ( - get_project_manager, - set_project_manager_to_folder_name -) - -from .menu import launch_pype_menu +bmdvr = None +bmdvf = None __all__ = [ # pipeline @@ -37,6 +50,7 @@ __all__ = [ "reload_pipeline", "publish", "launch_workfiles_app", + "maintained_selection", # utils "setup", @@ -44,16 +58,30 @@ __all__ = [ # lib "get_project_manager", + "get_current_project", + "get_current_sequence", + "get_current_track_items", + "create_current_sequence_media_bin", + "create_compound_clip", + "swap_clips", + "get_pype_clip_metadata", "set_project_manager_to_folder_name", # menu "launch_pype_menu", + # plugin + "Creator", + # workio "open_file", "save_file", "current_file", "has_unsaved_changes", "file_extensions", - "work_root" + "work_root", + + # singleton with black magic resolve module + "bmdvr", + "bmdvf" ] diff --git a/pype/hosts/resolve/action.py b/pype/hosts/resolve/action.py index 31830937c1..a9803cef4e 100644 --- a/pype/hosts/resolve/action.py +++ b/pype/hosts/resolve/action.py @@ -21,9 +21,9 @@ class SelectInvalidAction(pyblish.api.Action): def process(self, context, plugin): try: - from pype.hosts.resolve.utils import get_resolve_module - resolve = get_resolve_module() - self.log.debug(resolve) + from . import get_project_manager + pm = get_project_manager() + self.log.debug(pm) except ImportError: raise ImportError("Current host is not Resolve") diff --git a/pype/hosts/resolve/lib.py b/pype/hosts/resolve/lib.py index 2576136df5..deb4fa6339 100644 --- a/pype/hosts/resolve/lib.py +++ b/pype/hosts/resolve/lib.py @@ -1,20 +1,406 @@ import sys -from .utils import get_resolve_module -from pypeapp import Logger +import json +from opentimelineio import opentime +from pprint import pformat + +from pype.api import Logger log = Logger().get_logger(__name__, "resolve") self = sys.modules[__name__] self.pm = None +self.rename_index = 0 +self.rename_add = 0 +self.pype_metadata_key = "VFX Notes" def get_project_manager(): + from . import bmdvr if not self.pm: - resolve = get_resolve_module() - self.pm = resolve.GetProjectManager() + self.pm = bmdvr.GetProjectManager() return self.pm +def get_current_project(): + # initialize project manager + get_project_manager() + + return self.pm.GetCurrentProject() + + +def get_current_sequence(): + # get current project + project = get_current_project() + + return project.GetCurrentTimeline() + + +def get_current_track_items( + filter=False, + track_type=None, + selecting_color=None): + """ Gets all available current timeline track items + """ + track_type = track_type or "video" + selecting_color = selecting_color or "Chocolate" + project = get_current_project() + sequence = get_current_sequence() + selected_clips = list() + + # get all tracks count filtered by track type + selected_track_count = sequence.GetTrackCount(track_type) + + # loop all tracks and get items + _clips = dict() + for track_index in range(1, (int(selected_track_count) + 1)): + track_name = sequence.GetTrackName(track_type, track_index) + track_track_items = sequence.GetItemListInTrack( + track_type, track_index) + _clips[track_index] = track_track_items + + _data = { + "project": project, + "sequence": sequence, + "track": { + "name": track_name, + "index": track_index, + "type": track_type} + } + # get track item object and its color + for clip_index, ti in enumerate(_clips[track_index]): + data = _data.copy() + data["clip"] = { + "item": ti, + "index": clip_index + } + ti_color = ti.GetClipColor() + if filter is True: + if selecting_color in ti_color: + selected_clips.append(data) + # ti.ClearClipColor() + else: + selected_clips.append(data) + + return selected_clips + + +def create_current_sequence_media_bin(sequence): + seq_name = sequence.GetName() + media_pool = get_current_project().GetMediaPool() + root_folder = media_pool.GetRootFolder() + sub_folders = root_folder.GetSubFolderList() + testing_names = list() + + print(f"_ sub_folders: {sub_folders}") + for subfolder in sub_folders: + subf_name = subfolder.GetName() + if seq_name in subf_name: + testing_names.append(subfolder) + else: + testing_names.append(False) + + matching = next((f for f in testing_names if f is not False), None) + + if not matching: + new_folder = media_pool.AddSubFolder(root_folder, seq_name) + media_pool.SetCurrentFolder(new_folder) + else: + media_pool.SetCurrentFolder(matching) + + return media_pool.GetCurrentFolder() + + +def get_name_with_data(clip_data, presets): + """ + Take hierarchy data from presets and build name with parents data + + Args: + clip_data (dict): clip data from `get_current_track_items()` + presets (dict): data from create plugin + + Returns: + list: name, data + + """ + def _replace_hash_to_expression(name, text): + _spl = text.split("#") + _len = (len(_spl) - 1) + _repl = f"{{{name}:0>{_len}}}" + new_text = text.replace(("#" * _len), _repl) + return new_text + + # presets data + clip_name = presets["clipName"] + hierarchy = presets["hierarchy"] + hierarchy_data = presets["hierarchyData"].copy() + count_from = presets["countFrom"] + steps = presets["steps"] + + # reset rename_add + if self.rename_add < count_from: + self.rename_add = count_from + + # shot num calculate + if self.rename_index == 0: + shot_num = self.rename_add + else: + shot_num = self.rename_add + steps + + print(f"shot_num: {shot_num}") + + # clip data + _data = { + "sequence": clip_data["sequence"].GetName(), + "track": clip_data["track"]["name"].replace(" ", "_"), + "shot": shot_num + } + + # solve # in test to pythonic explression + for k, v in hierarchy_data.items(): + if "#" not in v: + continue + hierarchy_data[k] = _replace_hash_to_expression(k, v) + + # fill up pythonic expresisons + for k, v in hierarchy_data.items(): + hierarchy_data[k] = v.format(**_data) + + # fill up clip name and hierarchy keys + hierarchy = hierarchy.format(**hierarchy_data) + clip_name = clip_name.format(**hierarchy_data) + + self.rename_add = shot_num + print(f"shot_num: {shot_num}") + + return (clip_name, { + "hierarchy": hierarchy, + "hierarchyData": hierarchy_data + }) + + +def create_compound_clip(clip_data, folder, rename=False, **kwargs): + """ + Convert timeline object into nested timeline object + + Args: + clip_data (dict): timeline item object packed into dict + with project, timeline (sequence) + folder (resolve.MediaPool.Folder): media pool folder object, + rename (bool)[optional]: renaming in sequence or not + kwargs (optional): additional data needed for rename=True (presets) + + Returns: + resolve.MediaPoolItem: media pool item with compound clip timeline(cct) + """ + # get basic objects form data + project = clip_data["project"] + sequence = clip_data["sequence"] + clip = clip_data["clip"] + + # get details of objects + clip_item = clip["item"] + track = clip_data["track"] + + mp = project.GetMediaPool() + + # get clip attributes + clip_attributes = get_clip_attributes(clip_item) + print(f"_ clip_attributes: {pformat(clip_attributes)}") + + if rename: + presets = kwargs.get("presets") + if presets: + name, data = get_name_with_data(clip_data, presets) + # add hirarchy data to clip attributes + clip_attributes.update(data) + else: + name = "{:0>3}_{:0>4}".format( + int(track["index"]), int(clip["index"])) + else: + # build name + clip_name_split = clip_item.GetName().split(".") + name = "_".join([ + track["name"], + str(track["index"]), + clip_name_split[0], + str(clip["index"])] + ) + + # get metadata + mp_item = clip_item.GetMediaPoolItem() + mp_props = mp_item.GetClipProperty() + + mp_first_frame = int(mp_props["Start"]) + mp_last_frame = int(mp_props["End"]) + + # initialize basic source timing for otio + ci_l_offset = clip_item.GetLeftOffset() + ci_duration = clip_item.GetDuration() + rate = float(mp_props["FPS"]) + + # source rational times + mp_in_rc = opentime.RationalTime((ci_l_offset), rate) + mp_out_rc = opentime.RationalTime((ci_l_offset + ci_duration - 1), rate) + + # get frame in and out for clip swaping + in_frame = opentime.to_frames(mp_in_rc) + out_frame = opentime.to_frames(mp_out_rc) + + # keep original sequence + sq_origin = sequence + + # Set current folder to input media_pool_folder: + mp.SetCurrentFolder(folder) + + # check if clip doesnt exist already: + clips = folder.GetClipList() + cct = next((c for c in clips + if c.GetName() in name), None) + + if cct: + print(f"_ cct exists: {cct}") + else: + # Create empty timeline in current folder and give name: + cct = mp.CreateEmptyTimeline(name) + + # check if clip doesnt exist already: + clips = folder.GetClipList() + cct = next((c for c in clips + if c.GetName() in name), None) + print(f"_ cct created: {cct}") + + # Set current timeline to created timeline: + project.SetCurrentTimeline(cct) + + # Add input clip to the current timeline: + mp.AppendToTimeline([{ + "mediaPoolItem": mp_item, + "startFrame": mp_first_frame, + "endFrame": mp_last_frame + }]) + + # Set current timeline to the working timeline: + project.SetCurrentTimeline(sq_origin) + + # Add collected metadata and attributes to the comound clip: + if mp_item.GetMetadata(self.pype_metadata_key): + clip_attributes[self.pype_metadata_key] = mp_item.GetMetadata( + self.pype_metadata_key)[self.pype_metadata_key] + + # stringify + clip_attributes = json.dumps(clip_attributes) + + # add attributes to metadata + for k, v in mp_item.GetMetadata().items(): + cct.SetMetadata(k, v) + + # add metadata to cct + cct.SetMetadata(self.pype_metadata_key, clip_attributes) + + # reset start timecode of the compound clip + cct.SetClipProperty("Start TC", mp_props["Start TC"]) + + # swap clips on timeline + swap_clips(clip_item, cct, name, in_frame, out_frame) + + cct.SetClipColor("Pink") + return cct + + +def swap_clips(from_clip, to_clip, to_clip_name, to_in_frame, to_out_frame): + """ + Swaping clips on timeline in timelineItem + + It will add take and activate it to the frame range which is inputted + + Args: + from_clip (resolve.mediaPoolItem) + to_clip (resolve.mediaPoolItem) + to_clip_name (str): name of to_clip + to_in_frame (float): cut in frame, usually `GetLeftOffset()` + to_out_frame (float): cut out frame, usually left offset plus duration + + Returns: + bool: True if successfully replaced + + """ + # add clip item as take to timeline + take = from_clip.AddTake( + to_clip, + float(to_in_frame), + float(to_out_frame) + ) + + if not take: + return False + + for take_index in range(1, (int(from_clip.GetTakesCount()) + 1)): + take_item = from_clip.GetTakeByIndex(take_index) + take_mp_item = take_item["mediaPoolItem"] + if to_clip_name in take_mp_item.GetName(): + from_clip.SelectTakeByIndex(take_index) + from_clip.FinalizeTake() + return True + return False + + +def validate_tc(x): + # Validate and reformat timecode string + + if len(x) != 11: + print('Invalid timecode. Try again.') + + c = ':' + colonized = x[:2] + c + x[3:5] + c + x[6:8] + c + x[9:] + + if colonized.replace(':', '').isdigit(): + print(f"_ colonized: {colonized}") + return colonized + else: + print('Invalid timecode. Try again.') + + +def get_pype_clip_metadata(clip): + """ + Get pype metadata created by creator plugin + + Attributes: + clip (resolve.TimelineItem): resolve's object + + Returns: + dict: hierarchy, orig clip attributes + """ + mp_item = clip.GetMediaPoolItem() + metadata = mp_item.GetMetadata() + + return metadata.get(self.pype_metadata_key) + + +def get_clip_attributes(clip): + """ + Collect basic atrributes from resolve timeline item + + Args: + clip (resolve.TimelineItem): timeline item object + + Returns: + dict: all collected attributres as key: values + """ + mp_item = clip.GetMediaPoolItem() + + data = { + "clipIn": clip.GetStart(), + "clipOut": clip.GetEnd(), + "clipLeftOffset": clip.GetLeftOffset(), + "clipRightOffset": clip.GetRightOffset(), + "clipMarkers": clip.GetMarkers(), + "clipFlags": clip.GetFlagList(), + "sourceId": mp_item.GetMediaId(), + "sourceProperties": mp_item.GetClipProperty() + } + return data + + def set_project_manager_to_folder_name(folder_name): """ Sets context of Project manager to given folder by name. diff --git a/pype/hosts/resolve/menu_style.qss b/pype/hosts/resolve/menu_style.qss index df4fd7e949..ea11c4ca2e 100644 --- a/pype/hosts/resolve/menu_style.qss +++ b/pype/hosts/resolve/menu_style.qss @@ -1,6 +1,7 @@ QWidget { background-color: #282828; border-radius: 3; + font-size: 13px; } QPushButton { @@ -20,10 +21,38 @@ QPushButton:hover { color: #e64b3d; } +QSpinBox { + border: 1px solid #090909; + background-color: #201f1f; + color: #ffffff; + padding: 2; + max-width: 8em; + qproperty-alignment: AlignCenter; +} + +QLineEdit { + border: 1px solid #090909; + border-radius: 3px; + background-color: #201f1f; + color: #ffffff; + padding: 2; + min-width: 10em; + qproperty-alignment: AlignCenter; +} + #PypeMenu { border: 1px solid #fef9ef; } -#Spacer { +QVBoxLayout { background-color: #282828; } + +#Devider { + border: 1px solid #090909; + background-color: #585858; +} + +QLabel { + color: #77776b; +} diff --git a/pype/hosts/resolve/pipeline.py b/pype/hosts/resolve/pipeline.py index 967aed1436..92bef2e13b 100644 --- a/pype/hosts/resolve/pipeline.py +++ b/pype/hosts/resolve/pipeline.py @@ -2,27 +2,23 @@ Basic avalon integration """ import os -# import sys +import contextlib from avalon.tools import workfiles from avalon import api as avalon from pyblish import api as pyblish -from pypeapp import Logger +import pype +from pype.api import Logger log = Logger().get_logger(__name__, "resolve") -# self = sys.modules[__name__] - AVALON_CONFIG = os.environ["AVALON_CONFIG"] -PARENT_DIR = os.path.dirname(__file__) -PACKAGE_DIR = os.path.dirname(PARENT_DIR) -PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") -LOAD_PATH = os.path.join(PLUGINS_DIR, "resolve", "load") -CREATE_PATH = os.path.join(PLUGINS_DIR, "resolve", "create") -INVENTORY_PATH = os.path.join(PLUGINS_DIR, "resolve", "inventory") +LOAD_PATH = os.path.join(pype.PLUGINS_DIR, "resolve", "load") +CREATE_PATH = os.path.join(pype.PLUGINS_DIR, "resolve", "create") +INVENTORY_PATH = os.path.join(pype.PLUGINS_DIR, "resolve", "inventory") PUBLISH_PATH = os.path.join( - PLUGINS_DIR, "resolve", "publish" + pype.PLUGINS_DIR, "resolve", "publish" ).replace("\\", "/") AVALON_CONTAINERS = ":AVALON_CONTAINERS" @@ -40,11 +36,13 @@ def install(): See the Maya equivalent for inspiration on how to implement this. """ + from . import get_resolve_module # Disable all families except for the ones we explicitly want to see family_states = [ "imagesequence", - "mov" + "mov", + "clip" ] avalon.data["familiesStateDefault"] = False avalon.data["familiesStateToggled"] = family_states @@ -59,6 +57,8 @@ def install(): avalon.register_plugin_path(avalon.Creator, CREATE_PATH) avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) + get_resolve_module() + def uninstall(): """Uninstall all tha was installed @@ -140,3 +140,26 @@ def publish(parent): """Shorthand to publish from within host""" from avalon.tools import publish return publish.show(parent) + + +@contextlib.contextmanager +def maintained_selection(): + """Maintain selection during context + + Example: + >>> with maintained_selection(): + ... node['selected'].setValue(True) + >>> print(node['selected'].value()) + False + """ + try: + # do the operation + yield + finally: + pass + + +def reset_selection(): + """Deselect all selected nodes + """ + pass diff --git a/pype/hosts/resolve/plugin.py b/pype/hosts/resolve/plugin.py index 628d4bdb26..72eec04896 100644 --- a/pype/hosts/resolve/plugin.py +++ b/pype/hosts/resolve/plugin.py @@ -1,6 +1,182 @@ +import re from avalon import api -# from pype.hosts.resolve import lib as drlib +from pype.hosts import resolve from avalon.vendor import qargparse +from pype.api import config + +from Qt import QtWidgets, QtCore + + +class CreatorWidget(QtWidgets.QDialog): + + # output items + items = dict() + + def __init__(self, name, info, presets, parent=None): + super(CreatorWidget, self).__init__(parent) + + self.setObjectName(name) + + self.setWindowFlags( + QtCore.Qt.Window + | QtCore.Qt.CustomizeWindowHint + | QtCore.Qt.WindowTitleHint + | QtCore.Qt.WindowCloseButtonHint + | QtCore.Qt.WindowStaysOnTopHint + ) + self.setWindowTitle(name or "Pype Creator Input") + + # Where inputs and labels are set + self.content_widget = [QtWidgets.QWidget(self)] + top_layout = QtWidgets.QFormLayout(self.content_widget[0]) + top_layout.setObjectName("ContentLayout") + top_layout.addWidget(Spacer(5, self)) + + # first add widget tag line + top_layout.addWidget(QtWidgets.QLabel(info)) + + top_layout.addWidget(Spacer(5, self)) + + # main dynamic layout + self.content_widget.append(QtWidgets.QWidget(self)) + content_layout = QtWidgets.QFormLayout(self.content_widget[-1]) + + # add preset data into input widget layout + self.items = self.add_presets_to_layout(content_layout, presets) + + # Confirmation buttons + btns_widget = QtWidgets.QWidget(self) + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + + cancel_btn = QtWidgets.QPushButton("Cancel") + btns_layout.addWidget(cancel_btn) + + ok_btn = QtWidgets.QPushButton("Ok") + btns_layout.addWidget(ok_btn) + + # Main layout of the dialog + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(10, 10, 10, 10) + main_layout.setSpacing(0) + + # adding content widget + for w in self.content_widget: + main_layout.addWidget(w) + + main_layout.addWidget(btns_widget) + + ok_btn.clicked.connect(self._on_ok_clicked) + cancel_btn.clicked.connect(self._on_cancel_clicked) + + stylesheet = resolve.menu.load_stylesheet() + self.setStyleSheet(stylesheet) + + def _on_ok_clicked(self): + self.result = self.value(self.items) + self.close() + + def _on_cancel_clicked(self): + self.result = None + self.close() + + def value(self, data): + for k, v in data.items(): + if isinstance(v, dict): + print(f"nested: {k}") + data[k] = self.value(v) + elif getattr(v, "value", None): + print(f"normal int: {k}") + result = v.value() + data[k] = result() + else: + print(f"normal text: {k}") + result = v.text() + data[k] = result() + return data + + def camel_case_split(self, text): + matches = re.finditer( + '.+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)', text) + return " ".join([str(m.group(0)).capitalize() for m in matches]) + + def create_row(self, layout, type, text, **kwargs): + # get type attribute from qwidgets + attr = getattr(QtWidgets, type) + + # convert label text to normal capitalized text with spaces + label_text = self.camel_case_split(text) + + # assign the new text to lable widget + label = QtWidgets.QLabel(label_text) + label.setObjectName("LineLabel") + + # create attribute name text strip of spaces + attr_name = text.replace(" ", "") + + # create attribute and assign default values + setattr( + self, + attr_name, + attr(parent=self)) + + # assign the created attribute to variable + item = getattr(self, attr_name) + for func, val in kwargs.items(): + if getattr(item, func): + func_attr = getattr(item, func) + func_attr(val) + + # add to layout + layout.addRow(label, item) + + return item + + def add_presets_to_layout(self, content_layout, data): + for k, v in data.items(): + if isinstance(v, dict): + # adding spacer between sections + self.content_widget.append(QtWidgets.QWidget(self)) + devider = QtWidgets.QVBoxLayout(self.content_widget[-1]) + devider.addWidget(Spacer(5, self)) + devider.setObjectName("Devider") + + # adding nested layout with label + self.content_widget.append(QtWidgets.QWidget(self)) + nested_content_layout = QtWidgets.QFormLayout( + self.content_widget[-1]) + nested_content_layout.setObjectName("NestedContentLayout") + + # add nested key as label + self.create_row(nested_content_layout, "QLabel", k) + data[k] = self.add_presets_to_layout(nested_content_layout, v) + elif isinstance(v, str): + print(f"layout.str: {k}") + print(f"content_layout: {content_layout}") + data[k] = self.create_row( + content_layout, "QLineEdit", k, setText=v) + elif isinstance(v, int): + print(f"layout.int: {k}") + print(f"content_layout: {content_layout}") + data[k] = self.create_row( + content_layout, "QSpinBox", k, setValue=v) + return data + + +class Spacer(QtWidgets.QWidget): + def __init__(self, height, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + + self.setFixedHeight(height) + + real_spacer = QtWidgets.QWidget(self) + real_spacer.setObjectName("Spacer") + real_spacer.setFixedHeight(height) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(real_spacer) + + self.setLayout(layout) def get_reference_node_parents(ref): @@ -73,3 +249,25 @@ class SequenceLoader(api.Loader): """Remove an existing `container` """ pass + + +class Creator(api.Creator): + """Creator class wrapper + """ + marker_color = "Purple" + + def __init__(self, *args, **kwargs): + super(Creator, self).__init__(*args, **kwargs) + self.presets = config.get_presets()['plugins']["resolve"][ + "create"].get(self.__class__.__name__, {}) + + # adding basic current context resolve objects + self.project = resolve.get_current_project() + self.sequence = resolve.get_current_sequence() + + if (self.options or {}).get("useSelection"): + self.selected = resolve.get_current_track_items(filter=True) + else: + self.selected = resolve.get_current_track_items(filter=False) + + self.widget = CreatorWidget diff --git a/pype/hosts/resolve/preload_console.py b/pype/hosts/resolve/preload_console.py index ea1bd4f180..58975777b8 100644 --- a/pype/hosts/resolve/preload_console.py +++ b/pype/hosts/resolve/preload_console.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import time from pype.hosts.resolve.utils import get_resolve_module -from pypeapp import Logger +from pype.api import Logger log = Logger().get_logger(__name__, "resolve") diff --git a/pype/hosts/resolve/utility_scripts/Pype_menu.py b/pype/hosts/resolve/utility_scripts/Pype_menu.py index 1f5cd36277..230a7a80f0 100644 --- a/pype/hosts/resolve/utility_scripts/Pype_menu.py +++ b/pype/hosts/resolve/utility_scripts/Pype_menu.py @@ -3,7 +3,7 @@ import sys import avalon.api as avalon import pype -from pypeapp import Logger +from pype.api import Logger log = Logger().get_logger(__name__) diff --git a/pype/hosts/resolve/utility_scripts/__dev_compound_clip.py b/pype/hosts/resolve/utility_scripts/__dev_compound_clip.py deleted file mode 100644 index fe47008c70..0000000000 --- a/pype/hosts/resolve/utility_scripts/__dev_compound_clip.py +++ /dev/null @@ -1,65 +0,0 @@ -#! python3 -# -*- coding: utf-8 -*- - - -# convert clip def -def convert_clip(timeline=None): - """Convert timeline item (clip) into compound clip pype container - - Args: - timeline (MediaPool.Timeline): Object of timeline - - Returns: - bool: `True` if success - - Raises: - Exception: description - - """ - pass - - -# decorator function create_current_timeline_media_bin() -def create_current_timeline_media_bin(timeline=None): - """Convert timeline item (clip) into compound clip pype container - - Args: - timeline (MediaPool.Timeline): Object of timeline - - Returns: - bool: `True` if success - - Raises: - Exception: description - - """ - pass - - -# decorator function get_selected_track_items() -def get_selected_track_items(): - """Convert timeline item (clip) into compound clip pype container - - Args: - timeline (MediaPool.Timeline): Object of timeline - - Returns: - bool: `True` if success - - Raises: - Exception: description - - """ - print("testText") - - -# PypeCompoundClip() class -class PypeCompoundClip(object): - """docstring for .""" - - def __init__(self, arg): - super(self).__init__() - self.arg = arg - - def create_compound_clip(self): - pass diff --git a/pype/hosts/resolve/utility_scripts/__test_pyblish.py b/pype/hosts/resolve/utility_scripts/__test_pyblish.py deleted file mode 100644 index a6fe991025..0000000000 --- a/pype/hosts/resolve/utility_scripts/__test_pyblish.py +++ /dev/null @@ -1,57 +0,0 @@ -import os -import sys -import pype -import importlib -import pyblish.api -import pyblish.util -import avalon.api -from avalon.tools import publish -from pypeapp import Logger - -log = Logger().get_logger(__name__) - - -def main(env): - # Registers pype's Global pyblish plugins - pype.install() - - # Register Host (and it's pyblish plugins) - host_name = env["AVALON_APP"] - # TODO not sure if use "pype." or "avalon." for host import - host_import_str = f"pype.{host_name}" - - try: - host_module = importlib.import_module(host_import_str) - except ModuleNotFoundError: - log.error(( - f"Host \"{host_name}\" can't be imported." - f" Import string \"{host_import_str}\" failed." - )) - return False - - avalon.api.install(host_module) - - # Register additional paths - addition_paths_str = env.get("PUBLISH_PATHS") or "" - addition_paths = addition_paths_str.split(os.pathsep) - for path in addition_paths: - path = os.path.normpath(path) - if not os.path.exists(path): - continue - - pyblish.api.register_plugin_path(path) - - # Register project specific plugins - project_name = os.environ["AVALON_PROJECT"] - project_plugins_paths = env.get("PYPE_PROJECT_PLUGINS") or "" - for path in project_plugins_paths.split(os.pathsep): - plugin_path = os.path.join(path, project_name, "plugins") - if os.path.exists(plugin_path): - pyblish.api.register_plugin_path(plugin_path) - - return publish.show() - - -if __name__ == "__main__": - result = main(os.environ) - sys.exit(not bool(result)) diff --git a/pype/hosts/resolve/utility_scripts/__test_subprocess.py b/pype/hosts/resolve/utility_scripts/__test_subprocess.py deleted file mode 100644 index bdc57bbf00..0000000000 --- a/pype/hosts/resolve/utility_scripts/__test_subprocess.py +++ /dev/null @@ -1,35 +0,0 @@ -#! python3 -# -*- coding: utf-8 -*- -import os -from pypeapp import execute, Logger -from pype.hosts.resolve.utils import get_resolve_module - -log = Logger().get_logger("Resolve") - -CURRENT_DIR = os.getenv("RESOLVE_UTILITY_SCRIPTS_DIR", "") -python_dir = os.getenv("PYTHON36_RESOLVE") -python_exe = os.path.normpath( - os.path.join(python_dir, "python.exe") -) - -resolve = get_resolve_module() -PM = resolve.GetProjectManager() -P = PM.GetCurrentProject() - -log.info(P.GetName()) - - -# ______________________________________________________ -# testing subprocessing Scripts -testing_py = os.path.join(CURRENT_DIR, "ResolvePageSwitcher.py") -testing_py = os.path.normpath(testing_py) -log.info(f"Testing path to script: `{testing_py}`") - -returncode = execute( - [python_exe, os.path.normpath(testing_py)], - env=dict(os.environ) -) - -# Check if output file exists -if returncode != 0: - log.error("Executing failed!") diff --git a/pype/hosts/resolve/utility_scripts/test.py b/pype/hosts/resolve/utility_scripts/test.py new file mode 100644 index 0000000000..69dc4768bd --- /dev/null +++ b/pype/hosts/resolve/utility_scripts/test.py @@ -0,0 +1,21 @@ +#! python3 +import sys +from pype.api import Logger +import DaVinciResolveScript as bmdvr + + +log = Logger().get_logger(__name__) + + +def main(): + import pype.hosts.resolve as bmdvr + bm = bmdvr.utils.get_resolve_module() + log.info(f"blackmagicmodule: {bm}") + + +print(f"_>> bmdvr.scriptapp(Resolve): {bmdvr.scriptapp('Resolve')}") + + +if __name__ == "__main__": + result = main() + sys.exit(not bool(result)) diff --git a/pype/hosts/resolve/utils.py b/pype/hosts/resolve/utils.py index f5add53a6b..e11cc64b3b 100644 --- a/pype/hosts/resolve/utils.py +++ b/pype/hosts/resolve/utils.py @@ -9,18 +9,16 @@ import os import shutil from pypeapp import Logger - log = Logger().get_logger(__name__, "resolve") -self = sys.modules[__name__] -self.bmd = None - def get_resolve_module(): + from pype.hosts import resolve # dont run if already loaded - if self.bmd: - return self.bmd - + if resolve.bmdvr: + log.info(("resolve module is assigned to " + f"`pype.hosts.resolve.bmdvr`: {resolve.bmdvr}")) + return resolve.bmdvr try: """ The PYTHONPATH needs to be set correctly for this import @@ -71,8 +69,14 @@ def get_resolve_module(): ) sys.exit() # assign global var and return - self.bmd = bmd.scriptapp("Resolve") - return self.bmd + bmdvr = bmd.scriptapp("Resolve") + # bmdvf = bmd.scriptapp("Fusion") + resolve.bmdvr = bmdvr + resolve.bmdvf = bmdvr.Fusion() + log.info(("Assigning resolve module to " + f"`pype.hosts.resolve.bmdvr`: {resolve.bmdvr}")) + log.info(("Assigning resolve module to " + f"`pype.hosts.resolve.bmdvf`: {resolve.bmdvf}")) def _sync_utility_scripts(env=None): diff --git a/pype/hosts/resolve/workio.py b/pype/hosts/resolve/workio.py index e1e30a8734..9d8d320a3c 100644 --- a/pype/hosts/resolve/workio.py +++ b/pype/hosts/resolve/workio.py @@ -2,8 +2,9 @@ import os from pypeapp import Logger -from .lib import ( +from . import ( get_project_manager, + get_current_project, set_project_manager_to_folder_name ) @@ -26,7 +27,7 @@ def save_file(filepath): pm = get_project_manager() file = os.path.basename(filepath) fname, _ = os.path.splitext(file) - project = pm.GetCurrentProject() + project = get_current_project() name = project.GetName() if "Untitled Project" not in name: diff --git a/pype/lib.py b/pype/lib.py index 601c85f521..6fa204b379 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -19,7 +19,7 @@ from abc import ABCMeta, abstractmethod from avalon import io, pipeline import six import avalon.api -from .api import config, Anatomy +from .api import config, Anatomy, Logger log = logging.getLogger(__name__) @@ -1622,7 +1622,7 @@ class ApplicationAction(avalon.api.Action): parsed application `.toml` this can launch the application. """ - + _log = None config = None group = None variant = None @@ -1632,6 +1632,12 @@ class ApplicationAction(avalon.api.Action): "AVALON_TASK" ) + @property + def log(self): + if self._log is None: + self._log = Logger().get_logger(self.__class__.__name__) + return self._log + def is_compatible(self, session): for key in self.required_session_keys: if key not in session: @@ -1644,6 +1650,165 @@ class ApplicationAction(avalon.api.Action): project_name = session["AVALON_PROJECT"] asset_name = session["AVALON_ASSET"] task_name = session["AVALON_TASK"] - return launch_application( + launch_application( project_name, asset_name, task_name, self.name ) + + self._ftrack_after_launch_procedure( + project_name, asset_name, task_name + ) + + def _ftrack_after_launch_procedure( + self, project_name, asset_name, task_name + ): + # TODO move to launch hook + required_keys = ("FTRACK_SERVER", "FTRACK_API_USER", "FTRACK_API_KEY") + for key in required_keys: + if not os.environ.get(key): + self.log.debug(( + "Missing required environment \"{}\"" + " for Ftrack after launch procedure." + ).format(key)) + return + + try: + import ftrack_api + session = ftrack_api.Session(auto_connect_event_hub=True) + self.log.debug("Ftrack session created") + except Exception: + self.log.warning("Couldn't create Ftrack session") + return + + try: + entity = self._find_ftrack_task_entity( + session, project_name, asset_name, task_name + ) + self._ftrack_status_change(session, entity, project_name) + self._start_timer(session, entity, ftrack_api) + except Exception: + self.log.warning( + "Couldn't finish Ftrack procedure.", exc_info=True + ) + return + + finally: + session.close() + + def _find_ftrack_task_entity( + self, session, project_name, asset_name, task_name + ): + project_entity = session.query( + "Project where full_name is \"{}\"".format(project_name) + ).first() + if not project_entity: + self.log.warning( + "Couldn't find project \"{}\" in Ftrack.".format(project_name) + ) + return + + potential_task_entities = session.query(( + "TypedContext where parent.name is \"{}\" and project_id is \"{}\"" + ).format(asset_name, project_entity["id"])).all() + filtered_entities = [] + for _entity in potential_task_entities: + if ( + _entity.entity_type.lower() == "task" + and _entity["name"] == task_name + ): + filtered_entities.append(_entity) + + if not filtered_entities: + self.log.warning(( + "Couldn't find task \"{}\" under parent \"{}\" in Ftrack." + ).format(task_name, asset_name)) + return + + if len(filtered_entities) > 1: + self.log.warning(( + "Found more than one task \"{}\"" + " under parent \"{}\" in Ftrack." + ).format(task_name, asset_name)) + return + + return filtered_entities[0] + + def _ftrack_status_change(self, session, entity, project_name): + presets = config.get_presets(project_name)["ftrack"]["ftrack_config"] + statuses = presets.get("status_update") + if not statuses: + return + + actual_status = entity["status"]["name"].lower() + already_tested = set() + ent_path = "/".join( + [ent["name"] for ent in entity["link"]] + ) + while True: + next_status_name = None + for key, value in statuses.items(): + if key in already_tested: + continue + if actual_status in value or "_any_" in value: + if key != "_ignore_": + next_status_name = key + already_tested.add(key) + break + already_tested.add(key) + + if next_status_name is None: + break + + try: + query = "Status where name is \"{}\"".format( + next_status_name + ) + status = session.query(query).one() + + entity["status"] = status + session.commit() + self.log.debug("Changing status to \"{}\" <{}>".format( + next_status_name, ent_path + )) + break + + except Exception: + session.rollback() + msg = ( + "Status \"{}\" in presets wasn't found" + " on Ftrack entity type \"{}\"" + ).format(next_status_name, entity.entity_type) + self.log.warning(msg) + + def _start_timer(self, session, entity, _ftrack_api): + self.log.debug("Triggering timer start.") + + user_entity = session.query("User where username is \"{}\"".format( + os.environ["FTRACK_API_USER"] + )).first() + if not user_entity: + self.log.warning( + "Couldn't find user with username \"{}\" in Ftrack".format( + os.environ["FTRACK_API_USER"] + ) + ) + return + + source = { + "user": { + "id": user_entity["id"], + "username": user_entity["username"] + } + } + event_data = { + "actionIdentifier": "start.timer", + "selection": [{"entityId": entity["id"], "entityType": "task"}] + } + session.event_hub.publish( + _ftrack_api.event.base.Event( + topic="ftrack.action.launch", + data=event_data, + source=source + ), + on_error="ignore" + ) + self.log.debug("Timer start triggered successfully.") diff --git a/pype/modules/adobe_communicator/lib/__init__.py b/pype/modules/adobe_communicator/lib/__init__.py index 23aee81275..f918e49a60 100644 --- a/pype/modules/adobe_communicator/lib/__init__.py +++ b/pype/modules/adobe_communicator/lib/__init__.py @@ -1,8 +1,6 @@ -from .io_nonsingleton import DbConnector from .rest_api import AdobeRestApi, PUBLISH_PATHS __all__ = [ "PUBLISH_PATHS", - "DbConnector", "AdobeRestApi" ] diff --git a/pype/modules/adobe_communicator/lib/io_nonsingleton.py b/pype/modules/adobe_communicator/lib/io_nonsingleton.py deleted file mode 100644 index da37c657c6..0000000000 --- a/pype/modules/adobe_communicator/lib/io_nonsingleton.py +++ /dev/null @@ -1,460 +0,0 @@ -""" -Wrapper around interactions with the database - -Copy of io module in avalon-core. - - In this case not working as singleton with api.Session! -""" - -import os -import time -import errno -import shutil -import logging -import tempfile -import functools -import contextlib - -from avalon import schema -from avalon.vendor import requests -from avalon.io import extract_port_from_url - -# Third-party dependencies -import pymongo - - -def auto_reconnect(func): - """Handling auto reconnect in 3 retry times""" - @functools.wraps(func) - def decorated(*args, **kwargs): - object = args[0] - for retry in range(3): - try: - return func(*args, **kwargs) - except pymongo.errors.AutoReconnect: - object.log.error("Reconnecting..") - time.sleep(0.1) - else: - raise - - return decorated - - -class DbConnector(object): - - log = logging.getLogger(__name__) - - def __init__(self): - self.Session = {} - self._mongo_client = None - self._sentry_client = None - self._sentry_logging_handler = None - self._database = None - self._is_installed = False - - def __getitem__(self, key): - # gives direct access to collection withou setting `active_table` - return self._database[key] - - def __getattribute__(self, attr): - # not all methods of PyMongo database are implemented with this it is - # possible to use them too - try: - return super(DbConnector, self).__getattribute__(attr) - except AttributeError: - cur_proj = self.Session["AVALON_PROJECT"] - return self._database[cur_proj].__getattribute__(attr) - - def install(self): - """Establish a persistent connection to the database""" - if self._is_installed: - return - - logging.basicConfig() - self.Session.update(self._from_environment()) - - timeout = int(self.Session["AVALON_TIMEOUT"]) - mongo_url = self.Session["AVALON_MONGO"] - kwargs = { - "host": mongo_url, - "serverSelectionTimeoutMS": timeout - } - - port = extract_port_from_url(mongo_url) - if port is not None: - kwargs["port"] = int(port) - - self._mongo_client = pymongo.MongoClient(**kwargs) - - for retry in range(3): - try: - t1 = time.time() - self._mongo_client.server_info() - - except Exception: - self.log.error("Retrying..") - time.sleep(1) - timeout *= 1.5 - - else: - break - - else: - raise IOError( - "ERROR: Couldn't connect to %s in " - "less than %.3f ms" % (self.Session["AVALON_MONGO"], timeout)) - - self.log.info("Connected to %s, delay %.3f s" % ( - self.Session["AVALON_MONGO"], time.time() - t1)) - - self._install_sentry() - - self._database = self._mongo_client[self.Session["AVALON_DB"]] - self._is_installed = True - - def _install_sentry(self): - if "AVALON_SENTRY" not in self.Session: - return - - try: - from raven import Client - from raven.handlers.logging import SentryHandler - from raven.conf import setup_logging - except ImportError: - # Note: There was a Sentry address in this Session - return self.log.warning("Sentry disabled, raven not installed") - - client = Client(self.Session["AVALON_SENTRY"]) - - # Transmit log messages to Sentry - handler = SentryHandler(client) - handler.setLevel(logging.WARNING) - - setup_logging(handler) - - self._sentry_client = client - self._sentry_logging_handler = handler - self.log.info( - "Connected to Sentry @ %s" % self.Session["AVALON_SENTRY"] - ) - - def _from_environment(self): - Session = { - item[0]: os.getenv(item[0], item[1]) - for item in ( - # Root directory of projects on disk - ("AVALON_PROJECTS", None), - - # Name of current Project - ("AVALON_PROJECT", ""), - - # Name of current Asset - ("AVALON_ASSET", ""), - - # Name of current silo - ("AVALON_SILO", ""), - - # Name of current task - ("AVALON_TASK", None), - - # Name of current app - ("AVALON_APP", None), - - # Path to working directory - ("AVALON_WORKDIR", None), - - # Name of current Config - # TODO(marcus): Establish a suitable default config - ("AVALON_CONFIG", "no_config"), - - # Name of Avalon in graphical user interfaces - # Use this to customise the visual appearance of Avalon - # to better integrate with your surrounding pipeline - ("AVALON_LABEL", "Avalon"), - - # Used during any connections to the outside world - ("AVALON_TIMEOUT", "1000"), - - # Address to Asset Database - ("AVALON_MONGO", "mongodb://localhost:27017"), - - # Name of database used in MongoDB - ("AVALON_DB", "avalon"), - - # Address to Sentry - ("AVALON_SENTRY", None), - - # Address to Deadline Web Service - # E.g. http://192.167.0.1:8082 - ("AVALON_DEADLINE", None), - - # Enable features not necessarily stable. The user's own risk - ("AVALON_EARLY_ADOPTER", None), - - # Address of central asset repository, contains - # the following interface: - # /upload - # /download - # /manager (optional) - ("AVALON_LOCATION", "http://127.0.0.1"), - - # Boolean of whether to upload published material - # to central asset repository - ("AVALON_UPLOAD", None), - - # Generic username and password - ("AVALON_USERNAME", "avalon"), - ("AVALON_PASSWORD", "secret"), - - # Unique identifier for instances in working files - ("AVALON_INSTANCE_ID", "avalon.instance"), - ("AVALON_CONTAINER_ID", "avalon.container"), - - # Enable debugging - ("AVALON_DEBUG", None), - - ) if os.getenv(item[0], item[1]) is not None - } - - Session["schema"] = "avalon-core:session-2.0" - try: - schema.validate(Session) - except schema.ValidationError as e: - # TODO(marcus): Make this mandatory - self.log.warning(e) - - return Session - - def uninstall(self): - """Close any connection to the database""" - try: - self._mongo_client.close() - except AttributeError: - pass - - self._mongo_client = None - self._database = None - self._is_installed = False - - def active_project(self): - """Return the name of the active project""" - return self.Session["AVALON_PROJECT"] - - def activate_project(self, project_name): - self.Session["AVALON_PROJECT"] = project_name - - def projects(self): - """List available projects - - Returns: - list of project documents - - """ - - collection_names = self.collections() - for project in collection_names: - if project in ("system.indexes",): - continue - - # Each collection will have exactly one project document - document = self.find_project(project) - - if document is not None: - yield document - - def locate(self, path): - """Traverse a hierarchy from top-to-bottom - - Example: - representation = locate(["hulk", "Bruce", "modelDefault", 1, "ma"]) - - Returns: - representation (ObjectId) - - """ - - components = zip( - ("project", "asset", "subset", "version", "representation"), - path - ) - - parent = None - for type_, name in components: - latest = (type_ == "version") and name in (None, -1) - - try: - if latest: - parent = self.find_one( - filter={ - "type": type_, - "parent": parent - }, - projection={"_id": 1}, - sort=[("name", -1)] - )["_id"] - else: - parent = self.find_one( - filter={ - "type": type_, - "name": name, - "parent": parent - }, - projection={"_id": 1}, - )["_id"] - - except TypeError: - return None - - return parent - - @auto_reconnect - def collections(self): - return self._database.collection_names() - - @auto_reconnect - def find_project(self, project): - return self._database[project].find_one({"type": "project"}) - - @auto_reconnect - def insert_one(self, item): - assert isinstance(item, dict), "item must be of type " - schema.validate(item) - return self._database[self.Session["AVALON_PROJECT"]].insert_one(item) - - @auto_reconnect - def insert_many(self, items, ordered=True): - # check if all items are valid - assert isinstance(items, list), "`items` must be of type " - for item in items: - assert isinstance(item, dict), "`item` must be of type " - schema.validate(item) - - return self._database[self.Session["AVALON_PROJECT"]].insert_many( - items, - ordered=ordered) - - @auto_reconnect - def find(self, filter, projection=None, sort=None): - return self._database[self.Session["AVALON_PROJECT"]].find( - filter=filter, - projection=projection, - sort=sort - ) - - @auto_reconnect - def find_one(self, filter, projection=None, sort=None): - assert isinstance(filter, dict), "filter must be " - - return self._database[self.Session["AVALON_PROJECT"]].find_one( - filter=filter, - projection=projection, - sort=sort - ) - - @auto_reconnect - def save(self, *args, **kwargs): - return self._database[self.Session["AVALON_PROJECT"]].save( - *args, **kwargs) - - @auto_reconnect - def replace_one(self, filter, replacement): - return self._database[self.Session["AVALON_PROJECT"]].replace_one( - filter, replacement) - - @auto_reconnect - def update_many(self, filter, update): - return self._database[self.Session["AVALON_PROJECT"]].update_many( - filter, update) - - @auto_reconnect - def distinct(self, *args, **kwargs): - return self._database[self.Session["AVALON_PROJECT"]].distinct( - *args, **kwargs) - - @auto_reconnect - def drop(self, *args, **kwargs): - return self._database[self.Session["AVALON_PROJECT"]].drop( - *args, **kwargs) - - @auto_reconnect - def delete_many(self, *args, **kwargs): - return self._database[self.Session["AVALON_PROJECT"]].delete_many( - *args, **kwargs) - - def parenthood(self, document): - assert document is not None, "This is a bug" - - parents = list() - - while document.get("parent") is not None: - document = self.find_one({"_id": document["parent"]}) - - if document is None: - break - - if document.get("type") == "master_version": - _document = self.find_one({"_id": document["version_id"]}) - document["data"] = _document["data"] - - parents.append(document) - - return parents - - @contextlib.contextmanager - def tempdir(self): - tempdir = tempfile.mkdtemp() - try: - yield tempdir - finally: - shutil.rmtree(tempdir) - - def download(self, src, dst): - """Download `src` to `dst` - - Arguments: - src (str): URL to source file - dst (str): Absolute path to destination file - - Yields tuple (progress, error): - progress (int): Between 0-100 - error (Exception): Any exception raised when first making connection - - """ - - try: - response = requests.get( - src, - stream=True, - auth=requests.auth.HTTPBasicAuth( - self.Session["AVALON_USERNAME"], - self.Session["AVALON_PASSWORD"] - ) - ) - except requests.ConnectionError as e: - yield None, e - return - - with self.tempdir() as dirname: - tmp = os.path.join(dirname, os.path.basename(src)) - - with open(tmp, "wb") as f: - total_length = response.headers.get("content-length") - - if total_length is None: # no content length header - f.write(response.content) - else: - downloaded = 0 - total_length = int(total_length) - for data in response.iter_content(chunk_size=4096): - downloaded += len(data) - f.write(data) - - yield int(100.0 * downloaded / total_length), None - - try: - os.makedirs(os.path.dirname(dst)) - except OSError as e: - # An already existing destination directory is fine. - if e.errno != errno.EEXIST: - raise - - shutil.copy(tmp, dst) diff --git a/pype/modules/adobe_communicator/lib/rest_api.py b/pype/modules/adobe_communicator/lib/rest_api.py index 86739e4d80..35094d10dc 100644 --- a/pype/modules/adobe_communicator/lib/rest_api.py +++ b/pype/modules/adobe_communicator/lib/rest_api.py @@ -2,7 +2,7 @@ import os import sys import copy from pype.modules.rest_api import RestApi, route, abort, CallbackResult -from .io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB from pype.api import config, execute, Logger log = Logger().get_logger("AdobeCommunicator") @@ -14,7 +14,7 @@ PUBLISH_PATHS = [] class AdobeRestApi(RestApi): - dbcon = DbConnector() + dbcon = AvalonMongoDB() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/pype/modules/avalon_apps/rest_api.py b/pype/modules/avalon_apps/rest_api.py index 1cb9e544a7..2408e56bbc 100644 --- a/pype/modules/avalon_apps/rest_api.py +++ b/pype/modules/avalon_apps/rest_api.py @@ -4,14 +4,14 @@ import json import bson import bson.json_util from pype.modules.rest_api import RestApi, abort, CallbackResult -from pype.modules.ftrack.lib.io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB class AvalonRestApi(RestApi): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.dbcon = DbConnector() + self.dbcon = AvalonMongoDB() self.dbcon.install() @RestApi.route("/projects/", url_prefix="/avalon", methods="GET") diff --git a/pype/modules/ftrack/actions/action_delete_asset.py b/pype/modules/ftrack/actions/action_delete_asset.py index 27394770e1..7d2dac3320 100644 --- a/pype/modules/ftrack/actions/action_delete_asset.py +++ b/pype/modules/ftrack/actions/action_delete_asset.py @@ -5,7 +5,7 @@ from queue import Queue from bson.objectid import ObjectId from pype.modules.ftrack.lib import BaseAction, statics_icon -from pype.modules.ftrack.lib.io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB class DeleteAssetSubset(BaseAction): @@ -21,7 +21,7 @@ class DeleteAssetSubset(BaseAction): #: roles that are allowed to register this action role_list = ["Pypeclub", "Administrator", "Project Manager"] #: Db connection - dbcon = DbConnector() + dbcon = AvalonMongoDB() splitter = {"type": "label", "value": "---"} action_data_by_id = {} diff --git a/pype/modules/ftrack/actions/action_delete_old_versions.py b/pype/modules/ftrack/actions/action_delete_old_versions.py index 6a4c5a0cae..b55f091fdc 100644 --- a/pype/modules/ftrack/actions/action_delete_old_versions.py +++ b/pype/modules/ftrack/actions/action_delete_old_versions.py @@ -6,7 +6,7 @@ import clique from pymongo import UpdateOne from pype.modules.ftrack.lib import BaseAction, statics_icon -from pype.modules.ftrack.lib.io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB from pype.api import Anatomy import avalon.pipeline @@ -24,7 +24,7 @@ class DeleteOldVersions(BaseAction): role_list = ["Pypeclub", "Project Manager", "Administrator"] icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") - dbcon = DbConnector() + dbcon = AvalonMongoDB() inteface_title = "Choose your preferences" splitter_item = {"type": "label", "value": "---"} diff --git a/pype/modules/ftrack/actions/action_delivery.py b/pype/modules/ftrack/actions/action_delivery.py index c0d9e80286..8812ce9bc7 100644 --- a/pype/modules/ftrack/actions/action_delivery.py +++ b/pype/modules/ftrack/actions/action_delivery.py @@ -1,5 +1,6 @@ import os import copy +import json import shutil import collections @@ -9,10 +10,10 @@ from bson.objectid import ObjectId from avalon import pipeline from avalon.vendor import filelink -from pype.api import Anatomy +from pype.api import Anatomy, config from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY -from pype.modules.ftrack.lib.io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB class Delivery(BaseAction): @@ -23,7 +24,7 @@ class Delivery(BaseAction): role_list = ["Pypeclub", "Administrator", "Project manager"] icon = statics_icon("ftrack", "action_icons", "Delivery.svg") - db_con = DbConnector() + db_con = AvalonMongoDB() def discover(self, session, entities, event): for entity in entities: @@ -41,36 +42,22 @@ class Delivery(BaseAction): items = [] item_splitter = {"type": "label", "value": "---"} - # Prepare component names for processing - components = None - project = None - for entity in entities: - if project is None: - project_id = None - for ent_info in entity["link"]: - if ent_info["type"].lower() == "project": - project_id = ent_info["id"] - break + project_entity = self.get_project_from_entity(entities[0]) + project_name = project_entity["full_name"] + self.db_con.install() + self.db_con.Session["AVALON_PROJECT"] = project_name + project_doc = self.db_con.find_one({"type": "project"}) + if not project_doc: + return { + "success": False, + "message": ( + "Didn't found project \"{}\" in avalon." + ).format(project_name) + } - if project_id is None: - project = entity["asset"]["parent"]["project"] - else: - project = session.query(( - "select id, full_name from Project where id is \"{}\"" - ).format(project_id)).one() + repre_names = self._get_repre_names(entities) + self.db_con.uninstall() - _components = set( - [component["name"] for component in entity["components"]] - ) - if components is None: - components = _components - continue - - components = components.intersection(_components) - if not components: - break - - project_name = project["full_name"] items.append({ "type": "hidden", "name": "__project_name__", @@ -93,7 +80,7 @@ class Delivery(BaseAction): skipped = False # Add message if there are any common components - if not components or not new_anatomies: + if not repre_names or not new_anatomies: skipped = True items.append({ "type": "label", @@ -106,7 +93,7 @@ class Delivery(BaseAction): "value": skipped }) - if not components: + if not repre_names: if len(entities) == 1: items.append({ "type": "label", @@ -143,12 +130,12 @@ class Delivery(BaseAction): "type": "label" }) - for component in components: + for repre_name in repre_names: items.append({ "type": "boolean", "value": False, - "label": component, - "name": component + "label": repre_name, + "name": repre_name }) items.append(item_splitter) @@ -198,27 +185,233 @@ class Delivery(BaseAction): "title": title } + def _get_repre_names(self, entities): + version_ids = self._get_interest_version_ids(entities) + repre_docs = self.db_con.find({ + "type": "representation", + "parent": {"$in": version_ids} + }) + return list(sorted(repre_docs.distinct("name"))) + + def _get_interest_version_ids(self, entities): + parent_ent_by_id = {} + subset_names = set() + version_nums = set() + for entity in entities: + asset = entity["asset"] + parent = asset["parent"] + parent_ent_by_id[parent["id"]] = parent + + subset_name = asset["name"] + subset_names.add(subset_name) + + version = entity["version"] + version_nums.add(version) + + asset_docs_by_ftrack_id = self._get_asset_docs(parent_ent_by_id) + subset_docs = self._get_subset_docs( + asset_docs_by_ftrack_id, subset_names, entities + ) + version_docs = self._get_version_docs( + asset_docs_by_ftrack_id, subset_docs, version_nums, entities + ) + + return [version_doc["_id"] for version_doc in version_docs] + + def _get_version_docs( + self, asset_docs_by_ftrack_id, subset_docs, version_nums, entities + ): + subset_docs_by_id = { + subset_doc["_id"]: subset_doc + for subset_doc in subset_docs + } + version_docs = list(self.db_con.find({ + "type": "version", + "parent": {"$in": list(subset_docs_by_id.keys())}, + "name": {"$in": list(version_nums)} + })) + version_docs_by_parent_id = collections.defaultdict(dict) + for version_doc in version_docs: + subset_doc = subset_docs_by_id[version_doc["parent"]] + + asset_id = subset_doc["parent"] + subset_name = subset_doc["name"] + version = version_doc["name"] + if version_docs_by_parent_id[asset_id].get(subset_name) is None: + version_docs_by_parent_id[asset_id][subset_name] = {} + + version_docs_by_parent_id[asset_id][subset_name][version] = ( + version_doc + ) + + filtered_versions = [] + for entity in entities: + asset = entity["asset"] + + parent = asset["parent"] + asset_doc = asset_docs_by_ftrack_id[parent["id"]] + + subsets_by_name = version_docs_by_parent_id.get(asset_doc["_id"]) + if not subsets_by_name: + continue + + subset_name = asset["name"] + version_docs_by_version = subsets_by_name.get(subset_name) + if not version_docs_by_version: + continue + + version = entity["version"] + version_doc = version_docs_by_version.get(version) + if version_doc: + filtered_versions.append(version_doc) + return filtered_versions + + def _get_subset_docs( + self, asset_docs_by_ftrack_id, subset_names, entities + ): + asset_doc_ids = list() + for asset_doc in asset_docs_by_ftrack_id.values(): + asset_doc_ids.append(asset_doc["_id"]) + + subset_docs = list(self.db_con.find({ + "type": "subset", + "parent": {"$in": asset_doc_ids}, + "name": {"$in": list(subset_names)} + })) + subset_docs_by_parent_id = collections.defaultdict(dict) + for subset_doc in subset_docs: + asset_id = subset_doc["parent"] + subset_name = subset_doc["name"] + subset_docs_by_parent_id[asset_id][subset_name] = subset_doc + + filtered_subsets = [] + for entity in entities: + asset = entity["asset"] + + parent = asset["parent"] + asset_doc = asset_docs_by_ftrack_id[parent["id"]] + + subsets_by_name = subset_docs_by_parent_id.get(asset_doc["_id"]) + if not subsets_by_name: + continue + + subset_name = asset["name"] + subset_doc = subsets_by_name.get(subset_name) + if subset_doc: + filtered_subsets.append(subset_doc) + return filtered_subsets + + def _get_asset_docs(self, parent_ent_by_id): + asset_docs = list(self.db_con.find({ + "type": "asset", + "data.ftrackId": {"$in": list(parent_ent_by_id.keys())} + })) + asset_docs_by_ftrack_id = { + asset_doc["data"]["ftrackId"]: asset_doc + for asset_doc in asset_docs + } + + entities_by_mongo_id = {} + entities_by_names = {} + for ftrack_id, entity in parent_ent_by_id.items(): + if ftrack_id not in asset_docs_by_ftrack_id: + parent_mongo_id = entity["custom_attributes"].get( + CUST_ATTR_ID_KEY + ) + if parent_mongo_id: + entities_by_mongo_id[ObjectId(parent_mongo_id)] = entity + else: + entities_by_names[entity["name"]] = entity + + expressions = [] + if entities_by_mongo_id: + expression = { + "type": "asset", + "_id": {"$in": list(entities_by_mongo_id.keys())} + } + expressions.append(expression) + + if entities_by_names: + expression = { + "type": "asset", + "name": {"$in": list(entities_by_names.keys())} + } + expressions.append(expression) + + if expressions: + if len(expressions) == 1: + filter = expressions[0] + else: + filter = {"$or": expressions} + + asset_docs = self.db_con.find(filter) + for asset_doc in asset_docs: + if asset_doc["_id"] in entities_by_mongo_id: + entity = entities_by_mongo_id[asset_doc["_id"]] + asset_docs_by_ftrack_id[entity["id"]] = asset_doc + + elif asset_doc["name"] in entities_by_names: + entity = entities_by_names[asset_doc["name"]] + asset_docs_by_ftrack_id[entity["id"]] = asset_doc + + return asset_docs_by_ftrack_id + def launch(self, session, entities, event): if "values" not in event["data"]: return - self.report_items = collections.defaultdict(list) - values = event["data"]["values"] skipped = values.pop("__skipped__") if skipped: return None - component_names = [] + user_id = event["source"]["user"]["id"] + user_entity = session.query( + "User where id is {}".format(user_id) + ).one() + + job = session.create("Job", { + "user": user_entity, + "status": "running", + "data": json.dumps({ + "description": "Delivery processing." + }) + }) + session.commit() + + try: + self.db_con.install() + self.real_launch(session, entities, event) + job["status"] = "done" + + except Exception: + self.log.warning( + "Failed during processing delivery action.", + exc_info=True + ) + + finally: + if job["status"] != "done": + job["status"] = "failed" + session.commit() + self.db_con.uninstall() + + def real_launch(self, session, entities, event): + self.log.info("Delivery action just started.") + report_items = collections.defaultdict(list) + + values = event["data"]["values"] + location_path = values.pop("__location_path__") anatomy_name = values.pop("__new_anatomies__") project_name = values.pop("__project_name__") + repre_names = [] for key, value in values.items(): if value is True: - component_names.append(key) + repre_names.append(key) - if not component_names: + if not repre_names: return { "success": True, "message": "Not selected components to deliver." @@ -230,64 +423,15 @@ class Delivery(BaseAction): if not os.path.exists(location_path): os.makedirs(location_path) - self.db_con.install() self.db_con.Session["AVALON_PROJECT"] = project_name - repres_to_deliver = [] - for entity in entities: - asset = entity["asset"] - subset_name = asset["name"] - version = entity["version"] - - parent = asset["parent"] - parent_mongo_id = parent["custom_attributes"].get(CUST_ATTR_ID_KEY) - if parent_mongo_id: - parent_mongo_id = ObjectId(parent_mongo_id) - else: - asset_ent = self.db_con.find_one({ - "type": "asset", - "data.ftrackId": parent["id"] - }) - if not asset_ent: - ent_path = "/".join( - [ent["name"] for ent in parent["link"]] - ) - msg = "Not synchronized entities to avalon" - self.report_items[msg].append(ent_path) - self.log.warning("{} <{}>".format(msg, ent_path)) - continue - - parent_mongo_id = asset_ent["_id"] - - subset_ent = self.db_con.find_one({ - "type": "subset", - "parent": parent_mongo_id, - "name": subset_name - }) - - version_ent = self.db_con.find_one({ - "type": "version", - "name": version, - "parent": subset_ent["_id"] - }) - - repre_ents = self.db_con.find({ - "type": "representation", - "parent": version_ent["_id"] - }) - - repres_by_name = {} - for repre in repre_ents: - repre_name = repre["name"] - repres_by_name[repre_name] = repre - - for component in entity["components"]: - comp_name = component["name"] - if comp_name not in component_names: - continue - - repre = repres_by_name.get(comp_name) - repres_to_deliver.append(repre) + self.log.debug("Collecting representations to process.") + version_ids = self._get_interest_version_ids(entities) + repres_to_deliver = list(self.db_con.find({ + "type": "representation", + "parent": {"$in": version_ids}, + "name": {"$in": repre_names} + })) anatomy = Anatomy(project_name) @@ -304,9 +448,17 @@ class Delivery(BaseAction): for name in root_names: format_dict["root"][name] = location_path + datetime_data = config.get_datetime_data() for repre in repres_to_deliver: + source_path = repre.get("data", {}).get("path") + debug_msg = "Processing representation {}".format(repre["_id"]) + if source_path: + debug_msg += " with published path {}.".format(source_path) + self.log.debug(debug_msg) + # Get destination repre path anatomy_data = copy.deepcopy(repre["context"]) + anatomy_data.update(datetime_data) anatomy_filled = anatomy.format_all(anatomy_data) test_path = anatomy_filled["delivery"][anatomy_name] @@ -333,7 +485,7 @@ class Delivery(BaseAction): "- Invalid value DataType: \"{}\"
" ).format(str(repre["_id"]), keys) - self.report_items[msg].append(sub_msg) + report_items[msg].append(sub_msg) self.log.warning( "{} Representation: \"{}\" Filled: <{}>".format( msg, str(repre["_id"]), str(test_path) @@ -355,20 +507,19 @@ class Delivery(BaseAction): anatomy, anatomy_name, anatomy_data, - format_dict + format_dict, + report_items ) - if not frame: self.process_single_file(*args) else: self.process_sequence(*args) - self.db_con.uninstall() - - return self.report() + return self.report(report_items) def process_single_file( - self, repre_path, anatomy, anatomy_name, anatomy_data, format_dict + self, repre_path, anatomy, anatomy_name, anatomy_data, format_dict, + report_items ): anatomy_filled = anatomy.format(anatomy_data) if format_dict: @@ -384,7 +535,8 @@ class Delivery(BaseAction): self.copy_file(repre_path, delivery_path) def process_sequence( - self, repre_path, anatomy, anatomy_name, anatomy_data, format_dict + self, repre_path, anatomy, anatomy_name, anatomy_data, format_dict, + report_items ): dir_path, file_name = os.path.split(str(repre_path)) @@ -398,7 +550,7 @@ class Delivery(BaseAction): if not file_name_items: msg = "Source file was not found" - self.report_items[msg].append(repre_path) + report_items[msg].append(repre_path) self.log.warning("{} <{}>".format(msg, repre_path)) return @@ -418,7 +570,7 @@ class Delivery(BaseAction): if src_collection is None: # TODO log error! msg = "Source collection of files was not found" - self.report_items[msg].append(repre_path) + report_items[msg].append(repre_path) self.log.warning("{} <{}>".format(msg, repre_path)) return @@ -491,10 +643,10 @@ class Delivery(BaseAction): except OSError: shutil.copyfile(src_path, dst_path) - def report(self): + def report(self, report_items): items = [] title = "Delivery report" - for msg, _items in self.report_items.items(): + for msg, _items in report_items.items(): if not _items: continue diff --git a/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py b/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py index 94ca503233..36f7175768 100644 --- a/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py +++ b/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py @@ -6,7 +6,7 @@ import json from bson.objectid import ObjectId from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.api import Anatomy -from pype.modules.ftrack.lib.io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY @@ -25,7 +25,7 @@ class StoreThumbnailsToAvalon(BaseAction): icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") thumbnail_key = "AVALON_THUMBNAIL_ROOT" - db_con = DbConnector() + db_con = AvalonMongoDB() def discover(self, session, entities, event): for entity in entities: diff --git a/pype/modules/ftrack/events/event_sync_to_avalon.py b/pype/modules/ftrack/events/event_sync_to_avalon.py index efcb74a608..d0c9ea2e96 100644 --- a/pype/modules/ftrack/events/event_sync_to_avalon.py +++ b/pype/modules/ftrack/events/event_sync_to_avalon.py @@ -19,12 +19,12 @@ from pype.modules.ftrack.lib.avalon_sync import ( import ftrack_api from pype.modules.ftrack import BaseEvent -from pype.modules.ftrack.lib.io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB class SyncToAvalonEvent(BaseEvent): - dbcon = DbConnector() + dbcon = AvalonMongoDB() interest_entTypes = ["show", "task"] ignore_ent_types = ["Milestone"] @@ -717,6 +717,9 @@ class SyncToAvalonEvent(BaseEvent): if not self.ftrack_removed: return ent_infos = self.ftrack_removed + self.log.debug( + "Processing removed entities: {}".format(str(ent_infos)) + ) removable_ids = [] recreate_ents = [] removed_names = [] @@ -878,8 +881,9 @@ class SyncToAvalonEvent(BaseEvent): self.process_session.commit() found_idx = None - for idx, _entity in enumerate(self._avalon_ents): - if _entity["_id"] == avalon_entity["_id"]: + proj_doc, asset_docs = self._avalon_ents + for idx, asset_doc in enumerate(asset_docs): + if asset_doc["_id"] == avalon_entity["_id"]: found_idx = idx break @@ -894,7 +898,8 @@ class SyncToAvalonEvent(BaseEvent): new_entity_id ) # Update cached entities - self._avalon_ents[found_idx] = avalon_entity + asset_docs[found_idx] = avalon_entity + self._avalon_ents = proj_doc, asset_docs if self._avalon_ents_by_id is not None: mongo_id = avalon_entity["_id"] @@ -1258,6 +1263,10 @@ class SyncToAvalonEvent(BaseEvent): if not ent_infos: return + self.log.debug( + "Processing renamed entities: {}".format(str(ent_infos)) + ) + renamed_tasks = {} not_found = {} changeable_queue = queue.Queue() @@ -1453,6 +1462,10 @@ class SyncToAvalonEvent(BaseEvent): if not ent_infos: return + self.log.debug( + "Processing added entities: {}".format(str(ent_infos)) + ) + cust_attrs, hier_attrs = self.avalon_cust_attrs entity_type_conf_ids = {} # Skip if already exit in avalon db or tasks entities @@ -1729,6 +1742,10 @@ class SyncToAvalonEvent(BaseEvent): if not self.ftrack_moved: return + self.log.debug( + "Processing moved entities: {}".format(str(self.ftrack_moved)) + ) + ftrack_moved = {k: v for k, v in sorted( self.ftrack_moved.items(), key=(lambda line: len( @@ -1859,6 +1876,10 @@ class SyncToAvalonEvent(BaseEvent): if not self.ftrack_updated: return + self.log.debug( + "Processing updated entities: {}".format(str(self.ftrack_updated)) + ) + ent_infos = self.ftrack_updated ftrack_mongo_mapping = {} not_found_ids = [] diff --git a/pype/modules/ftrack/events/event_user_assigment.py b/pype/modules/ftrack/events/event_user_assigment.py index d1b3439c8f..19a67b745f 100644 --- a/pype/modules/ftrack/events/event_user_assigment.py +++ b/pype/modules/ftrack/events/event_user_assigment.py @@ -4,7 +4,7 @@ import subprocess from pype.modules.ftrack import BaseEvent from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY -from pype.modules.ftrack.lib.io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB from bson.objectid import ObjectId @@ -37,7 +37,7 @@ class UserAssigmentEvent(BaseEvent): 3) path to publish files of task user was (de)assigned to """ - db_con = DbConnector() + db_con = AvalonMongoDB() def error(self, *err): for e in err: diff --git a/pype/modules/ftrack/ftrack_server/lib.py b/pype/modules/ftrack/ftrack_server/lib.py index acf31ab437..ee6b1216dc 100644 --- a/pype/modules/ftrack/ftrack_server/lib.py +++ b/pype/modules/ftrack/ftrack_server/lib.py @@ -205,10 +205,16 @@ class ProcessEventHub(SocketBaseEventHub): else: try: self._handle(event) + + mongo_id = event["data"].get("_event_mongo_id") + if mongo_id is None: + continue + self.dbcon.update_one( - {"id": event["id"]}, + {"_id": mongo_id}, {"$set": {"pype_data.is_processed": True}} ) + except pymongo.errors.AutoReconnect: self.pypelog.error(( "Mongo server \"{}\" is not responding, exiting." @@ -244,6 +250,7 @@ class ProcessEventHub(SocketBaseEventHub): } try: event = ftrack_api.event.base.Event(**new_event_data) + event["data"]["_event_mongo_id"] = event_data["_id"] except Exception: self.logger.exception(L( 'Failed to convert payload into event: {0}', diff --git a/pype/modules/ftrack/ftrack_server/sub_event_status.py b/pype/modules/ftrack/ftrack_server/sub_event_status.py index c2e7b7477f..00a6687de3 100644 --- a/pype/modules/ftrack/ftrack_server/sub_event_status.py +++ b/pype/modules/ftrack/ftrack_server/sub_event_status.py @@ -12,7 +12,7 @@ from pype.modules.ftrack.ftrack_server.lib import ( SocketSession, StatusEventHub, TOPIC_STATUS_SERVER, TOPIC_STATUS_SERVER_RESULT ) -from pype.api import Logger, config +from pype.api import Logger log = Logger().get_logger("Event storer") action_identifier = ( @@ -23,17 +23,7 @@ action_data = { "label": "Pype Admin", "variant": "- Event server Status ({})".format(host_ip), "description": "Get Infromation about event server", - "actionIdentifier": action_identifier, - "icon": "{}/ftrack/action_icons/PypeAdmin.svg".format( - os.environ.get( - "PYPE_STATICS_SERVER", - "http://localhost:{}".format( - config.get_presets().get("services", {}).get( - "rest_api", {} - ).get("default_port", 8021) - ) - ) - ) + "actionIdentifier": action_identifier } diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py index 4bab1676d4..03124ab10d 100644 --- a/pype/modules/ftrack/lib/avalon_sync.py +++ b/pype/modules/ftrack/lib/avalon_sync.py @@ -5,7 +5,7 @@ import json import collections import copy -from pype.modules.ftrack.lib.io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB import avalon import avalon.api @@ -240,7 +240,7 @@ def get_hierarchical_attributes(session, entity, attr_names, attr_defaults={}): class SyncEntitiesFactory: - dbcon = DbConnector() + dbcon = AvalonMongoDB() project_query = ( "select full_name, name, custom_attributes" @@ -2144,6 +2144,7 @@ class SyncEntitiesFactory: "name": _name, "parent": parent_entity }) + self.session.commit() final_entity = {} for k, v in av_entity.items(): diff --git a/pype/modules/ftrack/lib/io_nonsingleton.py b/pype/modules/ftrack/lib/io_nonsingleton.py deleted file mode 100644 index da37c657c6..0000000000 --- a/pype/modules/ftrack/lib/io_nonsingleton.py +++ /dev/null @@ -1,460 +0,0 @@ -""" -Wrapper around interactions with the database - -Copy of io module in avalon-core. - - In this case not working as singleton with api.Session! -""" - -import os -import time -import errno -import shutil -import logging -import tempfile -import functools -import contextlib - -from avalon import schema -from avalon.vendor import requests -from avalon.io import extract_port_from_url - -# Third-party dependencies -import pymongo - - -def auto_reconnect(func): - """Handling auto reconnect in 3 retry times""" - @functools.wraps(func) - def decorated(*args, **kwargs): - object = args[0] - for retry in range(3): - try: - return func(*args, **kwargs) - except pymongo.errors.AutoReconnect: - object.log.error("Reconnecting..") - time.sleep(0.1) - else: - raise - - return decorated - - -class DbConnector(object): - - log = logging.getLogger(__name__) - - def __init__(self): - self.Session = {} - self._mongo_client = None - self._sentry_client = None - self._sentry_logging_handler = None - self._database = None - self._is_installed = False - - def __getitem__(self, key): - # gives direct access to collection withou setting `active_table` - return self._database[key] - - def __getattribute__(self, attr): - # not all methods of PyMongo database are implemented with this it is - # possible to use them too - try: - return super(DbConnector, self).__getattribute__(attr) - except AttributeError: - cur_proj = self.Session["AVALON_PROJECT"] - return self._database[cur_proj].__getattribute__(attr) - - def install(self): - """Establish a persistent connection to the database""" - if self._is_installed: - return - - logging.basicConfig() - self.Session.update(self._from_environment()) - - timeout = int(self.Session["AVALON_TIMEOUT"]) - mongo_url = self.Session["AVALON_MONGO"] - kwargs = { - "host": mongo_url, - "serverSelectionTimeoutMS": timeout - } - - port = extract_port_from_url(mongo_url) - if port is not None: - kwargs["port"] = int(port) - - self._mongo_client = pymongo.MongoClient(**kwargs) - - for retry in range(3): - try: - t1 = time.time() - self._mongo_client.server_info() - - except Exception: - self.log.error("Retrying..") - time.sleep(1) - timeout *= 1.5 - - else: - break - - else: - raise IOError( - "ERROR: Couldn't connect to %s in " - "less than %.3f ms" % (self.Session["AVALON_MONGO"], timeout)) - - self.log.info("Connected to %s, delay %.3f s" % ( - self.Session["AVALON_MONGO"], time.time() - t1)) - - self._install_sentry() - - self._database = self._mongo_client[self.Session["AVALON_DB"]] - self._is_installed = True - - def _install_sentry(self): - if "AVALON_SENTRY" not in self.Session: - return - - try: - from raven import Client - from raven.handlers.logging import SentryHandler - from raven.conf import setup_logging - except ImportError: - # Note: There was a Sentry address in this Session - return self.log.warning("Sentry disabled, raven not installed") - - client = Client(self.Session["AVALON_SENTRY"]) - - # Transmit log messages to Sentry - handler = SentryHandler(client) - handler.setLevel(logging.WARNING) - - setup_logging(handler) - - self._sentry_client = client - self._sentry_logging_handler = handler - self.log.info( - "Connected to Sentry @ %s" % self.Session["AVALON_SENTRY"] - ) - - def _from_environment(self): - Session = { - item[0]: os.getenv(item[0], item[1]) - for item in ( - # Root directory of projects on disk - ("AVALON_PROJECTS", None), - - # Name of current Project - ("AVALON_PROJECT", ""), - - # Name of current Asset - ("AVALON_ASSET", ""), - - # Name of current silo - ("AVALON_SILO", ""), - - # Name of current task - ("AVALON_TASK", None), - - # Name of current app - ("AVALON_APP", None), - - # Path to working directory - ("AVALON_WORKDIR", None), - - # Name of current Config - # TODO(marcus): Establish a suitable default config - ("AVALON_CONFIG", "no_config"), - - # Name of Avalon in graphical user interfaces - # Use this to customise the visual appearance of Avalon - # to better integrate with your surrounding pipeline - ("AVALON_LABEL", "Avalon"), - - # Used during any connections to the outside world - ("AVALON_TIMEOUT", "1000"), - - # Address to Asset Database - ("AVALON_MONGO", "mongodb://localhost:27017"), - - # Name of database used in MongoDB - ("AVALON_DB", "avalon"), - - # Address to Sentry - ("AVALON_SENTRY", None), - - # Address to Deadline Web Service - # E.g. http://192.167.0.1:8082 - ("AVALON_DEADLINE", None), - - # Enable features not necessarily stable. The user's own risk - ("AVALON_EARLY_ADOPTER", None), - - # Address of central asset repository, contains - # the following interface: - # /upload - # /download - # /manager (optional) - ("AVALON_LOCATION", "http://127.0.0.1"), - - # Boolean of whether to upload published material - # to central asset repository - ("AVALON_UPLOAD", None), - - # Generic username and password - ("AVALON_USERNAME", "avalon"), - ("AVALON_PASSWORD", "secret"), - - # Unique identifier for instances in working files - ("AVALON_INSTANCE_ID", "avalon.instance"), - ("AVALON_CONTAINER_ID", "avalon.container"), - - # Enable debugging - ("AVALON_DEBUG", None), - - ) if os.getenv(item[0], item[1]) is not None - } - - Session["schema"] = "avalon-core:session-2.0" - try: - schema.validate(Session) - except schema.ValidationError as e: - # TODO(marcus): Make this mandatory - self.log.warning(e) - - return Session - - def uninstall(self): - """Close any connection to the database""" - try: - self._mongo_client.close() - except AttributeError: - pass - - self._mongo_client = None - self._database = None - self._is_installed = False - - def active_project(self): - """Return the name of the active project""" - return self.Session["AVALON_PROJECT"] - - def activate_project(self, project_name): - self.Session["AVALON_PROJECT"] = project_name - - def projects(self): - """List available projects - - Returns: - list of project documents - - """ - - collection_names = self.collections() - for project in collection_names: - if project in ("system.indexes",): - continue - - # Each collection will have exactly one project document - document = self.find_project(project) - - if document is not None: - yield document - - def locate(self, path): - """Traverse a hierarchy from top-to-bottom - - Example: - representation = locate(["hulk", "Bruce", "modelDefault", 1, "ma"]) - - Returns: - representation (ObjectId) - - """ - - components = zip( - ("project", "asset", "subset", "version", "representation"), - path - ) - - parent = None - for type_, name in components: - latest = (type_ == "version") and name in (None, -1) - - try: - if latest: - parent = self.find_one( - filter={ - "type": type_, - "parent": parent - }, - projection={"_id": 1}, - sort=[("name", -1)] - )["_id"] - else: - parent = self.find_one( - filter={ - "type": type_, - "name": name, - "parent": parent - }, - projection={"_id": 1}, - )["_id"] - - except TypeError: - return None - - return parent - - @auto_reconnect - def collections(self): - return self._database.collection_names() - - @auto_reconnect - def find_project(self, project): - return self._database[project].find_one({"type": "project"}) - - @auto_reconnect - def insert_one(self, item): - assert isinstance(item, dict), "item must be of type " - schema.validate(item) - return self._database[self.Session["AVALON_PROJECT"]].insert_one(item) - - @auto_reconnect - def insert_many(self, items, ordered=True): - # check if all items are valid - assert isinstance(items, list), "`items` must be of type " - for item in items: - assert isinstance(item, dict), "`item` must be of type " - schema.validate(item) - - return self._database[self.Session["AVALON_PROJECT"]].insert_many( - items, - ordered=ordered) - - @auto_reconnect - def find(self, filter, projection=None, sort=None): - return self._database[self.Session["AVALON_PROJECT"]].find( - filter=filter, - projection=projection, - sort=sort - ) - - @auto_reconnect - def find_one(self, filter, projection=None, sort=None): - assert isinstance(filter, dict), "filter must be " - - return self._database[self.Session["AVALON_PROJECT"]].find_one( - filter=filter, - projection=projection, - sort=sort - ) - - @auto_reconnect - def save(self, *args, **kwargs): - return self._database[self.Session["AVALON_PROJECT"]].save( - *args, **kwargs) - - @auto_reconnect - def replace_one(self, filter, replacement): - return self._database[self.Session["AVALON_PROJECT"]].replace_one( - filter, replacement) - - @auto_reconnect - def update_many(self, filter, update): - return self._database[self.Session["AVALON_PROJECT"]].update_many( - filter, update) - - @auto_reconnect - def distinct(self, *args, **kwargs): - return self._database[self.Session["AVALON_PROJECT"]].distinct( - *args, **kwargs) - - @auto_reconnect - def drop(self, *args, **kwargs): - return self._database[self.Session["AVALON_PROJECT"]].drop( - *args, **kwargs) - - @auto_reconnect - def delete_many(self, *args, **kwargs): - return self._database[self.Session["AVALON_PROJECT"]].delete_many( - *args, **kwargs) - - def parenthood(self, document): - assert document is not None, "This is a bug" - - parents = list() - - while document.get("parent") is not None: - document = self.find_one({"_id": document["parent"]}) - - if document is None: - break - - if document.get("type") == "master_version": - _document = self.find_one({"_id": document["version_id"]}) - document["data"] = _document["data"] - - parents.append(document) - - return parents - - @contextlib.contextmanager - def tempdir(self): - tempdir = tempfile.mkdtemp() - try: - yield tempdir - finally: - shutil.rmtree(tempdir) - - def download(self, src, dst): - """Download `src` to `dst` - - Arguments: - src (str): URL to source file - dst (str): Absolute path to destination file - - Yields tuple (progress, error): - progress (int): Between 0-100 - error (Exception): Any exception raised when first making connection - - """ - - try: - response = requests.get( - src, - stream=True, - auth=requests.auth.HTTPBasicAuth( - self.Session["AVALON_USERNAME"], - self.Session["AVALON_PASSWORD"] - ) - ) - except requests.ConnectionError as e: - yield None, e - return - - with self.tempdir() as dirname: - tmp = os.path.join(dirname, os.path.basename(src)) - - with open(tmp, "wb") as f: - total_length = response.headers.get("content-length") - - if total_length is None: # no content length header - f.write(response.content) - else: - downloaded = 0 - total_length = int(total_length) - for data in response.iter_content(chunk_size=4096): - downloaded += len(data) - f.write(data) - - yield int(100.0 * downloaded / total_length), None - - try: - os.makedirs(os.path.dirname(dst)) - except OSError as e: - # An already existing destination directory is fine. - if e.errno != errno.EEXIST: - raise - - shutil.copy(tmp, dst) diff --git a/pype/modules/rest_api/rest_api.py b/pype/modules/rest_api/rest_api.py index cc98b56a3b..3e0c646560 100644 --- a/pype/modules/rest_api/rest_api.py +++ b/pype/modules/rest_api/rest_api.py @@ -1,6 +1,6 @@ import os import socket -from Qt import QtCore +import threading from socketserver import ThreadingMixIn from http.server import HTTPServer @@ -155,14 +155,15 @@ class RestApiServer: def is_running(self): return self.rest_api_thread.is_running + def tray_exit(self): + self.stop() + def stop(self): - self.rest_api_thread.is_running = False - - def thread_stopped(self): - self._is_running = False + self.rest_api_thread.stop() + self.rest_api_thread.join() -class RestApiThread(QtCore.QThread): +class RestApiThread(threading.Thread): """ Listener for REST requests. It is possible to register callbacks for url paths. @@ -174,6 +175,12 @@ class RestApiThread(QtCore.QThread): self.is_running = False self.module = module self.port = port + self.httpd = None + + def stop(self): + self.is_running = False + if self.httpd: + self.httpd.server_close() def run(self): self.is_running = True @@ -185,12 +192,14 @@ class RestApiThread(QtCore.QThread): ) with ThreadingSimpleServer(("", self.port), Handler) as httpd: + self.httpd = httpd while self.is_running: httpd.handle_request() + except Exception: log.warning( "Rest Api Server service has failed", exc_info=True ) + self.httpd = None self.is_running = False - self.module.thread_stopped() diff --git a/pype/modules/websocket_server/__init__.py b/pype/modules/websocket_server/__init__.py new file mode 100644 index 0000000000..eb5a0d9f27 --- /dev/null +++ b/pype/modules/websocket_server/__init__.py @@ -0,0 +1,5 @@ +from .websocket_server import WebSocketServer + + +def tray_init(tray_widget, main_widget): + return WebSocketServer() diff --git a/pype/modules/websocket_server/hosts/__init__.py b/pype/modules/websocket_server/hosts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pype/modules/websocket_server/hosts/external_app_1.py b/pype/modules/websocket_server/hosts/external_app_1.py new file mode 100644 index 0000000000..9352787175 --- /dev/null +++ b/pype/modules/websocket_server/hosts/external_app_1.py @@ -0,0 +1,47 @@ +import asyncio + +from pype.api import Logger +from wsrpc_aiohttp import WebSocketRoute + +log = Logger().get_logger("WebsocketServer") + + +class ExternalApp1(WebSocketRoute): + """ + One route, mimicking external application (like Harmony, etc). + All functions could be called from client. + 'do_notify' function calls function on the client - mimicking + notification after long running job on the server or similar + """ + + def init(self, **kwargs): + # Python __init__ must be return "self". + # This method might return anything. + log.debug("someone called ExternalApp1 route") + return kwargs + + async def server_function_one(self): + log.info('In function one') + + async def server_function_two(self): + log.info('In function two') + return 'function two' + + async def server_function_three(self): + log.info('In function three') + asyncio.ensure_future(self.do_notify()) + return '{"message":"function tree"}' + + async def server_function_four(self, *args, **kwargs): + log.info('In function four args {} kwargs {}'.format(args, kwargs)) + ret = dict(**kwargs) + ret["message"] = "function four received arguments" + return str(ret) + + # This method calls function on the client side + async def do_notify(self): + import time + time.sleep(5) + log.info('Calling function on server after delay') + awesome = 'Somebody server_function_three method!' + await self.socket.call('notify', result=awesome) diff --git a/pype/modules/websocket_server/hosts/photoshop.py b/pype/modules/websocket_server/hosts/photoshop.py new file mode 100644 index 0000000000..cdfb9413a0 --- /dev/null +++ b/pype/modules/websocket_server/hosts/photoshop.py @@ -0,0 +1,64 @@ +from pype.api import Logger +from wsrpc_aiohttp import WebSocketRoute +import functools + +import avalon.photoshop as photoshop + +log = Logger().get_logger("WebsocketServer") + + +class Photoshop(WebSocketRoute): + """ + One route, mimicking external application (like Harmony, etc). + All functions could be called from client. + 'do_notify' function calls function on the client - mimicking + notification after long running job on the server or similar + """ + instance = None + + def init(self, **kwargs): + # Python __init__ must be return "self". + # This method might return anything. + log.debug("someone called Photoshop route") + self.instance = self + return kwargs + + # server functions + async def ping(self): + log.debug("someone called Photoshop route ping") + + # This method calls function on the client side + # client functions + + async def read(self): + log.debug("photoshop.read client calls server server calls " + "Photo client") + return await self.socket.call('Photoshop.read') + + # panel routes for tools + async def creator_route(self): + self._tool_route("creator") + + async def workfiles_route(self): + self._tool_route("workfiles") + + async def loader_route(self): + self._tool_route("loader") + + async def publish_route(self): + self._tool_route("publish") + + async def sceneinventory_route(self): + self._tool_route("sceneinventory") + + async def projectmanager_route(self): + self._tool_route("projectmanager") + + def _tool_route(self, tool_name): + """The address accessed when clicking on the buttons.""" + partial_method = functools.partial(photoshop.show, tool_name) + + photoshop.execute_in_main_thread(partial_method) + + # Required return statement. + return "nothing" diff --git a/pype/modules/websocket_server/stubs/photoshop_server_stub.py b/pype/modules/websocket_server/stubs/photoshop_server_stub.py new file mode 100644 index 0000000000..da69127799 --- /dev/null +++ b/pype/modules/websocket_server/stubs/photoshop_server_stub.py @@ -0,0 +1,283 @@ +from pype.modules.websocket_server import WebSocketServer +""" + Stub handling connection from server to client. + Used anywhere solution is calling client methods. +""" +import json +from collections import namedtuple + + +class PhotoshopServerStub(): + """ + Stub for calling function on client (Photoshop js) side. + Expects that client is already connected (started when avalon menu + is opened). + 'self.websocketserver.call' is used as async wrapper + """ + + def __init__(self): + self.websocketserver = WebSocketServer.get_instance() + self.client = self.websocketserver.get_client() + + def open(self, path): + """ + Open file located at 'path' (local). + :param path: file path locally + :return: None + """ + self.websocketserver.call(self.client.call + ('Photoshop.open', path=path) + ) + + def read(self, layer, layers_meta=None): + """ + Parses layer metadata from Headline field of active document + :param layer: Layer("id": XXX, "name":'YYY') + :param data: json representation for single layer + :param all_layers: - for performance, could be + injected for usage in loop, if not, single call will be + triggered + :param layers_meta: json representation from Headline + (for performance - provide only if imprint is in + loop - value should be same) + :return: None + """ + if not layers_meta: + layers_meta = self.get_layers_metadata() + # json.dumps writes integer values in a dictionary to string, so + # anticipating it here. + if str(layer.id) in layers_meta and layers_meta[str(layer.id)]: + layers_meta[str(layer.id)].update(data) + else: + layers_meta[str(layer.id)] = data + + # Ensure only valid ids are stored. + if not all_layers: + all_layers = self.get_layers() + layer_ids = [layer.id for layer in all_layers] + cleaned_data = {} + + for id in layers_meta: + if int(id) in layer_ids: + cleaned_data[id] = layers_meta[id] + + payload = json.dumps(cleaned_data, indent=4) + + self.websocketserver.call(self.client.call + ('Photoshop.imprint', payload=payload) + ) + + def get_layers(self): + """ + Returns JSON document with all(?) layers in active document. + + :return: + Format of tuple: { 'id':'123', + 'name': 'My Layer 1', + 'type': 'GUIDE'|'FG'|'BG'|'OBJ' + 'visible': 'true'|'false' + """ + res = self.websocketserver.call(self.client.call + ('Photoshop.get_layers')) + + return self._to_records(res) + + def get_layers_in_layers(self, layers): + """ + Return all layers that belong to layers (might be groups). + :param layers: + :return: + """ + all_layers = self.get_layers() + ret = [] + parent_ids = set([lay.id for lay in layers]) + + for layer in all_layers: + parents = set(layer.parents) + if len(parent_ids & parents) > 0: + ret.append(layer) + if layer.id in parent_ids: + ret.append(layer) + + return ret + + def create_group(self, name): + """ + Create new group (eg. LayerSet) + :return: + """ + ret = self.websocketserver.call(self.client.call + ('Photoshop.create_group', + name=name)) + # create group on PS is asynchronous, returns only id + layer = {"id": ret, "name": name, "group": True} + return namedtuple('Layer', layer.keys())(*layer.values()) + + def group_selected_layers(self, name): + """ + Group selected layers into new LayerSet (eg. group) + :return: + """ + res = self.websocketserver.call(self.client.call + ('Photoshop.group_selected_layers', + name=name) + ) + return self._to_records(res) + + def get_selected_layers(self): + """ + Get a list of actually selected layers + :return: + """ + res = self.websocketserver.call(self.client.call + ('Photoshop.get_selected_layers')) + return self._to_records(res) + + def select_layers(self, layers): + """ + Selecte specified layers in Photoshop + :param layers: + :return: None + """ + layer_ids = [layer.id for layer in layers] + + self.websocketserver.call(self.client.call + ('Photoshop.get_layers', + layers=layer_ids) + ) + + def get_active_document_full_name(self): + """ + Returns full name with path of active document via ws call + :return: full path with name + """ + res = self.websocketserver.call( + self.client.call('Photoshop.get_active_document_full_name')) + + return res + + def get_active_document_name(self): + """ + Returns just a name of active document via ws call + :return: file name + """ + res = self.websocketserver.call(self.client.call + ('Photoshop.get_active_document_name')) + + return res + + def is_saved(self): + """ + Returns true if no changes in active document + :return: + """ + return self.websocketserver.call(self.client.call + ('Photoshop.is_saved')) + + def save(self): + """ + Saves active document + :return: None + """ + self.websocketserver.call(self.client.call + ('Photoshop.save')) + + def saveAs(self, image_path, ext, as_copy): + """ + Saves active document to psd (copy) or png or jpg + :param image_path: full local path + :param ext: + :param as_copy: + :return: None + """ + self.websocketserver.call(self.client.call + ('Photoshop.saveAs', + image_path=image_path, + ext=ext, + as_copy=as_copy)) + + def set_visible(self, layer_id, visibility): + """ + Set layer with 'layer_id' to 'visibility' + :param layer_id: + :param visibility: + :return: None + """ + self.websocketserver.call(self.client.call + ('Photoshop.set_visible', + layer_id=layer_id, + visibility=visibility)) + + def get_layers_metadata(self): + """ + Reads layers metadata from Headline from active document in PS. + (Headline accessible by File > File Info) + :return: - json documents + """ + layers_data = {} + res = self.websocketserver.call(self.client.call('Photoshop.read')) + try: + layers_data = json.loads(res) + except json.decoder.JSONDecodeError: + pass + return layers_data + + def import_smart_object(self, path): + """ + Import the file at `path` as a smart object to active document. + + Args: + path (str): File path to import. + """ + res = self.websocketserver.call(self.client.call + ('Photoshop.import_smart_object', + path=path)) + + return self._to_records(res).pop() + + def replace_smart_object(self, layer, path): + """ + Replace the smart object `layer` with file at `path` + + Args: + layer (namedTuple): Layer("id":XX, "name":"YY"..). + path (str): File to import. + """ + self.websocketserver.call(self.client.call + ('Photoshop.replace_smart_object', + layer=layer, + path=path)) + + def close(self): + self.client.close() + + def _to_records(self, res): + """ + Converts string json representation into list of named tuples for + dot notation access to work. + :return: + :param res: - json representation + """ + try: + layers_data = json.loads(res) + except json.decoder.JSONDecodeError: + raise ValueError("Received broken JSON {}".format(res)) + ret = [] + # convert to namedtuple to use dot donation + if isinstance(layers_data, dict): # TODO refactore + layers_data = [layers_data] + for d in layers_data: + ret.append(namedtuple('Layer', d.keys())(*d.values())) + return ret diff --git a/pype/modules/websocket_server/test_client/wsrpc_client.html b/pype/modules/websocket_server/test_client/wsrpc_client.html new file mode 100644 index 0000000000..9c3f469aca --- /dev/null +++ b/pype/modules/websocket_server/test_client/wsrpc_client.html @@ -0,0 +1,179 @@ + + + + + Title + + + + + + + + + + + + + +
+
Test of wsrpc javascript client
+ +
+ +
+
+
+
+

No return value

+
+
+
    +
  • Calls server_function_one
  • +
  • Function only logs on server
  • +
  • No return value
  • +
  •  
  • +
  •  
  • +
  •  
  • +
+ +
+
+
+
+

Return value

+
+
+
    +
  • Calls server_function_two
  • +
  • Function logs on server
  • +
  • Returns simple text value
  • +
  •  
  • +
  •  
  • +
  •  
  • +
+ +
+
+
+
+

Notify

+
+
+
    +
  • Calls server_function_three
  • +
  • Function logs on server
  • +
  • Returns json payload
  • +
  • Server then calls function ON the client after delay
  • +
  •  
  • +
+ +
+
+
+
+

Send value

+
+
+
    +
  • Calls server_function_four
  • +
  • Function logs on server
  • +
  • Returns modified sent values
  • +
  •  
  • +
  •  
  • +
  •  
  • +
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/pype/modules/websocket_server/test_client/wsrpc_client.py b/pype/modules/websocket_server/test_client/wsrpc_client.py new file mode 100644 index 0000000000..ef861513ae --- /dev/null +++ b/pype/modules/websocket_server/test_client/wsrpc_client.py @@ -0,0 +1,34 @@ +import asyncio + +from wsrpc_aiohttp import WSRPCClient + +""" + Simple testing Python client for wsrpc_aiohttp + Calls sequentially multiple methods on server +""" + +loop = asyncio.get_event_loop() + + +async def main(): + print("main") + client = WSRPCClient("ws://127.0.0.1:8099/ws/", + loop=asyncio.get_event_loop()) + + client.add_route('notify', notify) + await client.connect() + print("connected") + print(await client.proxy.ExternalApp1.server_function_one()) + print(await client.proxy.ExternalApp1.server_function_two()) + print(await client.proxy.ExternalApp1.server_function_three()) + print(await client.proxy.ExternalApp1.server_function_four(foo="one")) + await client.close() + + +def notify(socket, *args, **kwargs): + print("called from server") + + +if __name__ == "__main__": + # loop.run_until_complete(main()) + asyncio.run(main()) diff --git a/pype/modules/websocket_server/websocket_server.py b/pype/modules/websocket_server/websocket_server.py new file mode 100644 index 0000000000..1152c65e00 --- /dev/null +++ b/pype/modules/websocket_server/websocket_server.py @@ -0,0 +1,221 @@ +from pype.api import Logger + +import threading +from aiohttp import web +import asyncio +from wsrpc_aiohttp import STATIC_DIR, WebSocketAsync + +import os +import sys +import pyclbr +import importlib +import urllib + +log = Logger().get_logger("WebsocketServer") + + +class WebSocketServer(): + """ + Basic POC implementation of asychronic websocket RPC server. + Uses class in external_app_1.py to mimic implementation for single + external application. + 'test_client' folder contains two test implementations of client + """ + _instance = None + + def __init__(self): + self.qaction = None + self.failed_icon = None + self._is_running = False + WebSocketServer._instance = self + self.client = None + self.handlers = {} + + websocket_url = os.getenv("WEBSOCKET_URL") + if websocket_url: + parsed = urllib.parse.urlparse(websocket_url) + port = parsed.port + if not port: + port = 8099 # fallback + + self.app = web.Application() + + self.app.router.add_route("*", "/ws/", WebSocketAsync) + self.app.router.add_static("/js", STATIC_DIR) + self.app.router.add_static("/", ".") + + # add route with multiple methods for single "external app" + directories_with_routes = ['hosts'] + self.add_routes_for_directories(directories_with_routes) + + self.websocket_thread = WebsocketServerThread(self, port) + + def add_routes_for_directories(self, directories_with_routes): + """ Loops through selected directories to find all modules and + in them all classes implementing 'WebSocketRoute' that could be + used as route. + All methods in these classes are registered automatically. + """ + for dir_name in directories_with_routes: + dir_name = os.path.join(os.path.dirname(__file__), dir_name) + for file_name in os.listdir(dir_name): + if '.py' in file_name and '__' not in file_name: + self.add_routes_for_module(file_name, dir_name) + + def add_routes_for_module(self, file_name, dir_name): + """ Auto routes for all classes implementing 'WebSocketRoute' + in 'file_name' in 'dir_name' + """ + module_name = file_name.replace('.py', '') + module_info = pyclbr.readmodule(module_name, [dir_name]) + + for class_name, cls_object in module_info.items(): + sys.path.append(dir_name) + if 'WebSocketRoute' in cls_object.super: + log.debug('Adding route for {}'.format(class_name)) + module = importlib.import_module(module_name) + cls = getattr(module, class_name) + WebSocketAsync.add_route(class_name, cls) + sys.path.pop() + + def call(self, func): + log.debug("websocket.call {}".format(func)) + future = asyncio.run_coroutine_threadsafe(func, + self.websocket_thread.loop) + result = future.result() + return result + + def get_client(self): + """ + Return first connected client to WebSocket + TODO implement selection by Route + :return: client + """ + clients = WebSocketAsync.get_clients() + client = None + if len(clients) > 0: + key = list(clients.keys())[0] + client = clients.get(key) + + return client + + @staticmethod + def get_instance(): + if WebSocketServer._instance is None: + WebSocketServer() + return WebSocketServer._instance + + def tray_start(self): + self.websocket_thread.start() + + def tray_exit(self): + self.stop() + + def stop_websocket_server(self): + + self.stop() + + @property + def is_running(self): + return self.websocket_thread.is_running + + def stop(self): + if not self.is_running: + return + try: + log.debug("Stopping websocket server") + self.websocket_thread.is_running = False + self.websocket_thread.stop() + except Exception: + log.warning( + "Error has happened during Killing websocket server", + exc_info=True + ) + + def thread_stopped(self): + self._is_running = False + + +class WebsocketServerThread(threading.Thread): + """ Listener for websocket rpc requests. + + It would be probably better to "attach" this to main thread (as for + example Harmony needs to run something on main thread), but currently + it creates separate thread and separate asyncio event loop + """ + def __init__(self, module, port): + super(WebsocketServerThread, self).__init__() + self.is_running = False + self.port = port + self.module = module + self.loop = None + self.runner = None + self.site = None + self.tasks = [] + + def run(self): + self.is_running = True + + try: + log.info("Starting websocket server") + self.loop = asyncio.new_event_loop() # create new loop for thread + asyncio.set_event_loop(self.loop) + + self.loop.run_until_complete(self.start_server()) + + log.debug( + "Running Websocket server on URL:" + " \"ws://localhost:{}\"".format(self.port) + ) + + asyncio.ensure_future(self.check_shutdown(), loop=self.loop) + self.loop.run_forever() + except Exception: + log.warning( + "Websocket Server service has failed", exc_info=True + ) + finally: + self.loop.close() # optional + + self.is_running = False + self.module.thread_stopped() + log.info("Websocket server stopped") + + async def start_server(self): + """ Starts runner and TCPsite """ + self.runner = web.AppRunner(self.module.app) + await self.runner.setup() + self.site = web.TCPSite(self.runner, 'localhost', self.port) + await self.site.start() + + def stop(self): + """Sets is_running flag to false, 'check_shutdown' shuts server down""" + self.is_running = False + + async def check_shutdown(self): + """ Future that is running and checks if server should be running + periodically. + """ + while self.is_running: + while self.tasks: + task = self.tasks.pop(0) + log.debug("waiting for task {}".format(task)) + await task + log.debug("returned value {}".format(task.result)) + + await asyncio.sleep(0.5) + + log.debug("Starting shutdown") + await self.site.stop() + log.debug("Site stopped") + await self.runner.cleanup() + log.debug("Runner stopped") + tasks = [task for task in asyncio.all_tasks() if + task is not asyncio.current_task()] + list(map(lambda task: task.cancel(), tasks)) # cancel all the tasks + results = await asyncio.gather(*tasks, return_exceptions=True) + log.debug(f'Finished awaiting cancelled tasks, results: {results}...') + await self.loop.shutdown_asyncgens() + # to really make sure everything else has time to stop + await asyncio.sleep(0.07) + self.loop.stop() diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_api.py b/pype/plugins/ftrack/publish/integrate_ftrack_api.py index 0c4c6d49b5..2c8e06a099 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_api.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_api.py @@ -97,6 +97,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): except Exception: tp, value, tb = sys.exc_info() session.rollback() + session._configure_locations() six.reraise(tp, value, tb) def process(self, instance): @@ -178,6 +179,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): except Exception: tp, value, tb = sys.exc_info() session.rollback() + session._configure_locations() six.reraise(tp, value, tb) # Adding metadata @@ -228,6 +230,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): except Exception: tp, value, tb = sys.exc_info() session.rollback() + session._configure_locations() six.reraise(tp, value, tb) # Adding metadata @@ -242,6 +245,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): session.commit() except Exception: session.rollback() + session._configure_locations() self.log.warning(( "Comment was not possible to set for AssetVersion" "\"{0}\". Can't set it's value to: \"{1}\"" @@ -258,6 +262,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): continue except Exception: session.rollback() + session._configure_locations() self.log.warning(( "Custom Attrubute \"{0}\"" @@ -272,6 +277,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): except Exception: tp, value, tb = sys.exc_info() session.rollback() + session._configure_locations() six.reraise(tp, value, tb) # Component @@ -316,6 +322,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): except Exception: tp, value, tb = sys.exc_info() session.rollback() + session._configure_locations() six.reraise(tp, value, tb) # Reset members in memory @@ -432,6 +439,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): except Exception: tp, value, tb = sys.exc_info() session.rollback() + session._configure_locations() six.reraise(tp, value, tb) if assetversion_entity not in used_asset_versions: diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py index 9ef82cbc4b..2646dc90cc 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py @@ -88,8 +88,14 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): instance.data["frameEnd"] - instance.data["frameStart"] ) - if not comp.get('fps'): - comp['fps'] = instance.context.data['fps'] + fps = comp.get('fps') + if fps is None: + fps = instance.data.get( + "fps", instance.context.data['fps'] + ) + + comp['fps'] = fps + location = self.get_ftrack_location( 'ftrack.server', ft_session ) diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_note.py b/pype/plugins/ftrack/publish/integrate_ftrack_note.py index 9566207145..acd295854d 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_note.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_note.py @@ -145,4 +145,5 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): except Exception: tp, value, tb = sys.exc_info() session.rollback() + session._configure_locations() six.reraise(tp, value, tb) diff --git a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py index cc569ce2d1..50a25457f5 100644 --- a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py +++ b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py @@ -2,6 +2,7 @@ import sys import six import pyblish.api from avalon import io +from pprint import pformat try: from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_AUTO_SYNC @@ -40,9 +41,14 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): def process(self, context): self.context = context - if "hierarchyContext" not in context.data: + if "hierarchyContext" not in self.context.data: return + hierarchy_context = self.context.data["hierarchyContext"] + + self.log.debug( + f"__ hierarchy_context: `{pformat(hierarchy_context)}`") + self.session = self.context.data["ftrackSession"] project_name = self.context.data["projectEntity"]["name"] query = 'Project where full_name is "{}"'.format(project_name) @@ -55,7 +61,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): self.ft_project = None - input_data = context.data["hierarchyContext"] + input_data = hierarchy_context # disable termporarily ftrack project's autosyncing if auto_sync_state: @@ -128,6 +134,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): except Exception: tp, value, tb = sys.exc_info() self.session.rollback() + self.session._configure_locations() six.reraise(tp, value, tb) # TASKS @@ -156,6 +163,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): except Exception: tp, value, tb = sys.exc_info() self.session.rollback() + self.session._configure_locations() six.reraise(tp, value, tb) # Incoming links. @@ -165,8 +173,31 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): except Exception: tp, value, tb = sys.exc_info() self.session.rollback() + self.session._configure_locations() six.reraise(tp, value, tb) + # Create notes. + user = self.session.query( + "User where username is \"{}\"".format(self.session.api_user) + ).first() + if user: + for comment in entity_data.get("comments", []): + entity.create_note(comment, user) + else: + self.log.warning( + "Was not able to query current User {}".format( + self.session.api_user + ) + ) + try: + self.session.commit() + except Exception: + tp, value, tb = sys.exc_info() + self.session.rollback() + self.session._configure_locations() + six.reraise(tp, value, tb) + + # Import children. if 'childs' in entity_data: self.import_to_ftrack( entity_data['childs'], entity) @@ -180,6 +211,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): except Exception: tp, value, tb = sys.exc_info() self.session.rollback() + self.session._configure_locations() six.reraise(tp, value, tb) # Create new links. @@ -221,6 +253,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): except Exception: tp, value, tb = sys.exc_info() self.session.rollback() + self.session._configure_locations() six.reraise(tp, value, tb) return task @@ -235,6 +268,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): except Exception: tp, value, tb = sys.exc_info() self.session.rollback() + self.session._configure_locations() six.reraise(tp, value, tb) return entity @@ -249,7 +283,8 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): except Exception: tp, value, tb = sys.exc_info() self.session.rollback() - raise + self.session._configure_locations() + six.reraise(tp, value, tb) def auto_sync_on(self, project): @@ -262,4 +297,5 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): except Exception: tp, value, tb = sys.exc_info() self.session.rollback() - raise + self.session._configure_locations() + six.reraise(tp, value, tb) diff --git a/pype/plugins/global/load/copy_file.py b/pype/plugins/global/load/copy_file.py index bbb8e1d6f7..1acacf6b27 100644 --- a/pype/plugins/global/load/copy_file.py +++ b/pype/plugins/global/load/copy_file.py @@ -20,8 +20,8 @@ class CopyFile(api.Loader): def copy_file_to_clipboard(path): from avalon.vendor.Qt import QtCore, QtWidgets - app = QtWidgets.QApplication.instance() - assert app, "Must have running QApplication instance" + clipboard = QtWidgets.QApplication.clipboard() + assert clipboard, "Must have running QApplication instance" # Build mime data for clipboard data = QtCore.QMimeData() @@ -29,5 +29,4 @@ class CopyFile(api.Loader): data.setUrls([url]) # Set to Clipboard - clipboard = app.clipboard() clipboard.setMimeData(data) diff --git a/pype/plugins/global/load/copy_file_path.py b/pype/plugins/global/load/copy_file_path.py index cfda9dc271..f64f3e76d8 100644 --- a/pype/plugins/global/load/copy_file_path.py +++ b/pype/plugins/global/load/copy_file_path.py @@ -19,11 +19,10 @@ class CopyFilePath(api.Loader): @staticmethod def copy_path_to_clipboard(path): - from avalon.vendor.Qt import QtCore, QtWidgets + from avalon.vendor.Qt import QtWidgets - app = QtWidgets.QApplication.instance() - assert app, "Must have running QApplication instance" + clipboard = QtWidgets.QApplication.clipboard() + assert clipboard, "Must have running QApplication instance" # Set to Clipboard - clipboard = app.clipboard() clipboard.setText(os.path.normpath(path)) diff --git a/pype/plugins/global/publish/extract_burnin.py b/pype/plugins/global/publish/extract_burnin.py index dedfd98979..6e8da1b054 100644 --- a/pype/plugins/global/publish/extract_burnin.py +++ b/pype/plugins/global/publish/extract_burnin.py @@ -25,7 +25,8 @@ class ExtractBurnin(pype.api.Extractor): "shell", "nukestudio", "premiere", - "standalonepublisher" + "standalonepublisher", + "harmony" ] optional = True @@ -194,11 +195,14 @@ class ExtractBurnin(pype.api.Extractor): if "delete" in new_repre["tags"]: new_repre["tags"].remove("delete") - # Update name and outputName to be able have multiple outputs - # Join previous "outputName" with filename suffix - new_name = "_".join([new_repre["outputName"], filename_suffix]) - new_repre["name"] = new_name - new_repre["outputName"] = new_name + if len(repre_burnin_defs.keys()) > 1: + # Update name and outputName to be + # able have multiple outputs in case of more burnin presets + # Join previous "outputName" with filename suffix + new_name = "_".join( + [new_repre["outputName"], filename_suffix]) + new_repre["name"] = new_name + new_repre["outputName"] = new_name # Prepare paths and files for process. self.input_output_paths(new_repre, temp_data, filename_suffix) diff --git a/pype/plugins/global/publish/extract_hierarchy_avalon.py b/pype/plugins/global/publish/extract_hierarchy_avalon.py index 7cea825541..4253c35929 100644 --- a/pype/plugins/global/publish/extract_hierarchy_avalon.py +++ b/pype/plugins/global/publish/extract_hierarchy_avalon.py @@ -10,6 +10,7 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): families = ["clip", "shot"] def process(self, context): + # processing starts here if "hierarchyContext" not in context.data: self.log.info("skipping IntegrateHierarchyToAvalon") return @@ -17,7 +18,29 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): if not io.Session: io.install() - input_data = context.data["hierarchyContext"] + active_assets = [] + hierarchy_context = context.data["hierarchyContext"] + hierarchy_assets = self._get_assets(hierarchy_context) + + # filter only the active publishing insatnces + for instance in context: + if instance.data.get("publish") is False: + continue + + if not instance.data.get("asset"): + continue + + active_assets.append(instance.data["asset"]) + + # filter out only assets which are activated as isntances + new_hierarchy_assets = {k: v for k, v in hierarchy_assets.items() + if k in active_assets} + + # modify the hierarchy context so there are only fitred assets + self._set_assets(hierarchy_context, new_hierarchy_assets) + + input_data = context.data["hierarchyContext"] = hierarchy_context + self.project = None self.import_to_avalon(input_data) @@ -83,7 +106,6 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): for task_name in new_tasks: if task_name not in cur_entity_data["tasks"]: cur_entity_data["tasks"].append(task_name) - cur_entity_data.update(data) data = cur_entity_data else: @@ -150,3 +172,41 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): entity_id = io.insert_one(item).inserted_id return io.find_one({"_id": entity_id}) + + def _get_assets(self, input_dict): + """ Returns only asset dictionary. + Usually the last part of deep dictionary which + is not having any children + """ + for key in input_dict.keys(): + # check if child key is available + if input_dict[key].get("childs"): + # loop deeper + return self._get_assets(input_dict[key]["childs"]) + else: + # give the dictionary with assets + return input_dict + + def _set_assets(self, input_dict, new_assets=None): + """ Modify the hierarchy context dictionary. + It will replace the asset dictionary with only the filtred one. + """ + for key in input_dict.keys(): + # check if child key is available + if input_dict[key].get("childs"): + # return if this is just for testing purpose and no + # new_assets property is avalable + if not new_assets: + return True + + # test for deeper inner children availabelity + if self._set_assets(input_dict[key]["childs"]): + # if one level deeper is still children available + # then process farther + self._set_assets(input_dict[key]["childs"], new_assets) + else: + # or just assign the filtred asset ditionary + input_dict[key]["childs"] = new_assets + else: + # test didnt find more childs in input dictionary + return None diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index 89a4bbd664..d23ce4360f 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -81,6 +81,11 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): jpeg_items.append("-i {}".format(full_input_path)) # output arguments from presets jpeg_items.extend(ffmpeg_args.get("output") or []) + + # If its a movie file, we just want one frame. + if repre["ext"] == "mov": + jpeg_items.append("-vframes 1") + # output file jpeg_items.append(full_output_path) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index a3c2ffe52b..68549e9186 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -6,6 +6,8 @@ import copy import clique import errno import six +import re +import shutil from pymongo import DeleteOne, InsertOne import pyblish.api @@ -680,6 +682,14 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): instance.data.get('subsetGroup')}} ) + # Update families on subset. + families = [instance.data["family"]] + families.extend(instance.data.get("families", [])) + io.update_many( + {"type": "subset", "_id": io.ObjectId(subset["_id"])}, + {"$set": {"data.families": families}} + ) + return subset def create_version(self, subset, version_number, data=None): @@ -952,21 +962,37 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): """ if integrated_file_sizes: for file_url, _file_size in integrated_file_sizes.items(): + if not os.path.exists(file_url): + self.log.debug( + "File {} was not found.".format(file_url) + ) + continue + try: if mode == 'remove': - self.log.debug("Removing file ...{}".format(file_url)) + self.log.debug("Removing file {}".format(file_url)) os.remove(file_url) if mode == 'finalize': - self.log.debug("Renaming file ...{}".format(file_url)) - import re - os.rename(file_url, - re.sub('\.{}$'.format(self.TMP_FILE_EXT), - '', - file_url) - ) + new_name = re.sub( + r'\.{}$'.format(self.TMP_FILE_EXT), + '', + file_url + ) - except FileNotFoundError: - pass # file not there, nothing to delete + if os.path.exists(new_name): + self.log.debug( + "Overwriting file {} to {}".format( + file_url, new_name + ) + ) + shutil.copy(file_url, new_name) + else: + self.log.debug( + "Renaming file {} to {}".format( + file_url, new_name + ) + ) + os.rename(file_url, new_name) except OSError: self.log.error("Cannot {} file {}".format(mode, file_url), exc_info=True) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index ab5d6cf9b2..99f0ae7cb6 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -428,7 +428,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "to render, don't know what to do " "with them.") col = rem[0] - _, ext = os.path.splitext(col) + ext = os.path.splitext(col)[1].lstrip(".") else: # but we really expect only one collection. # Nothing else make sense. @@ -718,7 +718,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "resolutionWidth": data.get("resolutionWidth", 1920), "resolutionHeight": data.get("resolutionHeight", 1080), "multipartExr": data.get("multipartExr", False), - "jobBatchName": data.get("jobBatchName", "") + "jobBatchName": data.get("jobBatchName", ""), + "review": data.get("review", True) } if "prerender" in instance.data["families"]: diff --git a/pype/plugins/global/publish/validate_intent.py b/pype/plugins/global/publish/validate_intent.py new file mode 100644 index 0000000000..80bcb0e164 --- /dev/null +++ b/pype/plugins/global/publish/validate_intent.py @@ -0,0 +1,31 @@ +import pyblish.api +import os + + +class ValidateIntent(pyblish.api.ContextPlugin): + """Validate intent of the publish. + + It is required to fill the intent of this publish. Chech the log + for more details + """ + + order = pyblish.api.ValidatorOrder + + label = "Validate Intent" + # TODO: this should be off by default and only activated viac config + tasks = ["animation"] + hosts = ["harmony"] + if os.environ.get("AVALON_TASK") not in tasks: + active = False + + def process(self, context): + msg = ( + "Please make sure that you select the intent of this publish." + ) + + intent = context.data.get("intent") + self.log.debug(intent) + assert intent, msg + + intent_value = intent.get("value") + assert intent is not "", msg diff --git a/pype/plugins/global/publish/validate_version.py b/pype/plugins/global/publish/validate_version.py index 9c7ce72307..6701041541 100644 --- a/pype/plugins/global/publish/validate_version.py +++ b/pype/plugins/global/publish/validate_version.py @@ -10,7 +10,7 @@ class ValidateVersion(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = "Validate Version" - hosts = ["nuke", "maya", "blender"] + hosts = ["nuke", "maya", "blender", "standalonepublisher"] def process(self, instance): version = instance.data.get("version") diff --git a/pype/plugins/harmony/load/load_audio.py b/pype/plugins/harmony/load/load_audio.py index 694fda3247..600791e61a 100644 --- a/pype/plugins/harmony/load/load_audio.py +++ b/pype/plugins/harmony/load/load_audio.py @@ -31,7 +31,7 @@ func class ImportAudioLoader(api.Loader): """Import audio.""" - families = ["shot"] + families = ["shot", "audio"] representations = ["wav"] label = "Import Audio" diff --git a/pype/plugins/harmony/load/load_imagesequence.py b/pype/plugins/harmony/load/load_imagesequence.py index 774782b092..c5f50a7d23 100644 --- a/pype/plugins/harmony/load/load_imagesequence.py +++ b/pype/plugins/harmony/load/load_imagesequence.py @@ -230,7 +230,7 @@ class ImageSequenceLoader(api.Loader): """Load images Stores the imported asset in a container named after the asset. """ - families = ["shot", "render", "image", "plate"] + families = ["shot", "render", "image", "plate", "reference"] representations = ["jpeg", "png", "jpg"] def load(self, context, name=None, namespace=None, data=None): diff --git a/pype/plugins/harmony/publish/extract_render.py b/pype/plugins/harmony/publish/extract_render.py index 45b52e0307..70dceb9ca2 100644 --- a/pype/plugins/harmony/publish/extract_render.py +++ b/pype/plugins/harmony/publish/extract_render.py @@ -4,6 +4,7 @@ import subprocess import pyblish.api from avalon import harmony +import pype.lib import clique @@ -43,6 +44,9 @@ class ExtractRender(pyblish.api.InstancePlugin): frame_start = result[4] frame_end = result[5] audio_path = result[6] + if audio_path: + instance.data["audio"] = [{"filename": audio_path}] + instance.data["fps"] = frame_rate # Set output path to temp folder. path = tempfile.mkdtemp() @@ -87,17 +91,13 @@ class ExtractRender(pyblish.api.InstancePlugin): if len(list(col)) > 1: collection = col else: - # assert len(collections) == 1, ( - # "There should only be one image sequence in {}. Found: {}".format( - # path, len(collections) - # ) - # ) collection = collections[0] # Generate thumbnail. thumbnail_path = os.path.join(path, "thumbnail.png") + ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") args = [ - "ffmpeg", "-y", + ffmpeg_path, "-y", "-i", os.path.join(path, list(collections[0])[0]), "-vf", "scale=300:-1", "-vframes", "1", @@ -117,57 +117,17 @@ class ExtractRender(pyblish.api.InstancePlugin): self.log.debug(output.decode("utf-8")) - # Generate mov. - mov_path = os.path.join(path, instance.data["name"] + ".mov") - if os.path.isfile(audio_path): - args = [ - "ffmpeg", "-y", - "-i", audio_path, - "-i", - os.path.join(path, collection.head + "%04d" + collection.tail), - mov_path - ] - else: - args = [ - "ffmpeg", "-y", - "-i", - os.path.join(path, collection.head + "%04d" + collection.tail), - mov_path - ] - - process = subprocess.Popen( - args, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - stdin=subprocess.PIPE - ) - - output = process.communicate()[0] - - if process.returncode != 0: - raise ValueError(output.decode("utf-8")) - - self.log.debug(output.decode("utf-8")) - # Generate representations. extension = collection.tail[1:] representation = { "name": extension, "ext": extension, "files": list(collection), - "stagingDir": path - } - movie = { - "name": "mov", - "ext": "mov", - "files": os.path.basename(mov_path), "stagingDir": path, - "frameStart": frame_start, - "frameEnd": frame_end, - "fps": frame_rate, - "preview": True, - "tags": ["review", "ftrackreview"] + "tags": ["review"], + "fps": frame_rate } + thumbnail = { "name": "thumbnail", "ext": "png", @@ -175,7 +135,7 @@ class ExtractRender(pyblish.api.InstancePlugin): "stagingDir": path, "tags": ["thumbnail"] } - instance.data["representations"] = [representation, movie, thumbnail] + instance.data["representations"] = [representation, thumbnail] # Required for extract_review plugin (L222 onwards). instance.data["frameStart"] = frame_start diff --git a/pype/plugins/maya/create/create_review.py b/pype/plugins/maya/create/create_review.py index d76b78c580..97731d7950 100644 --- a/pype/plugins/maya/create/create_review.py +++ b/pype/plugins/maya/create/create_review.py @@ -12,6 +12,7 @@ class CreateReview(avalon.maya.Creator): icon = "video-camera" defaults = ['Main'] keepImages = False + isolate = False def __init__(self, *args, **kwargs): super(CreateReview, self).__init__(*args, **kwargs) @@ -22,6 +23,7 @@ class CreateReview(avalon.maya.Creator): for key, value in animation_data.items(): data[key] = value + data["isolate"] = self.isolate data["keepImages"] = self.keepImages self.data = data diff --git a/pype/plugins/maya/load/load_audio.py b/pype/plugins/maya/load/load_audio.py index ca38082ed0..81bcca48e1 100644 --- a/pype/plugins/maya/load/load_audio.py +++ b/pype/plugins/maya/load/load_audio.py @@ -1,7 +1,7 @@ from maya import cmds, mel import pymel.core as pc -from avalon import api +from avalon import api, io from avalon.maya.pipeline import containerise from avalon.maya import lib @@ -58,6 +58,13 @@ class AudioLoader(api.Loader): type="string" ) + # Set frame range. + version = io.find_one({"_id": representation["parent"]}) + subset = io.find_one({"_id": version["parent"]}) + asset = io.find_one({"_id": subset["parent"]}) + audio_node.sourceStart.set(1 - asset["data"]["frameStart"]) + audio_node.sourceEnd.set(asset["data"]["frameEnd"]) + def switch(self, container, representation): self.update(container, representation) diff --git a/pype/plugins/maya/load/load_image_plane.py b/pype/plugins/maya/load/load_image_plane.py index 17a6866f80..e17382f7ed 100644 --- a/pype/plugins/maya/load/load_image_plane.py +++ b/pype/plugins/maya/load/load_image_plane.py @@ -1,7 +1,7 @@ import pymel.core as pc import maya.cmds as cmds -from avalon import api +from avalon import api, io from avalon.maya.pipeline import containerise from avalon.maya import lib from Qt import QtWidgets @@ -12,7 +12,7 @@ class ImagePlaneLoader(api.Loader): families = ["plate", "render"] label = "Create imagePlane on selected camera." - representations = ["mov", "exr", "preview"] + representations = ["mov", "exr", "preview", "png"] icon = "image" color = "orange" @@ -29,6 +29,8 @@ class ImagePlaneLoader(api.Loader): # Getting camera from selection. selection = pc.ls(selection=True) + camera = None + if len(selection) > 1: QtWidgets.QMessageBox.critical( None, @@ -39,25 +41,29 @@ class ImagePlaneLoader(api.Loader): return if len(selection) < 1: - QtWidgets.QMessageBox.critical( + result = QtWidgets.QMessageBox.critical( None, "Error!", - "No camera selected.", - QtWidgets.QMessageBox.Ok + "No camera selected. Do you want to create a camera?", + QtWidgets.QMessageBox.Ok, + QtWidgets.QMessageBox.Cancel ) - return - - relatives = pc.listRelatives(selection[0], shapes=True) - if not pc.ls(relatives, type="camera"): - QtWidgets.QMessageBox.critical( - None, - "Error!", - "Selected node is not a camera.", - QtWidgets.QMessageBox.Ok - ) - return - - camera = selection[0] + if result == QtWidgets.QMessageBox.Ok: + camera = pc.createNode("camera") + else: + return + else: + relatives = pc.listRelatives(selection[0], shapes=True) + if pc.ls(relatives, type="camera"): + camera = selection[0] + else: + QtWidgets.QMessageBox.critical( + None, + "Error!", + "Selected node is not a camera.", + QtWidgets.QMessageBox.Ok + ) + return try: camera.displayResolution.set(1) @@ -81,6 +87,7 @@ class ImagePlaneLoader(api.Loader): image_plane_shape.frameOffset.set(1 - start_frame) image_plane_shape.frameIn.set(start_frame) image_plane_shape.frameOut.set(end_frame) + image_plane_shape.frameCache.set(end_frame) image_plane_shape.useFrameExtension.set(1) movie_representations = ["mov", "preview"] @@ -140,6 +147,17 @@ class ImagePlaneLoader(api.Loader): type="string" ) + # Set frame range. + version = io.find_one({"_id": representation["parent"]}) + subset = io.find_one({"_id": version["parent"]}) + asset = io.find_one({"_id": subset["parent"]}) + start_frame = asset["data"]["frameStart"] + end_frame = asset["data"]["frameEnd"] + image_plane_shape.frameOffset.set(1 - start_frame) + image_plane_shape.frameIn.set(start_frame) + image_plane_shape.frameOut.set(end_frame) + image_plane_shape.frameCache.set(end_frame) + def switch(self, container, representation): self.update(container, representation) diff --git a/pype/plugins/maya/publish/collect_review.py b/pype/plugins/maya/publish/collect_review.py index e2df54c10b..886e3b82dd 100644 --- a/pype/plugins/maya/publish/collect_review.py +++ b/pype/plugins/maya/publish/collect_review.py @@ -64,6 +64,7 @@ class CollectReview(pyblish.api.InstancePlugin): data['handles'] = instance.data.get('handles', None) data['step'] = instance.data['step'] data['fps'] = instance.data['fps'] + data["isolate"] = instance.data["isolate"] cmds.setAttr(str(instance) + '.active', 1) self.log.debug('data {}'.format(instance.context[i].data)) instance.context[i].data.update(data) diff --git a/pype/plugins/maya/publish/extract_playblast.py b/pype/plugins/maya/publish/extract_playblast.py index 80a4dadf28..8f8b7fcb36 100644 --- a/pype/plugins/maya/publish/extract_playblast.py +++ b/pype/plugins/maya/publish/extract_playblast.py @@ -76,6 +76,11 @@ class ExtractPlayblast(pype.api.Extractor): pm.currentTime(refreshFrameInt - 1, edit=True) pm.currentTime(refreshFrameInt, edit=True) + # Isolate view is requested by having objects in the set besides a + # camera. + if instance.data.get("isolate"): + preset["isolate"] = instance.data["setMembers"] + with maintained_time(): filename = preset.get("filename", "%TEMP%") diff --git a/pype/plugins/maya/publish/extract_thumbnail.py b/pype/plugins/maya/publish/extract_thumbnail.py index c0eb2a608e..524fc1e17c 100644 --- a/pype/plugins/maya/publish/extract_thumbnail.py +++ b/pype/plugins/maya/publish/extract_thumbnail.py @@ -77,6 +77,11 @@ class ExtractThumbnail(pype.api.Extractor): pm.currentTime(refreshFrameInt - 1, edit=True) pm.currentTime(refreshFrameInt, edit=True) + # Isolate view is requested by having objects in the set besides a + # camera. + if instance.data.get("isolate"): + preset["isolate"] = instance.data["setMembers"] + with maintained_time(): filename = preset.get("filename", "%TEMP%") diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index 747d2727b7..e4048592a7 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -25,6 +25,7 @@ import re import hashlib from datetime import datetime import itertools +from collections import OrderedDict import clique import requests @@ -67,7 +68,7 @@ payload_skeleton = { def _format_tiles( filename, index, tiles_x, tiles_y, - width, height, prefix, origin="blc"): + width, height, prefix): """Generate tile entries for Deadline tile job. Returns two dictionaries - one that can be directly used in Deadline @@ -113,12 +114,14 @@ def _format_tiles( """ tile = 0 out = {"JobInfo": {}, "PluginInfo": {}} - cfg = {} + cfg = OrderedDict() w_space = width / tiles_x h_space = height / tiles_y + cfg["TilesCropped"] = "False" + for tile_x in range(1, tiles_x + 1): - for tile_y in range(1, tiles_y + 1): + for tile_y in reversed(range(1, tiles_y + 1)): tile_prefix = "_tile_{}x{}_{}x{}_".format( tile_x, tile_y, tiles_x, @@ -143,14 +146,13 @@ def _format_tiles( cfg["Tile{}".format(tile)] = new_filename cfg["Tile{}Tile".format(tile)] = new_filename + cfg["Tile{}FileName".format(tile)] = new_filename cfg["Tile{}X".format(tile)] = (tile_x - 1) * w_space - if origin == "blc": - cfg["Tile{}Y".format(tile)] = (tile_y - 1) * h_space - else: - cfg["Tile{}Y".format(tile)] = int(height) - ((tile_y - 1) * h_space) # noqa: E501 - cfg["Tile{}Width".format(tile)] = tile_x * w_space - cfg["Tile{}Height".format(tile)] = tile_y * h_space + cfg["Tile{}Y".format(tile)] = int(height) - (tile_y * h_space) + + cfg["Tile{}Width".format(tile)] = w_space + cfg["Tile{}Height".format(tile)] = h_space tile += 1 return out, cfg @@ -538,7 +540,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): "AuxFiles": [], "JobInfo": { "BatchName": payload["JobInfo"]["BatchName"], - "Frames": 0, + "Frames": 1, "Name": "{} - Tile Assembly Job".format( payload["JobInfo"]["Name"]), "OutputDirectory0": @@ -590,7 +592,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): payload["JobInfo"]["Name"], frame, instance.data.get("tilesX") * instance.data.get("tilesY") # noqa: E501 - ) + ) self.log.info( "... preparing job {}".format( new_payload["JobInfo"]["Name"])) diff --git a/pype/plugins/nuke/load/load_image.py b/pype/plugins/nuke/load/load_image.py new file mode 100644 index 0000000000..377d52aa14 --- /dev/null +++ b/pype/plugins/nuke/load/load_image.py @@ -0,0 +1,233 @@ +import re +import nuke + +from avalon.vendor import qargparse +from avalon import api, io + +from pype.hosts.nuke import presets + + +class LoadImage(api.Loader): + """Load still image into Nuke""" + + families = [ + "render2d", "source", "plate", + "render", "prerender", "review", + "image" + ] + representations = ["exr", "dpx", "jpg", "jpeg", "png", "psd"] + + label = "Load Image" + order = -10 + icon = "image" + color = "white" + + options = [ + qargparse.Integer( + "frame_number", + label="Frame Number", + default=int(nuke.root()["first_frame"].getValue()), + min=1, + max=999999, + help="What frame is reading from?" + ) + ] + + def load(self, context, name, namespace, options): + from avalon.nuke import ( + containerise, + viewer_update_and_undo_stop + ) + self.log.info("__ options: `{}`".format(options)) + frame_number = options.get("frame_number", 1) + + version = context['version'] + version_data = version.get("data", {}) + repr_id = context["representation"]["_id"] + + self.log.info("version_data: {}\n".format(version_data)) + self.log.debug( + "Representation id `{}` ".format(repr_id)) + + last = first = int(frame_number) + + # Fallback to asset name when namespace is None + if namespace is None: + namespace = context['asset']['name'] + + file = self.fname + + if not file: + repr_id = context["representation"]["_id"] + self.log.warning( + "Representation id `{}` is failing to load".format(repr_id)) + return + + file = file.replace("\\", "/") + + repr_cont = context["representation"]["context"] + frame = repr_cont.get("frame") + if frame: + padding = len(frame) + file = file.replace( + frame, + format(frame_number, "0{}".format(padding))) + + read_name = "Read_{0}_{1}_{2}".format( + repr_cont["asset"], + repr_cont["subset"], + repr_cont["representation"]) + + # Create the Loader with the filename path set + with viewer_update_and_undo_stop(): + r = nuke.createNode( + "Read", + "name {}".format(read_name)) + r["file"].setValue(file) + + # Set colorspace defined in version data + colorspace = context["version"]["data"].get("colorspace") + if colorspace: + r["colorspace"].setValue(str(colorspace)) + + # load nuke presets for Read's colorspace + read_clrs_presets = presets.get_colorspace_preset().get( + "nuke", {}).get("read", {}) + + # check if any colorspace presets for read is mathing + preset_clrsp = next((read_clrs_presets[k] + for k in read_clrs_presets + if bool(re.search(k, file))), + None) + if preset_clrsp is not None: + r["colorspace"].setValue(str(preset_clrsp)) + + r["origfirst"].setValue(first) + r["first"].setValue(first) + r["origlast"].setValue(last) + r["last"].setValue(last) + + # add additional metadata from the version to imprint Avalon knob + add_keys = ["source", "colorspace", "author", "fps", "version"] + + data_imprint = { + "frameStart": first, + "frameEnd": last + } + for k in add_keys: + if k == 'version': + data_imprint.update({k: context["version"]['name']}) + else: + data_imprint.update( + {k: context["version"]['data'].get(k, str(None))}) + + data_imprint.update({"objectName": read_name}) + + r["tile_color"].setValue(int("0x4ecd25ff", 16)) + + return containerise(r, + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__, + data=data_imprint) + + def switch(self, container, representation): + self.update(container, representation) + + def update(self, container, representation): + """Update the Loader's path + + Nuke automatically tries to reset some variables when changing + the loader's path to a new file. These automatic changes are to its + inputs: + + """ + + from avalon.nuke import ( + update_container + ) + + node = nuke.toNode(container["objectName"]) + frame_number = node["first"].value() + + assert node.Class() == "Read", "Must be Read" + + repr_cont = representation["context"] + + file = api.get_representation_path(representation) + + if not file: + repr_id = representation["_id"] + self.log.warning( + "Representation id `{}` is failing to load".format(repr_id)) + return + + file = file.replace("\\", "/") + + frame = repr_cont.get("frame") + if frame: + padding = len(frame) + file = file.replace( + frame, + format(frame_number, "0{}".format(padding))) + + # Get start frame from version data + version = io.find_one({ + "type": "version", + "_id": representation["parent"] + }) + + # get all versions in list + versions = io.find({ + "type": "version", + "parent": version["parent"] + }).distinct('name') + + max_version = max(versions) + + version_data = version.get("data", {}) + + last = first = int(frame_number) + + # Set the global in to the start frame of the sequence + node["origfirst"].setValue(first) + node["first"].setValue(first) + node["origlast"].setValue(last) + node["last"].setValue(last) + + updated_dict = {} + updated_dict.update({ + "representation": str(representation["_id"]), + "frameStart": str(first), + "frameEnd": str(last), + "version": str(version.get("name")), + "colorspace": version_data.get("colorspace"), + "source": version_data.get("source"), + "fps": str(version_data.get("fps")), + "author": version_data.get("author"), + "outputDir": version_data.get("outputDir"), + }) + + # change color of node + if version.get("name") not in [max_version]: + node["tile_color"].setValue(int("0xd84f20ff", 16)) + else: + node["tile_color"].setValue(int("0x4ecd25ff", 16)) + + # Update the imprinted representation + update_container( + node, + updated_dict + ) + self.log.info("udated to version: {}".format(version.get("name"))) + + def remove(self, container): + + from avalon.nuke import viewer_update_and_undo_stop + + node = nuke.toNode(container['objectName']) + assert node.Class() == "Read", "Must be Read" + + with viewer_update_and_undo_stop(): + nuke.delete(node) diff --git a/pype/plugins/nuke/load/load_sequence.py b/pype/plugins/nuke/load/load_sequence.py index 601e28c7c1..c5ce288540 100644 --- a/pype/plugins/nuke/load/load_sequence.py +++ b/pype/plugins/nuke/load/load_sequence.py @@ -120,12 +120,12 @@ class LoadSequence(api.Loader): if "#" not in file: frame = repr_cont.get("frame") padding = len(frame) - file = file.replace(frame, "#"*padding) + file = file.replace(frame, "#" * padding) read_name = "Read_{0}_{1}_{2}".format( - repr_cont["asset"], - repr_cont["subset"], - repr_cont["representation"]) + repr_cont["asset"], + repr_cont["subset"], + repr_cont["representation"]) # Create the Loader with the filename path set with viewer_update_and_undo_stop(): @@ -250,7 +250,7 @@ class LoadSequence(api.Loader): if "#" not in file: frame = repr_cont.get("frame") padding = len(frame) - file = file.replace(frame, "#"*padding) + file = file.replace(frame, "#" * padding) # Get start frame from version data version = io.find_one({ @@ -276,10 +276,10 @@ class LoadSequence(api.Loader): last = version_data.get("frameEnd") if first is None: - self.log.warning("Missing start frame for updated version" - "assuming starts at frame 0 for: " - "{} ({})".format( - node['name'].value(), representation)) + self.log.warning( + "Missing start frame for updated version" + "assuming starts at frame 0 for: " + "{} ({})".format(node['name'].value(), representation)) first = 0 first -= self.handle_start diff --git a/pype/plugins/nuke/publish/extract_thumbnail.py b/pype/plugins/nuke/publish/extract_thumbnail.py index a3ef09bc9f..a53cb4d146 100644 --- a/pype/plugins/nuke/publish/extract_thumbnail.py +++ b/pype/plugins/nuke/publish/extract_thumbnail.py @@ -15,10 +15,12 @@ class ExtractThumbnail(pype.api.Extractor): order = pyblish.api.ExtractorOrder + 0.01 label = "Extract Thumbnail" - families = ["review", "render.farm"] + families = ["review"] hosts = ["nuke"] def process(self, instance): + if "render.farm" in instance.data["families"]: + return with anlib.maintained_selection(): self.log.debug("instance: {}".format(instance)) diff --git a/pype/plugins/nukestudio/publish/collect_hierarchy_context.py b/pype/plugins/nukestudio/publish/collect_hierarchy_context.py index a41e987bdb..930efd618e 100644 --- a/pype/plugins/nukestudio/publish/collect_hierarchy_context.py +++ b/pype/plugins/nukestudio/publish/collect_hierarchy_context.py @@ -273,8 +273,6 @@ class CollectHierarchyContext(pyblish.api.ContextPlugin): instance.data["clipOut"] - instance.data["clipIn"]) - - self.log.debug( "__ instance.data[parents]: {}".format( instance.data["parents"] @@ -319,6 +317,7 @@ class CollectHierarchyContext(pyblish.api.ContextPlugin): }) in_info['tasks'] = instance.data['tasks'] + in_info["comments"] = instance.data.get("comments", []) parents = instance.data.get('parents', []) self.log.debug("__ in_info: {}".format(in_info)) diff --git a/pype/plugins/nukestudio/publish/collect_shots.py b/pype/plugins/nukestudio/publish/collect_shots.py index 455e25bf82..7055167143 100644 --- a/pype/plugins/nukestudio/publish/collect_shots.py +++ b/pype/plugins/nukestudio/publish/collect_shots.py @@ -40,11 +40,12 @@ class CollectShots(api.InstancePlugin): data["name"] = data["subset"] + "_" + data["asset"] data["label"] = ( - "{} - {} - tasks:{} - assetbuilds:{}".format( + "{} - {} - tasks: {} - assetbuilds: {} - comments: {}".format( data["asset"], data["subset"], data["tasks"], - [x["name"] for x in data.get("assetbuilds", [])] + [x["name"] for x in data.get("assetbuilds", [])], + len(data.get("comments", [])) ) ) diff --git a/pype/plugins/nukestudio/publish/collect_tag_comments.py b/pype/plugins/nukestudio/publish/collect_tag_comments.py index 1ec98e3d3b..e14e53d439 100644 --- a/pype/plugins/nukestudio/publish/collect_tag_comments.py +++ b/pype/plugins/nukestudio/publish/collect_tag_comments.py @@ -17,7 +17,7 @@ class CollectClipTagComments(api.InstancePlugin): for tag in instance.data["tags"]: if tag["name"].lower() == "comment": instance.data["comments"].append( - tag.metadata().dict()["tag.note"] + tag["metadata"]["tag.note"] ) # Find tags on the source clip. diff --git a/pype/plugins/photoshop/create/create_image.py b/pype/plugins/photoshop/create/create_image.py index 5b2f9f7981..c1a7d92a2c 100644 --- a/pype/plugins/photoshop/create/create_image.py +++ b/pype/plugins/photoshop/create/create_image.py @@ -1,5 +1,6 @@ -from avalon import api, photoshop +from avalon import api from avalon.vendor import Qt +from avalon import photoshop class CreateImage(api.Creator): @@ -13,11 +14,12 @@ class CreateImage(api.Creator): groups = [] layers = [] create_group = False - group_constant = photoshop.get_com_objects().constants().psLayerSet + + stub = photoshop.stub() if (self.options or {}).get("useSelection"): multiple_instances = False - selection = photoshop.get_selected_layers() - + selection = stub.get_selected_layers() + self.log.info("selection {}".format(selection)) if len(selection) > 1: # Ask user whether to create one image or image per selected # item. @@ -40,19 +42,18 @@ class CreateImage(api.Creator): if multiple_instances: for item in selection: - if item.LayerType == group_constant: + if item.group: groups.append(item) else: layers.append(item) else: - group = photoshop.group_selected_layers() - group.Name = self.name + group = stub.group_selected_layers(self.name) groups.append(group) elif len(selection) == 1: # One selected item. Use group if its a LayerSet (group), else # create a new group. - if selection[0].LayerType == group_constant: + if selection[0].group: groups.append(selection[0]) else: layers.append(selection[0]) @@ -63,16 +64,14 @@ class CreateImage(api.Creator): create_group = True if create_group: - group = photoshop.app().ActiveDocument.LayerSets.Add() - group.Name = self.name + group = stub.create_group(self.name) groups.append(group) for layer in layers: - photoshop.select_layers([layer]) - group = photoshop.group_selected_layers() - group.Name = layer.Name + stub.select_layers([layer]) + group = stub.group_selected_layers(layer.name) groups.append(group) for group in groups: - self.data.update({"subset": "image" + group.Name}) - photoshop.imprint(group, self.data) + self.data.update({"subset": "image" + group.name}) + stub.imprint(group, self.data) diff --git a/pype/plugins/photoshop/load/load_image.py b/pype/plugins/photoshop/load/load_image.py index 18efe750d5..75c02bb327 100644 --- a/pype/plugins/photoshop/load/load_image.py +++ b/pype/plugins/photoshop/load/load_image.py @@ -1,5 +1,7 @@ from avalon import api, photoshop +stub = photoshop.stub() + class ImageLoader(api.Loader): """Load images @@ -12,7 +14,7 @@ class ImageLoader(api.Loader): def load(self, context, name=None, namespace=None, data=None): with photoshop.maintained_selection(): - layer = photoshop.import_smart_object(self.fname) + layer = stub.import_smart_object(self.fname) self[:] = [layer] @@ -28,11 +30,11 @@ class ImageLoader(api.Loader): layer = container.pop("layer") with photoshop.maintained_selection(): - photoshop.replace_smart_object( + stub.replace_smart_object( layer, api.get_representation_path(representation) ) - photoshop.imprint( + stub.imprint( layer, {"representation": str(representation["_id"])} ) diff --git a/pype/plugins/photoshop/publish/collect_current_file.py b/pype/plugins/photoshop/publish/collect_current_file.py index 4308588559..3cc3e3f636 100644 --- a/pype/plugins/photoshop/publish/collect_current_file.py +++ b/pype/plugins/photoshop/publish/collect_current_file.py @@ -1,6 +1,7 @@ import os import pyblish.api + from avalon import photoshop @@ -13,5 +14,5 @@ class CollectCurrentFile(pyblish.api.ContextPlugin): def process(self, context): context.data["currentFile"] = os.path.normpath( - photoshop.app().ActiveDocument.FullName + photoshop.stub().get_active_document_full_name() ).replace("\\", "/") diff --git a/pype/plugins/photoshop/publish/collect_instances.py b/pype/plugins/photoshop/publish/collect_instances.py index 4937f2a1e4..81d1c80bf6 100644 --- a/pype/plugins/photoshop/publish/collect_instances.py +++ b/pype/plugins/photoshop/publish/collect_instances.py @@ -1,9 +1,9 @@ import pythoncom -from avalon import photoshop - import pyblish.api +from avalon import photoshop + class CollectInstances(pyblish.api.ContextPlugin): """Gather instances by LayerSet and file metadata @@ -27,8 +27,11 @@ class CollectInstances(pyblish.api.ContextPlugin): # can be. pythoncom.CoInitialize() - for layer in photoshop.get_layers_in_document(): - layer_data = photoshop.read(layer) + stub = photoshop.stub() + layers = stub.get_layers() + layers_meta = stub.get_layers_metadata() + for layer in layers: + layer_data = stub.read(layer, layers_meta) # Skip layers without metadata. if layer_data is None: @@ -38,18 +41,19 @@ class CollectInstances(pyblish.api.ContextPlugin): if "container" in layer_data["id"]: continue - child_layers = [*layer.Layers] - if not child_layers: - self.log.info("%s skipped, it was empty." % layer.Name) - continue + # child_layers = [*layer.Layers] + # self.log.debug("child_layers {}".format(child_layers)) + # if not child_layers: + # self.log.info("%s skipped, it was empty." % layer.Name) + # continue - instance = context.create_instance(layer.Name) + instance = context.create_instance(layer.name) instance.append(layer) instance.data.update(layer_data) instance.data["families"] = self.families_mapping[ layer_data["family"] ] - instance.data["publish"] = layer.Visible + instance.data["publish"] = layer.visible # Produce diagnostic message for any graphical # user interface interested in visualising it. diff --git a/pype/plugins/photoshop/publish/extract_image.py b/pype/plugins/photoshop/publish/extract_image.py index 6dfccdc4f2..38920b5557 100644 --- a/pype/plugins/photoshop/publish/extract_image.py +++ b/pype/plugins/photoshop/publish/extract_image.py @@ -21,35 +21,37 @@ class ExtractImage(pype.api.Extractor): self.log.info("Outputting image to {}".format(staging_dir)) # Perform extraction + stub = photoshop.stub() files = {} with photoshop.maintained_selection(): self.log.info("Extracting %s" % str(list(instance))) with photoshop.maintained_visibility(): # Hide all other layers. - extract_ids = [ - x.id for x in photoshop.get_layers_in_layers([instance[0]]) - ] - for layer in photoshop.get_layers_in_document(): - if layer.id not in extract_ids: - layer.Visible = False + extract_ids = set([ll.id for ll in stub. + get_layers_in_layers([instance[0]])]) - save_options = {} + for layer in stub.get_layers(): + # limit unnecessary calls to client + if layer.visible and layer.id not in extract_ids: + stub.set_visible(layer.id, False) + if not layer.visible and layer.id in extract_ids: + stub.set_visible(layer.id, True) + + save_options = [] if "png" in self.formats: - save_options["png"] = photoshop.com_objects.PNGSaveOptions() + save_options.append('png') if "jpg" in self.formats: - save_options["jpg"] = photoshop.com_objects.JPEGSaveOptions() + save_options.append('jpg') file_basename = os.path.splitext( - photoshop.app().ActiveDocument.Name + stub.get_active_document_name() )[0] - for extension, save_option in save_options.items(): + for extension in save_options: _filename = "{}.{}".format(file_basename, extension) files[extension] = _filename full_filename = os.path.join(staging_dir, _filename) - photoshop.app().ActiveDocument.SaveAs( - full_filename, save_option, True - ) + stub.saveAs(full_filename, extension, True) representations = [] for extension, filename in files.items(): diff --git a/pype/plugins/photoshop/publish/extract_review.py b/pype/plugins/photoshop/publish/extract_review.py index 078ee53899..6fb50bba9f 100644 --- a/pype/plugins/photoshop/publish/extract_review.py +++ b/pype/plugins/photoshop/publish/extract_review.py @@ -13,10 +13,11 @@ class ExtractReview(pype.api.Extractor): families = ["review"] def process(self, instance): - staging_dir = self.staging_dir(instance) self.log.info("Outputting image to {}".format(staging_dir)) + stub = photoshop.stub() + layers = [] for image_instance in instance.context: if image_instance.data["family"] != "image": @@ -25,25 +26,22 @@ class ExtractReview(pype.api.Extractor): # Perform extraction output_image = "{}.jpg".format( - os.path.splitext(photoshop.app().ActiveDocument.Name)[0] + os.path.splitext(stub.get_active_document_name())[0] ) output_image_path = os.path.join(staging_dir, output_image) with photoshop.maintained_visibility(): # Hide all other layers. - extract_ids = [ - x.id for x in photoshop.get_layers_in_layers(layers) - ] - for layer in photoshop.get_layers_in_document(): - if layer.id in extract_ids: - layer.Visible = True - else: - layer.Visible = False + extract_ids = set([ll.id for ll in stub. + get_layers_in_layers(layers)]) + self.log.info("extract_ids {}".format(extract_ids)) + for layer in stub.get_layers(): + # limit unnecessary calls to client + if layer.visible and layer.id not in extract_ids: + stub.set_visible(layer.id, False) + if not layer.visible and layer.id in extract_ids: + stub.set_visible(layer.id, True) - photoshop.app().ActiveDocument.SaveAs( - output_image_path, - photoshop.com_objects.JPEGSaveOptions(), - True - ) + stub.saveAs(output_image_path, 'jpg', True) ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") @@ -66,8 +64,6 @@ class ExtractReview(pype.api.Extractor): ] output = pype.lib._subprocess(args) - self.log.debug(output) - instance.data["representations"].append({ "name": "thumbnail", "ext": "jpg", @@ -75,7 +71,6 @@ class ExtractReview(pype.api.Extractor): "stagingDir": staging_dir, "tags": ["thumbnail"] }) - # Generate mov. mov_path = os.path.join(staging_dir, "review.mov") args = [ @@ -86,9 +81,7 @@ class ExtractReview(pype.api.Extractor): mov_path ] output = pype.lib._subprocess(args) - self.log.debug(output) - instance.data["representations"].append({ "name": "mov", "ext": "mov", diff --git a/pype/plugins/photoshop/publish/extract_save_scene.py b/pype/plugins/photoshop/publish/extract_save_scene.py index b3d4f0e447..63a4b7b7ea 100644 --- a/pype/plugins/photoshop/publish/extract_save_scene.py +++ b/pype/plugins/photoshop/publish/extract_save_scene.py @@ -11,4 +11,4 @@ class ExtractSaveScene(pype.api.Extractor): families = ["workfile"] def process(self, instance): - photoshop.app().ActiveDocument.Save() + photoshop.stub().save() diff --git a/pype/plugins/photoshop/publish/increment_workfile.py b/pype/plugins/photoshop/publish/increment_workfile.py index ba9ab8606a..eca2583595 100644 --- a/pype/plugins/photoshop/publish/increment_workfile.py +++ b/pype/plugins/photoshop/publish/increment_workfile.py @@ -1,6 +1,7 @@ import pyblish.api from pype.action import get_errored_plugins_from_data from pype.lib import version_up + from avalon import photoshop @@ -24,6 +25,6 @@ class IncrementWorkfile(pyblish.api.InstancePlugin): ) scene_path = version_up(instance.context.data["currentFile"]) - photoshop.app().ActiveDocument.SaveAs(scene_path) + photoshop.stub().saveAs(scene_path, 'psd', True) self.log.info("Incremented workfile to: {}".format(scene_path)) diff --git a/pype/plugins/photoshop/publish/validate_instance_asset.py b/pype/plugins/photoshop/publish/validate_instance_asset.py index ab1d02269f..f05d9601dd 100644 --- a/pype/plugins/photoshop/publish/validate_instance_asset.py +++ b/pype/plugins/photoshop/publish/validate_instance_asset.py @@ -23,11 +23,12 @@ class ValidateInstanceAssetRepair(pyblish.api.Action): # Apply pyblish.logic to get the instances for the plug-in instances = pyblish.api.instances_by_plugin(failed, plugin) - + stub = photoshop.stub() for instance in instances: - data = photoshop.read(instance[0]) + data = stub.read(instance[0]) + data["asset"] = os.environ["AVALON_ASSET"] - photoshop.imprint(instance[0], data) + stub.imprint(instance[0], data) class ValidateInstanceAsset(pyblish.api.InstancePlugin): diff --git a/pype/plugins/photoshop/publish/validate_naming.py b/pype/plugins/photoshop/publish/validate_naming.py index 51e00da352..2483adcb5e 100644 --- a/pype/plugins/photoshop/publish/validate_naming.py +++ b/pype/plugins/photoshop/publish/validate_naming.py @@ -21,13 +21,14 @@ class ValidateNamingRepair(pyblish.api.Action): # Apply pyblish.logic to get the instances for the plug-in instances = pyblish.api.instances_by_plugin(failed, plugin) - + stub = photoshop.stub() for instance in instances: + self.log.info("validate_naming instance {}".format(instance)) name = instance.data["name"].replace(" ", "_") instance[0].Name = name - data = photoshop.read(instance[0]) + data = stub.read(instance[0]) data["subset"] = "image" + name - photoshop.imprint(instance[0], data) + stub.imprint(instance[0], data) return True diff --git a/pype/plugins/resolve/create/create_shot_clip.py b/pype/plugins/resolve/create/create_shot_clip.py new file mode 100644 index 0000000000..bd2e013fac --- /dev/null +++ b/pype/plugins/resolve/create/create_shot_clip.py @@ -0,0 +1,79 @@ +from pprint import pformat +from pype.hosts import resolve +from pype.hosts.resolve import lib + + +class CreateShotClip(resolve.Creator): + """Publishable clip""" + + label = "Shot" + family = "clip" + icon = "film" + defaults = ["Main"] + + gui_name = "Pype sequencial rename with hirerarchy" + gui_info = "Define sequencial rename and fill hierarchy data." + gui_inputs = { + "clipName": "{episode}{sequence}{shot}", + "hierarchy": "{folder}/{sequence}/{shot}", + "countFrom": 10, + "steps": 10, + "hierarchyData": { + "folder": "shots", + "shot": "sh####", + "track": "{track}", + "sequence": "sc010", + "episode": "ep01" + } + } + presets = None + + def process(self): + # solve gui inputs overwrites from presets + # overwrite gui inputs from presets + for k, v in self.gui_inputs.items(): + if isinstance(v, dict): + # nested dictionary (only one level allowed) + for _k, _v in v.items(): + if self.presets.get(_k): + self.gui_inputs[k][_k] = self.presets[_k] + if self.presets.get(k): + self.gui_inputs[k] = self.presets[k] + + # open widget for plugins inputs + widget = self.widget(self.gui_name, self.gui_info, self.gui_inputs) + widget.exec_() + + print(f"__ selected_clips: {self.selected}") + if len(self.selected) < 1: + return + + if not widget.result: + print("Operation aborted") + return + + # sequence attrs + sq_frame_start = self.sequence.GetStartFrame() + sq_markers = self.sequence.GetMarkers() + print(f"__ sq_frame_start: {pformat(sq_frame_start)}") + print(f"__ seq_markers: {pformat(sq_markers)}") + + # create media bin for compound clips (trackItems) + mp_folder = resolve.create_current_sequence_media_bin(self.sequence) + print(f"_ mp_folder: {mp_folder.GetName()}") + + lib.rename_add = 0 + for i, t_data in enumerate(self.selected): + lib.rename_index = i + + # clear color after it is done + t_data["clip"]["item"].ClearClipColor() + + # convert track item to timeline media pool item + resolve.create_compound_clip( + t_data, + mp_folder, + rename=True, + **dict( + {"presets": widget.result}) + ) diff --git a/pype/plugins/resolve/publish/collect_clips.py b/pype/plugins/resolve/publish/collect_clips.py new file mode 100644 index 0000000000..f86e5c8384 --- /dev/null +++ b/pype/plugins/resolve/publish/collect_clips.py @@ -0,0 +1,162 @@ +import os +from pyblish import api +from pype.hosts import resolve +import json + + +class CollectClips(api.ContextPlugin): + """Collect all Track items selection.""" + + order = api.CollectorOrder + 0.01 + label = "Collect Clips" + hosts = ["resolve"] + + def process(self, context): + # create asset_names conversion table + if not context.data.get("assetsShared"): + self.log.debug("Created `assetsShared` in context") + context.data["assetsShared"] = dict() + + projectdata = context.data["projectEntity"]["data"] + selection = resolve.get_current_track_items( + filter=True, selecting_color="Pink") + + for clip_data in selection: + data = dict() + + # get basic objects form data + project = clip_data["project"] + sequence = clip_data["sequence"] + clip = clip_data["clip"] + + # sequence attrs + sq_frame_start = sequence.GetStartFrame() + self.log.debug(f"sq_frame_start: {sq_frame_start}") + + sq_markers = sequence.GetMarkers() + + # get details of objects + clip_item = clip["item"] + track = clip_data["track"] + + mp = project.GetMediaPool() + + # get clip attributes + clip_metadata = resolve.get_pype_clip_metadata(clip_item) + clip_metadata = json.loads(clip_metadata) + self.log.debug(f"clip_metadata: {clip_metadata}") + + compound_source_prop = clip_metadata["sourceProperties"] + self.log.debug(f"compound_source_prop: {compound_source_prop}") + + asset_name = clip_item.GetName() + mp_item = clip_item.GetMediaPoolItem() + mp_prop = mp_item.GetClipProperty() + source_first = int(compound_source_prop["Start"]) + source_last = int(compound_source_prop["End"]) + source_duration = compound_source_prop["Frames"] + fps = float(mp_prop["FPS"]) + self.log.debug(f"source_first: {source_first}") + self.log.debug(f"source_last: {source_last}") + self.log.debug(f"source_duration: {source_duration}") + self.log.debug(f"fps: {fps}") + + source_path = os.path.normpath( + compound_source_prop["File Path"]) + source_name = compound_source_prop["File Name"] + source_id = clip_metadata["sourceId"] + self.log.debug(f"source_path: {source_path}") + self.log.debug(f"source_name: {source_name}") + self.log.debug(f"source_id: {source_id}") + + clip_left_offset = int(clip_item.GetLeftOffset()) + clip_right_offset = int(clip_item.GetRightOffset()) + self.log.debug(f"clip_left_offset: {clip_left_offset}") + self.log.debug(f"clip_right_offset: {clip_right_offset}") + + # source in/out + source_in = int(source_first + clip_left_offset) + source_out = int(source_first + clip_right_offset) + self.log.debug(f"source_in: {source_in}") + self.log.debug(f"source_out: {source_out}") + + clip_in = int(clip_item.GetStart() - sq_frame_start) + clip_out = int(clip_item.GetEnd() - sq_frame_start) + clip_duration = int(clip_item.GetDuration()) + self.log.debug(f"clip_in: {clip_in}") + self.log.debug(f"clip_out: {clip_out}") + self.log.debug(f"clip_duration: {clip_duration}") + + is_sequence = False + + self.log.debug( + "__ assets_shared: {}".format( + context.data["assetsShared"])) + + # Check for clips with the same range + # this is for testing if any vertically neighbouring + # clips has been already processed + clip_matching_with_range = next( + (k for k, v in context.data["assetsShared"].items() + if (v.get("_clipIn", 0) == clip_in) + and (v.get("_clipOut", 0) == clip_out) + ), False) + + # check if clip name is the same in matched + # vertically neighbouring clip + # if it is then it is correct and resent variable to False + # not to be rised wrong name exception + if asset_name in str(clip_matching_with_range): + clip_matching_with_range = False + + # rise wrong name exception if found one + assert (not clip_matching_with_range), ( + "matching clip: {asset}" + " timeline range ({clip_in}:{clip_out})" + " conflicting with {clip_matching_with_range}" + " >> rename any of clips to be the same as the other <<" + ).format( + **locals()) + + if ("[" in source_name) and ("]" in source_name): + is_sequence = True + + data.update({ + "name": "_".join([ + track["name"], asset_name, source_name]), + "item": clip_item, + "source": mp_item, + # "timecodeStart": str(source.timecodeStart()), + "timelineStart": sq_frame_start, + "sourcePath": source_path, + "sourceFileHead": source_name, + "isSequence": is_sequence, + "track": track["name"], + "trackIndex": track["index"], + "sourceFirst": source_first, + + "sourceIn": source_in, + "sourceOut": source_out, + "mediaDuration": source_duration, + "clipIn": clip_in, + "clipOut": clip_out, + "clipDuration": clip_duration, + "asset": asset_name, + "subset": "plateMain", + "family": "clip", + "families": [], + "handleStart": projectdata.get("handleStart", 0), + "handleEnd": projectdata.get("handleEnd", 0)}) + + instance = context.create_instance(**data) + + self.log.info("Created instance: {}".format(instance)) + self.log.info("Created instance.data: {}".format(instance.data)) + + context.data["assetsShared"][asset_name] = { + "_clipIn": clip_in, + "_clipOut": clip_out + } + self.log.info( + "context.data[\"assetsShared\"]: {}".format( + context.data["assetsShared"])) diff --git a/pype/plugins/resolve/publish/collect_host.py b/pype/plugins/resolve/publish/collect_host.py deleted file mode 100644 index a5c4b0936c..0000000000 --- a/pype/plugins/resolve/publish/collect_host.py +++ /dev/null @@ -1,17 +0,0 @@ -import pyblish.api -from pype.hosts.resolve.utils import get_resolve_module - - -class CollectProject(pyblish.api.ContextPlugin): - """Collect Project object""" - - order = pyblish.api.CollectorOrder - 0.1 - label = "Collect Project" - hosts = ["resolve"] - - def process(self, context): - resolve = get_resolve_module() - PM = resolve.GetProjectManager() - P = PM.GetCurrentProject() - - self.log.info(P.GetName()) diff --git a/pype/plugins/resolve/publish/collect_project.py b/pype/plugins/resolve/publish/collect_project.py new file mode 100644 index 0000000000..aa57f93619 --- /dev/null +++ b/pype/plugins/resolve/publish/collect_project.py @@ -0,0 +1,29 @@ +import os +import pyblish.api +from pype.hosts.resolve.utils import get_resolve_module + + +class CollectProject(pyblish.api.ContextPlugin): + """Collect Project object""" + + order = pyblish.api.CollectorOrder - 0.1 + label = "Collect Project" + hosts = ["resolve"] + + def process(self, context): + exported_projet_ext = ".drp" + current_dir = os.getenv("AVALON_WORKDIR") + resolve = get_resolve_module() + PM = resolve.GetProjectManager() + P = PM.GetCurrentProject() + name = P.GetName() + + fname = name + exported_projet_ext + current_file = os.path.join(current_dir, fname) + normalised = os.path.normpath(current_file) + + context.data["project"] = P + context.data["currentFile"] = normalised + + self.log.info(name) + self.log.debug(normalised) diff --git a/pype/plugins/standalonepublisher/publish/collect_context.py b/pype/plugins/standalonepublisher/publish/collect_context.py index a5479fdf13..9dbeec93fb 100644 --- a/pype/plugins/standalonepublisher/publish/collect_context.py +++ b/pype/plugins/standalonepublisher/publish/collect_context.py @@ -123,7 +123,7 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): "label": subset, "name": subset, "family": in_data["family"], - "version": in_data.get("version", 1), + # "version": in_data.get("version", 1), "frameStart": in_data.get("representations", [None])[0].get( "frameStart", None ), diff --git a/pype/plugins/standalonepublisher/publish/collect_editorial.py b/pype/plugins/standalonepublisher/publish/collect_editorial.py index a31125d9a8..5e6fd106e4 100644 --- a/pype/plugins/standalonepublisher/publish/collect_editorial.py +++ b/pype/plugins/standalonepublisher/publish/collect_editorial.py @@ -32,7 +32,7 @@ class CollectEditorial(pyblish.api.InstancePlugin): actions = [] # presets - extensions = [".mov"] + extensions = [".mov", ".mp4"] def process(self, instance): # remove context test attribute diff --git a/pype/plugins/standalonepublisher/publish/collect_psd_instances.py b/pype/plugins/standalonepublisher/publish/collect_psd_instances.py index 9c8e2eae83..b5db437473 100644 --- a/pype/plugins/standalonepublisher/publish/collect_psd_instances.py +++ b/pype/plugins/standalonepublisher/publish/collect_psd_instances.py @@ -9,7 +9,7 @@ class CollectPsdInstances(pyblish.api.InstancePlugin): """ label = "Collect Psd Instances" - order = pyblish.api.CollectorOrder + 0.492 + order = pyblish.api.CollectorOrder + 0.489 hosts = ["standalonepublisher"] families = ["background_batch"] @@ -34,8 +34,6 @@ class CollectPsdInstances(pyblish.api.InstancePlugin): context = instance.context asset_data = instance.data["assetEntity"] asset_name = instance.data["asset"] - anatomy_data = instance.data["anatomyData"] - for subset_name, subset_data in self.subsets.items(): instance_name = f"{asset_name}_{subset_name}" task = subset_data.get("task", "background") @@ -55,16 +53,8 @@ class CollectPsdInstances(pyblish.api.InstancePlugin): new_instance.data["label"] = f"{instance_name}" new_instance.data["subset"] = subset_name + new_instance.data["task"] = task - # fix anatomy data - anatomy_data_new = copy.deepcopy(anatomy_data) - # updating hierarchy data - anatomy_data_new.update({ - "asset": asset_data["name"], - "task": task, - "subset": subset_name - }) - new_instance.data["anatomyData"] = anatomy_data_new if subset_name in self.unchecked_by_default: new_instance.data["publish"] = False diff --git a/pype/resources/app_icons/maya.png b/pype/resources/app_icons/maya.png index e84a6a3742..95c605f50d 100644 Binary files a/pype/resources/app_icons/maya.png and b/pype/resources/app_icons/maya.png differ diff --git a/pype/scripts/otio_burnin.py b/pype/scripts/otio_burnin.py index 718943855c..156896a759 100644 --- a/pype/scripts/otio_burnin.py +++ b/pype/scripts/otio_burnin.py @@ -526,7 +526,7 @@ def burnins_from_data( bit_rate = burnin._streams[0].get("bit_rate") if bit_rate: - ffmpeg_args.append("--b:v {}".format(bit_rate)) + ffmpeg_args.append("-b:v {}".format(bit_rate)) pix_fmt = burnin._streams[0].get("pix_fmt") if pix_fmt: diff --git a/pype/settings/__init__.py b/pype/settings/__init__.py new file mode 100644 index 0000000000..7e73d541a4 --- /dev/null +++ b/pype/settings/__init__.py @@ -0,0 +1,9 @@ +from .lib import ( + system_settings, + project_settings +) + +__all__ = ( + "system_settings", + "project_settings" +) diff --git a/pype/settings/defaults/project_anatomy/colorspace.json b/pype/settings/defaults/project_anatomy/colorspace.json new file mode 100644 index 0000000000..8b934f810d --- /dev/null +++ b/pype/settings/defaults/project_anatomy/colorspace.json @@ -0,0 +1,42 @@ +{ + "nuke": { + "root": { + "colorManagement": "Nuke", + "OCIO_config": "nuke-default", + "defaultViewerLUT": "Nuke Root LUTs", + "monitorLut": "sRGB", + "int8Lut": "sRGB", + "int16Lut": "sRGB", + "logLut": "Cineon", + "floatLut": "linear" + }, + "viewer": { + "viewerProcess": "sRGB" + }, + "write": { + "render": { + "colorspace": "linear" + }, + "prerender": { + "colorspace": "linear" + }, + "still": { + "colorspace": "sRGB" + } + }, + "read": { + "[^-a-zA-Z0-9]beauty[^-a-zA-Z0-9]": "linear", + "[^-a-zA-Z0-9](P|N|Z|crypto)[^-a-zA-Z0-9]": "linear", + "[^-a-zA-Z0-9](plateRef)[^-a-zA-Z0-9]": "sRGB" + } + }, + "maya": { + + }, + "houdini": { + + }, + "resolve": { + + } +} diff --git a/pype/settings/defaults/project_anatomy/dataflow.json b/pype/settings/defaults/project_anatomy/dataflow.json new file mode 100644 index 0000000000..d2f470b5bc --- /dev/null +++ b/pype/settings/defaults/project_anatomy/dataflow.json @@ -0,0 +1,55 @@ +{ + "nuke": { + "nodes": { + "connected": true, + "modifymetadata": { + "_id": "connect_metadata", + "_previous": "ENDING", + "metadata.set.pype_studio_name": "{PYPE_STUDIO_NAME}", + "metadata.set.avalon_project_name": "{AVALON_PROJECT}", + "metadata.set.avalon_project_code": "{PYPE_STUDIO_CODE}", + "metadata.set.avalon_asset_name": "{AVALON_ASSET}" + }, + "crop": { + "_id": "connect_crop", + "_previous": "connect_metadata", + "box": [ + "{metadata.crop.x}", + "{metadata.crop.y}", + "{metadata.crop.right}", + "{metadata.crop.top}" + ] + }, + "write": { + "render": { + "_id": "output_write", + "_previous": "connect_crop", + "file_type": "exr", + "datatype": "16 bit half", + "compression": "Zip (1 scanline)", + "autocrop": true, + "tile_color": "0xff0000ff", + "channels": "rgb" + }, + "prerender": { + "_id": "output_write", + "_previous": "connect_crop", + "file_type": "exr", + "datatype": "16 bit half", + "compression": "Zip (1 scanline)", + "autocrop": false, + "tile_color": "0xc9892aff", + "channels": "rgba" + }, + "still": { + "_previous": "connect_crop", + "channels": "rgba", + "file_type": "tiff", + "datatype": "16 bit", + "compression": "LZW", + "tile_color": "0x4145afff" + } + } + } + } +} diff --git a/pype/settings/defaults/project_anatomy/roots.json b/pype/settings/defaults/project_anatomy/roots.json new file mode 100644 index 0000000000..0282471a60 --- /dev/null +++ b/pype/settings/defaults/project_anatomy/roots.json @@ -0,0 +1,5 @@ +{ + "windows": "C:/projects", + "linux": "/mnt/share/projects", + "darwin": "/Volumes/path" +} diff --git a/pype/settings/defaults/project_anatomy/templates.json b/pype/settings/defaults/project_anatomy/templates.json new file mode 100644 index 0000000000..0fff0265b3 --- /dev/null +++ b/pype/settings/defaults/project_anatomy/templates.json @@ -0,0 +1,30 @@ +{ + "version_padding": 3, + "version": "v{version:0>{@version_padding}}", + "frame_padding": 4, + "frame": "{frame:0>{@frame_padding}}", + "work": { + "folder": "{root}/{project[name]}/{hierarchy}/{asset}/work/{task}", + "file": "{project[code]}_{asset}_{task}_{@version}<_{comment}>.{ext}", + "path": "{@folder}/{@file}" + }, + "render": { + "folder": "{root}/{project[name]}/{hierarchy}/{asset}/publish/render/{subset}/{@version}", + "file": "{project[code]}_{asset}_{subset}_{@version}<_{output}><.{@frame}>.{representation}", + "path": "{@folder}/{@file}" + }, + "texture": { + "path": "{root}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}" + }, + "publish": { + "folder": "{root}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/{@version}", + "file": "{project[code]}_{asset}_{subset}_{@version}<_{output}><.{@frame}>.{representation}", + "path": "{@folder}/{@file}", + "thumbnail": "{thumbnail_root}/{project[name]}/{_id}_{thumbnail_type}{ext}" + }, + "master": { + "folder": "{root}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/master", + "file": "{project[code]}_{asset}_{subset}_master<_{output}><.{frame}>.{representation}", + "path": "{@folder}/{@file}" + } +} \ No newline at end of file diff --git a/pype/settings/defaults/project_settings/ftrack/ftrack_config.json b/pype/settings/defaults/project_settings/ftrack/ftrack_config.json new file mode 100644 index 0000000000..1ef3a9d69f --- /dev/null +++ b/pype/settings/defaults/project_settings/ftrack/ftrack_config.json @@ -0,0 +1,16 @@ +{ + "sync_to_avalon": { + "statuses_name_change": ["not ready", "ready"] + }, + + "status_update": { + "_ignore_": ["in progress", "ommited", "on hold"], + "Ready": ["not ready"], + "In Progress" : ["_any_"] + }, + "status_version_to_task": { + "__description__": "Status `from` (key) must be lowered!", + "in progress": "in progress", + "approved": "approved" + } +} diff --git a/pype/settings/defaults/project_settings/ftrack/ftrack_custom_attributes.json b/pype/settings/defaults/project_settings/ftrack/ftrack_custom_attributes.json new file mode 100644 index 0000000000..f03d473cd0 --- /dev/null +++ b/pype/settings/defaults/project_settings/ftrack/ftrack_custom_attributes.json @@ -0,0 +1,165 @@ +[{ + "label": "FPS", + "key": "fps", + "type": "number", + "is_hierarchical": true, + "group": "avalon", + "write_security_role": ["ALL"], + "read_security_role": ["ALL"], + "default": null, + "config": { + "isdecimal": true + } +}, { + "label": "Applications", + "key": "applications", + "type": "enumerator", + "entity_type": "show", + "group": "avalon", + "config": { + "multiselect": true, + "data": [ + {"blender_2.80": "Blender 2.80"}, + {"blender_2.81": "Blender 2.81"}, + {"blender_2.82": "Blender 2.82"}, + {"blender_2.83": "Blender 2.83"}, + {"celaction_local": "CelAction2D Local"}, + {"maya_2017": "Maya 2017"}, + {"maya_2018": "Maya 2018"}, + {"maya_2019": "Maya 2019"}, + {"nuke_10.0": "Nuke 10.0"}, + {"nuke_11.2": "Nuke 11.2"}, + {"nuke_11.3": "Nuke 11.3"}, + {"nuke_12.0": "Nuke 12.0"}, + {"nukex_10.0": "NukeX 10.0"}, + {"nukex_11.2": "NukeX 11.2"}, + {"nukex_11.3": "NukeX 11.3"}, + {"nukex_12.0": "NukeX 12.0"}, + {"nukestudio_10.0": "NukeStudio 10.0"}, + {"nukestudio_11.2": "NukeStudio 11.2"}, + {"nukestudio_11.3": "NukeStudio 11.3"}, + {"nukestudio_12.0": "NukeStudio 12.0"}, + {"harmony_17": "Harmony 17"}, + {"houdini_16.5": "Houdini 16.5"}, + {"houdini_17": "Houdini 17"}, + {"houdini_18": "Houdini 18"}, + {"photoshop_2020": "Photoshop 2020"}, + {"python_3": "Python 3"}, + {"python_2": "Python 2"}, + {"premiere_2019": "Premiere Pro 2019"}, + {"premiere_2020": "Premiere Pro 2020"}, + {"resolve_16": "BM DaVinci Resolve 16"} + ] + } +}, { + "label": "Avalon auto-sync", + "key": "avalon_auto_sync", + "type": "boolean", + "entity_type": "show", + "group": "avalon", + "write_security_role": ["API", "Administrator"], + "read_security_role": ["API", "Administrator"] +}, { + "label": "Intent", + "key": "intent", + "type": "enumerator", + "entity_type": "assetversion", + "group": "avalon", + "config": { + "multiselect": false, + "data": [ + {"test": "Test"}, + {"wip": "WIP"}, + {"final": "Final"} + ] + } +}, { + "label": "Library Project", + "key": "library_project", + "type": "boolean", + "entity_type": "show", + "group": "avalon", + "write_security_role": ["API", "Administrator"], + "read_security_role": ["API", "Administrator"] +}, { + "label": "Clip in", + "key": "clipIn", + "type": "number", + "is_hierarchical": true, + "group": "avalon", + "default": null +}, { + "label": "Clip out", + "key": "clipOut", + "type": "number", + "is_hierarchical": true, + "group": "avalon", + "default": null +}, { + "label": "Frame start", + "key": "frameStart", + "type": "number", + "is_hierarchical": true, + "group": "avalon", + "default": null +}, { + "label": "Frame end", + "key": "frameEnd", + "type": "number", + "is_hierarchical": true, + "group": "avalon", + "default": null +}, { + "label": "Tools", + "key": "tools_env", + "type": "enumerator", + "is_hierarchical": true, + "group": "avalon", + "config": { + "multiselect": true, + "data": [ + {"mtoa_3.0.1": "mtoa_3.0.1"}, + {"mtoa_3.1.1": "mtoa_3.1.1"}, + {"mtoa_3.2.0": "mtoa_3.2.0"}, + {"yeti_2.1.2": "yeti_2.1"} + ] + } +}, { + "label": "Resolution Width", + "key": "resolutionWidth", + "type": "number", + "is_hierarchical": true, + "group": "avalon", + "default": null +}, { + "label": "Resolution Height", + "key": "resolutionHeight", + "type": "number", + "is_hierarchical": true, + "group": "avalon", + "default": null +}, { + "label": "Pixel aspect", + "key": "pixelAspect", + "type": "number", + "is_hierarchical": true, + "group": "avalon", + "config": { + "isdecimal": true + } +}, { + "label": "Frame handles start", + "key": "handleStart", + "type": "number", + "is_hierarchical": true, + "group": "avalon", + "default": null +}, { + "label": "Frame handles end", + "key": "handleEnd", + "type": "number", + "is_hierarchical": true, + "group": "avalon", + "default": null +} +] diff --git a/pype/settings/defaults/project_settings/ftrack/partnership_ftrack_cred.json b/pype/settings/defaults/project_settings/ftrack/partnership_ftrack_cred.json new file mode 100644 index 0000000000..6b3a32f181 --- /dev/null +++ b/pype/settings/defaults/project_settings/ftrack/partnership_ftrack_cred.json @@ -0,0 +1,5 @@ +{ + "server_url": "", + "api_key": "", + "api_user": "" +} diff --git a/pype/settings/defaults/project_settings/ftrack/plugins/server.json b/pype/settings/defaults/project_settings/ftrack/plugins/server.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/pype/settings/defaults/project_settings/ftrack/plugins/server.json @@ -0,0 +1 @@ +{} diff --git a/pype/settings/defaults/project_settings/ftrack/plugins/user.json b/pype/settings/defaults/project_settings/ftrack/plugins/user.json new file mode 100644 index 0000000000..1ba8e9b511 --- /dev/null +++ b/pype/settings/defaults/project_settings/ftrack/plugins/user.json @@ -0,0 +1,5 @@ +{ + "TestAction": { + "ignore_me": true + } +} diff --git a/pype/settings/defaults/project_settings/ftrack/project_defaults.json b/pype/settings/defaults/project_settings/ftrack/project_defaults.json new file mode 100644 index 0000000000..a4e3aa3362 --- /dev/null +++ b/pype/settings/defaults/project_settings/ftrack/project_defaults.json @@ -0,0 +1,18 @@ +{ + "fps": 25, + "frameStart": 1001, + "frameEnd": 1100, + "clipIn": 1001, + "clipOut": 1100, + "handleStart": 10, + "handleEnd": 10, + + "resolutionHeight": 1080, + "resolutionWidth": 1920, + "pixelAspect": 1.0, + "applications": [ + "maya_2019", "nuke_11.3", "nukex_11.3", "nukestudio_11.3", "deadline" + ], + "tools_env": [], + "avalon_auto_sync": true +} diff --git a/pype/settings/defaults/project_settings/global/creator.json b/pype/settings/defaults/project_settings/global/creator.json new file mode 100644 index 0000000000..d14e779f01 --- /dev/null +++ b/pype/settings/defaults/project_settings/global/creator.json @@ -0,0 +1,8 @@ +{ + "Model": ["model"], + "Render Globals": ["light", "render"], + "Layout": ["layout"], + "Set Dress": ["setdress"], + "Look": ["look"], + "Rig": ["rigging"] +} diff --git a/pype/settings/defaults/project_settings/global/project_folder_structure.json b/pype/settings/defaults/project_settings/global/project_folder_structure.json new file mode 100644 index 0000000000..83bd5f12a9 --- /dev/null +++ b/pype/settings/defaults/project_settings/global/project_folder_structure.json @@ -0,0 +1,22 @@ +{ + "__project_root__": { + "prod" : {}, + "resources" : { + "footage": { + "plates": {}, + "offline": {} + }, + "audio": {}, + "art_dept": {} + }, + "editorial" : {}, + "assets[ftrack.Library]": { + "characters[ftrack]": {}, + "locations[ftrack]": {} + }, + "shots[ftrack.Sequence]": { + "scripts": {}, + "editorial[ftrack.Folder]": {} + } + } +} diff --git a/pype/settings/defaults/project_settings/global/sw_folders.json b/pype/settings/defaults/project_settings/global/sw_folders.json new file mode 100644 index 0000000000..a154935dce --- /dev/null +++ b/pype/settings/defaults/project_settings/global/sw_folders.json @@ -0,0 +1,8 @@ +{ + "compositing": ["nuke", "ae"], + "modeling": ["maya", "app2"], + "lookdev": ["substance"], + "animation": [], + "lighting": [], + "rigging": [] +} diff --git a/pype/settings/defaults/project_settings/global/workfiles.json b/pype/settings/defaults/project_settings/global/workfiles.json new file mode 100644 index 0000000000..393b2e3c10 --- /dev/null +++ b/pype/settings/defaults/project_settings/global/workfiles.json @@ -0,0 +1,7 @@ +{ + "last_workfile_on_startup": [ + { + "enabled": false + } + ] +} diff --git a/pype/settings/defaults/project_settings/maya/capture.json b/pype/settings/defaults/project_settings/maya/capture.json new file mode 100644 index 0000000000..b6c4893034 --- /dev/null +++ b/pype/settings/defaults/project_settings/maya/capture.json @@ -0,0 +1,108 @@ +{ + "Codec": { + "compression": "jpg", + "format": "image", + "quality": 95 + }, + "Display Options": { + "background": [ + 0.7137254901960784, + 0.7137254901960784, + 0.7137254901960784 + ], + "backgroundBottom": [ + 0.7137254901960784, + 0.7137254901960784, + 0.7137254901960784 + ], + "backgroundTop": [ + 0.7137254901960784, + 0.7137254901960784, + 0.7137254901960784 + ], + "override_display": true + }, + "Generic": { + "isolate_view": true, + "off_screen": true + }, + "IO": { + "name": "", + "open_finished": false, + "raw_frame_numbers": false, + "recent_playblasts": [], + "save_file": false + }, + "PanZoom": { + "pan_zoom": true + }, + "Renderer": { + "rendererName": "vp2Renderer" + }, + "Resolution": { + "height": 1080, + "mode": "Custom", + "percent": 1.0, + "width": 1920 + }, + "Time Range": { + "end_frame": 25, + "frame": "", + "start_frame": 0, + "time": "Time Slider" + }, + "Viewport Options": { + "cameras": false, + "clipGhosts": false, + "controlVertices": false, + "deformers": false, + "dimensions": false, + "displayLights": 0, + "dynamicConstraints": false, + "dynamics": false, + "fluids": false, + "follicles": false, + "gpuCacheDisplayFilter": false, + "greasePencils": false, + "grid": false, + "hairSystems": false, + "handles": false, + "high_quality": true, + "hud": false, + "hulls": false, + "ikHandles": false, + "imagePlane": false, + "joints": false, + "lights": false, + "locators": false, + "manipulators": false, + "motionTrails": false, + "nCloths": false, + "nParticles": false, + "nRigids": false, + "nurbsCurves": false, + "nurbsSurfaces": false, + "override_viewport_options": true, + "particleInstancers": false, + "pivots": false, + "planes": false, + "pluginShapes": false, + "polymeshes": true, + "shadows": false, + "strokes": false, + "subdivSurfaces": false, + "textures": false, + "twoSidedLighting": true + }, + "Camera Options": { + "displayGateMask": false, + "displayResolution": false, + "displayFilmGate": false, + "displayFieldChart": false, + "displaySafeAction": false, + "displaySafeTitle": false, + "displayFilmPivot": false, + "displayFilmOrigin": false, + "overscan": 1.0 + } +} diff --git a/pype/settings/defaults/project_settings/muster/templates_mapping.json b/pype/settings/defaults/project_settings/muster/templates_mapping.json new file mode 100644 index 0000000000..4edab9077d --- /dev/null +++ b/pype/settings/defaults/project_settings/muster/templates_mapping.json @@ -0,0 +1,19 @@ +{ + "3delight": 41, + "arnold": 46, + "arnold_sf": 57, + "gelato": 30, + "harware": 3, + "krakatoa": 51, + "file_layers": 7, + "mentalray": 2, + "mentalray_sf": 6, + "redshift": 55, + "renderman": 29, + "software": 1, + "software_sf": 5, + "turtle": 10, + "vector": 4, + "vray": 37, + "ffmpeg": 48 +} diff --git a/pype/settings/defaults/project_settings/plugins/celaction/publish.json b/pype/settings/defaults/project_settings/plugins/celaction/publish.json new file mode 100644 index 0000000000..fd1af23d84 --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/celaction/publish.json @@ -0,0 +1,11 @@ +{ + "ExtractCelactionDeadline": { + "enabled": true, + "deadline_department": "", + "deadline_priority": 50, + "deadline_pool": "", + "deadline_pool_secondary": "", + "deadline_group": "", + "deadline_chunk_size": 10 + } +} \ No newline at end of file diff --git a/pype/settings/defaults/project_settings/plugins/config.json b/pype/settings/defaults/project_settings/plugins/config.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/config.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pype/settings/defaults/project_settings/plugins/ftrack/publish.json b/pype/settings/defaults/project_settings/plugins/ftrack/publish.json new file mode 100644 index 0000000000..d8d93a36ee --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/ftrack/publish.json @@ -0,0 +1,7 @@ +{ + "IntegrateFtrackNote": { + "enabled": false, + "note_with_intent_template": "{intent}: {comment}", + "note_labels": [] + } +} \ No newline at end of file diff --git a/pype/settings/defaults/project_settings/plugins/global/create.json b/pype/settings/defaults/project_settings/plugins/global/create.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/global/create.json @@ -0,0 +1 @@ +{} diff --git a/pype/settings/defaults/project_settings/plugins/global/filter.json b/pype/settings/defaults/project_settings/plugins/global/filter.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/global/filter.json @@ -0,0 +1 @@ +{} diff --git a/pype/settings/defaults/project_settings/plugins/global/load.json b/pype/settings/defaults/project_settings/plugins/global/load.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/global/load.json @@ -0,0 +1 @@ +{} diff --git a/pype/settings/defaults/project_settings/plugins/global/publish.json b/pype/settings/defaults/project_settings/plugins/global/publish.json new file mode 100644 index 0000000000..0a7f6fbf3d --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/global/publish.json @@ -0,0 +1,98 @@ +{ + "IntegrateMasterVersion": { + "enabled": false + }, + "ExtractJpegEXR": { + "enabled": true, + "ffmpeg_args": { + "input": [ + "-gamma 2.2" + ], + "output": [] + } + }, + "ExtractReview": { + "enabled": true, + "profiles": [ + { + "families": [], + "hosts": [], + "outputs": { + "h264": { + "ext": "mp4", + "tags": [ + "burnin", + "ftrackreview" + ], + "ffmpeg_args": { + "video_filters": [], + "audio_filters": [], + "input": [ + "-gamma 2.2" + ], + "output": [ + "-pix_fmt yuv420p", + "-crf 18", + "-intra" + ] + }, + "filter": { + "families": [ + "render", + "review", + "ftrack" + ] + } + } + } + } + ] + }, + "ExtractBurnin": { + "enabled": false, + "options": { + "font_size": 42, + "opacity": 1, + "bg_opacity": 0, + "x_offset": 5, + "y_offset": 5, + "bg_padding": 5 + }, + "fields": {}, + "profiles": [ + { + "burnins": { + "burnin": { + "TOP_LEFT": "{yy}-{mm}-{dd}", + "TOP_RIGHT": "{anatomy[version]}", + "TOP_CENTERED": "", + "BOTTOM_RIGHT": "{frame_start}-{current_frame}-{frame_end}", + "BOTTOM_CENTERED": "{asset}", + "BOTTOM_LEFT": "{username}" + } + } + } + ] + }, + "IntegrateAssetNew": { + "template_name_profiles": { + "publish": { + "families": [], + "tasks": [] + }, + "render": { + "families": [ + "review", + "render", + "prerender" + ] + } + } + }, + "ProcessSubmittedJobOnFarm": { + "enabled": false, + "deadline_department": "", + "deadline_pool": "", + "deadline_group": "" + } +} \ No newline at end of file diff --git a/pype/settings/defaults/project_settings/plugins/maya/create.json b/pype/settings/defaults/project_settings/plugins/maya/create.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/maya/create.json @@ -0,0 +1 @@ +{} diff --git a/pype/settings/defaults/project_settings/plugins/maya/filter.json b/pype/settings/defaults/project_settings/plugins/maya/filter.json new file mode 100644 index 0000000000..83d6f05f31 --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/maya/filter.json @@ -0,0 +1,9 @@ +{ + "Preset n1": { + "ValidateNoAnimation": false, + "ValidateShapeDefaultNames": false + }, + "Preset n2": { + "ValidateNoAnimation": false + } +} diff --git a/pype/settings/defaults/project_settings/plugins/maya/load.json b/pype/settings/defaults/project_settings/plugins/maya/load.json new file mode 100644 index 0000000000..260fbb35ee --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/maya/load.json @@ -0,0 +1,18 @@ +{ + "colors": { + "model": [0.821, 0.518, 0.117], + "rig": [0.144, 0.443, 0.463], + "pointcache": [0.368, 0.821, 0.117], + "animation": [0.368, 0.821, 0.117], + "ass": [1.0, 0.332, 0.312], + "camera": [0.447, 0.312, 1.0], + "fbx": [1.0, 0.931, 0.312], + "mayaAscii": [0.312, 1.0, 0.747], + "setdress": [0.312, 1.0, 0.747], + "layout": [0.312, 1.0, 0.747], + "vdbcache": [0.312, 1.0, 0.428], + "vrayproxy": [0.258, 0.95, 0.541], + "yeticache": [0.2, 0.8, 0.3], + "yetiRig": [0, 0.8, 0.5] + } +} diff --git a/pype/settings/defaults/project_settings/plugins/maya/publish.json b/pype/settings/defaults/project_settings/plugins/maya/publish.json new file mode 100644 index 0000000000..2b3637ff80 --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/maya/publish.json @@ -0,0 +1,17 @@ +{ + "ValidateModelName": { + "enabled": false, + "material_file": "/path/to/shader_name_definition.txt", + "regex": "(.*)_(\\d)*_(?P.*)_(GEO)" + }, + "ValidateAssemblyName": { + "enabled": false + }, + "ValidateShaderName": { + "enabled": false, + "regex": "(?P.*)_(.*)_SHD" + }, + "ValidateMeshHasOverlappingUVs": { + "enabled": false + } +} \ No newline at end of file diff --git a/pype/settings/defaults/project_settings/plugins/maya/workfile_build.json b/pype/settings/defaults/project_settings/plugins/maya/workfile_build.json new file mode 100644 index 0000000000..443bc2cb2c --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/maya/workfile_build.json @@ -0,0 +1,136 @@ +[ + { + "tasks": [ + "lighting" + ], + "current_context": [ + { + "subset_name_filters": [ + ".+[Mm]ain" + ], + "families": [ + "model" + ], + "repre_names": [ + "abc", + "ma" + ], + "loaders": [ + "ReferenceLoader" + ] + }, + { + "families": [ + "animation", + "pointcache" + ], + "repre_names": [ + "abc" + ], + "loaders": [ + "ReferenceLoader" + ] + }, + { + "families": [ + "rendersetup" + ], + "repre_names": [ + "json" + ], + "loaders": [ + "RenderSetupLoader" + ] + }, + { + "families": [ + "camera" + ], + "repre_names": [ + "abc" + ], + "loaders": [ + "ReferenceLoader" + ] + } + ], + "linked_assets": [ + { + "families": [ + "setdress" + ], + "repre_names": [ + "ma" + ], + "loaders": [ + "ReferenceLoader" + ] + }, + { + "families": [ + "ass" + ], + "repre_names": [ + "ass" + ], + "loaders": [ + "assLoader" + ] + } + ] + }, + { + "tasks": [ + "animation" + ], + "current_context": [ + { + "families": [ + "camera" + ], + "repre_names": [ + "abc", + "ma" + ], + "loaders": [ + "ReferenceLoader" + ] + }, + { + "families": [ + "audio" + ], + "repre_names": [ + "wav" + ], + "loaders": [ + "RenderSetupLoader" + ] + } + ], + "linked_assets": [ + { + "families": [ + "setdress" + ], + "repre_names": [ + "proxy" + ], + "loaders": [ + "ReferenceLoader" + ] + }, + { + "families": [ + "rig" + ], + "repre_names": [ + "ass" + ], + "loaders": [ + "rigLoader" + ] + } + ] + } +] \ No newline at end of file diff --git a/pype/settings/defaults/project_settings/plugins/nuke/create.json b/pype/settings/defaults/project_settings/plugins/nuke/create.json new file mode 100644 index 0000000000..79ab665696 --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/nuke/create.json @@ -0,0 +1,8 @@ +{ + "CreateWriteRender": { + "fpath_template": "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}" + }, + "CreateWritePrerender": { + "fpath_template": "{work}/prerenders/nuke/{subset}/{subset}.{frame}.{ext}" + } +} \ No newline at end of file diff --git a/pype/settings/defaults/project_settings/plugins/nuke/load.json b/pype/settings/defaults/project_settings/plugins/nuke/load.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/nuke/load.json @@ -0,0 +1 @@ +{} diff --git a/pype/settings/defaults/project_settings/plugins/nuke/publish.json b/pype/settings/defaults/project_settings/plugins/nuke/publish.json new file mode 100644 index 0000000000..08a099a0a0 --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/nuke/publish.json @@ -0,0 +1,53 @@ +{ + "ExtractThumbnail": { + "enabled": true, + "nodes": { + "Reformat": [ + [ + "type", + "to format" + ], + [ + "format", + "HD_1080" + ], + [ + "filter", + "Lanczos6" + ], + [ + "black_outside", + true + ], + [ + "pbb", + false + ] + ] + } + }, + "ValidateNukeWriteKnobs": { + "enabled": false, + "knobs": { + "render": { + "review": true + } + } + }, + "ExtractReviewDataLut": { + "enabled": false + }, + "ExtractReviewDataMov": { + "enabled": true, + "viewer_lut_raw": false + }, + "ExtractSlateFrame": { + "viewer_lut_raw": false + }, + "NukeSubmitDeadline": { + "deadline_priority": 50, + "deadline_pool": "", + "deadline_pool_secondary": "", + "deadline_chunk_size": 1 + } +} \ No newline at end of file diff --git a/pype/settings/defaults/project_settings/plugins/nuke/workfile_build.json b/pype/settings/defaults/project_settings/plugins/nuke/workfile_build.json new file mode 100644 index 0000000000..4b48b46184 --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/nuke/workfile_build.json @@ -0,0 +1,23 @@ +[ + { + "tasks": [ + "compositing" + ], + "current_context": [ + { + "families": [ + "render", + "plate" + ], + "repre_names": [ + "exr", + "dpx" + ], + "loaders": [ + "LoadSequence" + ] + } + ], + "linked_assets": [] + } +] \ No newline at end of file diff --git a/pype/settings/defaults/project_settings/plugins/nukestudio/filter.json b/pype/settings/defaults/project_settings/plugins/nukestudio/filter.json new file mode 100644 index 0000000000..bd6a0dc1bd --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/nukestudio/filter.json @@ -0,0 +1,10 @@ +{ + "strict": { + "ValidateVersion": true, + "VersionUpWorkfile": true + }, + "benevolent": { + "ValidateVersion": false, + "VersionUpWorkfile": false + } +} \ No newline at end of file diff --git a/pype/settings/defaults/project_settings/plugins/nukestudio/publish.json b/pype/settings/defaults/project_settings/plugins/nukestudio/publish.json new file mode 100644 index 0000000000..d99a878c35 --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/nukestudio/publish.json @@ -0,0 +1,9 @@ +{ + "CollectInstanceVersion": { + "enabled": false + }, + "ExtractReviewCutUpVideo": { + "enabled": true, + "tags_addition": [] + } +} \ No newline at end of file diff --git a/pype/settings/defaults/project_settings/plugins/resolve/create.json b/pype/settings/defaults/project_settings/plugins/resolve/create.json new file mode 100644 index 0000000000..8ff5b15714 --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/resolve/create.json @@ -0,0 +1,7 @@ +{ + "CreateShotClip": { + "clipName": "{track}{sequence}{shot}", + "folder": "takes", + "steps": 20 + } +} \ No newline at end of file diff --git a/pype/settings/defaults/project_settings/plugins/standalonepublisher/publish.json b/pype/settings/defaults/project_settings/plugins/standalonepublisher/publish.json new file mode 100644 index 0000000000..2f1a3e7aca --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/standalonepublisher/publish.json @@ -0,0 +1,27 @@ +{ + "ExtractThumbnailSP": { + "ffmpeg_args": { + "input": [ + "-gamma 2.2" + ], + "output": [] + } + }, + "ExtractReviewSP": { + "outputs": { + "h264": { + "input": [ + "-gamma 2.2" + ], + "output": [ + "-pix_fmt yuv420p", + "-crf 18" + ], + "tags": [ + "preview" + ], + "ext": "mov" + } + } + } +} \ No newline at end of file diff --git a/pype/settings/defaults/project_settings/plugins/test/create.json b/pype/settings/defaults/project_settings/plugins/test/create.json new file mode 100644 index 0000000000..fa0b2fc05f --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/test/create.json @@ -0,0 +1,8 @@ +{ + "MyTestCreator": { + "my_test_property": "B", + "active": false, + "new_property": "new", + "family": "new_family" + } +} diff --git a/pype/settings/defaults/project_settings/plugins/test/publish.json b/pype/settings/defaults/project_settings/plugins/test/publish.json new file mode 100644 index 0000000000..3180dd5d8a --- /dev/null +++ b/pype/settings/defaults/project_settings/plugins/test/publish.json @@ -0,0 +1,10 @@ +{ + "MyTestPlugin": { + "label": "loaded from preset", + "optional": true, + "families": ["changed", "by", "preset"] + }, + "MyTestRemovedPlugin": { + "enabled": false + } +} diff --git a/pype/settings/defaults/project_settings/premiere/asset_default.json b/pype/settings/defaults/project_settings/premiere/asset_default.json new file mode 100644 index 0000000000..84d2bde3d8 --- /dev/null +++ b/pype/settings/defaults/project_settings/premiere/asset_default.json @@ -0,0 +1,5 @@ +{ + "frameStart": 1001, + "handleStart": 0, + "handleEnd": 0 +} diff --git a/pype/settings/defaults/project_settings/premiere/rules_tasks.json b/pype/settings/defaults/project_settings/premiere/rules_tasks.json new file mode 100644 index 0000000000..333c9cd70b --- /dev/null +++ b/pype/settings/defaults/project_settings/premiere/rules_tasks.json @@ -0,0 +1,21 @@ +{ + "defaultTasks": ["Layout", "Animation"], + "taskToSubsets": { + "Layout": ["reference", "audio"], + "Animation": ["audio"] + }, + "subsetToRepresentations": { + "reference": { + "preset": "h264", + "representation": "mp4" + }, + "thumbnail": { + "preset": "jpeg_thumb", + "representation": "jpg" + }, + "audio": { + "preset": "48khz", + "representation": "wav" + } + } +} diff --git a/pype/settings/defaults/project_settings/standalonepublisher/families.json b/pype/settings/defaults/project_settings/standalonepublisher/families.json new file mode 100644 index 0000000000..d05941cc26 --- /dev/null +++ b/pype/settings/defaults/project_settings/standalonepublisher/families.json @@ -0,0 +1,90 @@ +{ + "create_look": { + "name": "look", + "label": "Look", + "family": "look", + "icon": "paint-brush", + "defaults": ["Main"], + "help": "Shader connections defining shape look" + }, + "create_model": { + "name": "model", + "label": "Model", + "family": "model", + "icon": "cube", + "defaults": ["Main", "Proxy", "Sculpt"], + "help": "Polygonal static geometry" + }, + "create_workfile": { + "name": "workfile", + "label": "Workfile", + "family": "workfile", + "icon": "cube", + "defaults": ["Main"], + "help": "Working scene backup" + }, + "create_camera": { + "name": "camera", + "label": "Camera", + "family": "camera", + "icon": "video-camera", + "defaults": ["Main"], + "help": "Single baked camera" + }, + "create_pointcache": { + "name": "pointcache", + "label": "Pointcache", + "family": "pointcache", + "icon": "gears", + "defaults": ["Main"], + "help": "Alembic pointcache for animated data" + }, + "create_rig": { + "name": "rig", + "label": "Rig", + "family": "rig", + "icon": "wheelchair", + "defaults": ["Main"], + "help": "Artist-friendly rig with controls" + }, + "create_layout": { + "name": "layout", + "label": "Layout", + "family": "layout", + "icon": "cubes", + "defaults": ["Main"], + "help": "Simple scene for animators with camera" + }, + "create_plate": { + "name": "plate", + "label": "Plate", + "family": "plate", + "icon": "camera", + "defaults": ["Main", "BG", "Reference"], + "help": "Plates for compositors" + }, + "create_matchmove": { + "name": "matchmove", + "label": "Matchmove script", + "family": "matchmove", + "icon": "empire", + "defaults": ["Camera", "Object", "Mocap"], + "help": "Script exported from matchmoving application" + }, + "create_images": { + "name": "image", + "label": "Image file", + "family": "image", + "icon": "image", + "defaults": ["ConceptArt", "Reference", "Texture", "MattePaint"], + "help": "Holder for all kinds of image data" + }, + "create_editorial": { + "name": "editorial", + "label": "Editorial", + "family": "editorial", + "icon": "image", + "defaults": ["Main"], + "help": "Editorial files to generate shots." + } +} diff --git a/pype/settings/defaults/project_settings/tools/slates/example_HD.json b/pype/settings/defaults/project_settings/tools/slates/example_HD.json new file mode 100644 index 0000000000..b06391fb63 --- /dev/null +++ b/pype/settings/defaults/project_settings/tools/slates/example_HD.json @@ -0,0 +1,212 @@ +{ + "width": 1920, + "height": 1080, + "destination_path": "{destination_path}", + "style": { + "*": { + "font-family": "arial", + "font-color": "#ffffff", + "font-bold": false, + "font-italic": false, + "bg-color": "#0077ff", + "alignment-horizontal": "left", + "alignment-vertical": "top" + }, + "layer": { + "padding": 0, + "margin": 0 + }, + "rectangle": { + "padding": 0, + "margin": 0, + "bg-color": "#E9324B", + "fill": true + }, + "main_frame": { + "padding": 0, + "margin": 0, + "bg-color": "#252525" + }, + "table": { + "padding": 0, + "margin": 0, + "bg-color": "transparent" + }, + "table-item": { + "padding": 5, + "padding-bottom": 10, + "margin": 0, + "bg-color": "#212121", + "bg-alter-color": "#272727", + "font-color": "#dcdcdc", + "font-bold": false, + "font-italic": false, + "alignment-horizontal": "left", + "alignment-vertical": "top", + "word-wrap": false, + "ellide": true, + "max-lines": 1 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989", + "font-bold": true, + "ellide": false, + "word-wrap": true, + "max-lines": null + }, + "table-item-col[1]": { + "font-size": 40, + "padding-left": 10 + }, + "#colorbar": { + "bg-color": "#9932CC" + } + }, + "items": [{ + "type": "layer", + "direction": 1, + "name": "MainLayer", + "style": { + "#MainLayer": { + "width": 1094, + "height": 1000, + "margin": 25, + "padding": 0 + }, + "#LeftSide": { + "margin-right": 25 + } + }, + "items": [{ + "type": "layer", + "name": "LeftSide", + "items": [{ + "type": "layer", + "direction": 1, + "style": { + "table-item": { + "bg-color": "transparent", + "padding-bottom": 20 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989", + "alignment-horizontal": "right" + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "font-bold": true, + "font-size": 40 + } + }, + "items": [{ + "type": "table", + "values": [ + ["Show:", "{project[name]}"] + ], + "style": { + "table-item-field[0:0]": { + "width": 150 + }, + "table-item-field[0:1]": { + "width": 580 + } + } + }, { + "type": "table", + "values": [ + ["Submitting For:", "{intent}"] + ], + "style": { + "table-item-field[0:0]": { + "width": 160 + }, + "table-item-field[0:1]": { + "width": 218, + "alignment-horizontal": "right" + } + } + }] + }, { + "type": "rectangle", + "style": { + "bg-color": "#bc1015", + "width": 1108, + "height": 5, + "fill": true + } + }, { + "type": "table", + "use_alternate_color": true, + "values": [ + ["Version name:", "{version_name}"], + ["Date:", "{date}"], + ["Shot Types:", "{shot_type}"], + ["Submission Note:", "{submission_note}"] + ], + "style": { + "table-item": { + "padding-bottom": 20 + }, + "table-item-field[0:1]": { + "font-bold": true + }, + "table-item-field[3:0]": { + "word-wrap": true, + "ellide": true, + "max-lines": 4 + }, + "table-item-col[0]": { + "alignment-horizontal": "right", + "width": 150 + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "width": 958 + } + } + }] + }, { + "type": "layer", + "name": "RightSide", + "items": [{ + "type": "placeholder", + "name": "thumbnail", + "path": "{thumbnail_path}", + "style": { + "width": 730, + "height": 412 + } + }, { + "type": "placeholder", + "name": "colorbar", + "path": "{color_bar_path}", + "return_data": true, + "style": { + "width": 730, + "height": 55 + } + }, { + "type": "table", + "use_alternate_color": true, + "values": [ + ["Vendor:", "{vendor}"], + ["Shot Name:", "{shot_name}"], + ["Frames:", "{frame_start} - {frame_end} ({duration})"] + ], + "style": { + "table-item-col[0]": { + "alignment-horizontal": "left", + "width": 200 + }, + "table-item-col[1]": { + "alignment-horizontal": "right", + "width": 530, + "font-size": 30 + } + } + }] + }] + }] +} diff --git a/pype/settings/defaults/project_settings/unreal/project_setup.json b/pype/settings/defaults/project_settings/unreal/project_setup.json new file mode 100644 index 0000000000..8a4dffc526 --- /dev/null +++ b/pype/settings/defaults/project_settings/unreal/project_setup.json @@ -0,0 +1,4 @@ +{ + "dev_mode": false, + "install_unreal_python_engine": false +} diff --git a/pype/settings/defaults/system_settings/environments/avalon.json b/pype/settings/defaults/system_settings/environments/avalon.json new file mode 100644 index 0000000000..832ba07e71 --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/avalon.json @@ -0,0 +1,16 @@ +{ + "AVALON_CONFIG": "pype", + "AVALON_PROJECTS": "{PYPE_PROJECTS_PATH}", + "AVALON_USERNAME": "avalon", + "AVALON_PASSWORD": "secret", + "AVALON_DEBUG": "1", + "AVALON_MONGO": "mongodb://localhost:2707", + "AVALON_DB": "avalon", + "AVALON_DB_DATA": "{PYPE_SETUP_PATH}/../mongo_db_data", + "AVALON_EARLY_ADOPTER": "1", + "AVALON_SCHEMA": "{PYPE_MODULE_ROOT}/schema", + "AVALON_LOCATION": "http://127.0.0.1", + "AVALON_LABEL": "Pype", + "AVALON_TIMEOUT": "1000", + "AVALON_THUMBNAIL_ROOT": "" +} diff --git a/pype/settings/defaults/system_settings/environments/blender.json b/pype/settings/defaults/system_settings/environments/blender.json new file mode 100644 index 0000000000..6f4f6a012d --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/blender.json @@ -0,0 +1,7 @@ +{ + "BLENDER_USER_SCRIPTS": "{PYPE_SETUP_PATH}/repos/avalon-core/setup/blender", + "PYTHONPATH": [ + "{PYPE_SETUP_PATH}/repos/avalon-core/setup/blender", + "{PYTHONPATH}" + ] +} diff --git a/pype/settings/defaults/system_settings/environments/celaction.json b/pype/settings/defaults/system_settings/environments/celaction.json new file mode 100644 index 0000000000..cdd4e609ab --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/celaction.json @@ -0,0 +1,3 @@ +{ + "CELACTION_TEMPLATE": "{PYPE_MODULE_ROOT}/pype/hosts/celaction/celaction_template_scene.scn" +} diff --git a/pype/settings/defaults/system_settings/environments/deadline.json b/pype/settings/defaults/system_settings/environments/deadline.json new file mode 100644 index 0000000000..e8ef52805b --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/deadline.json @@ -0,0 +1,3 @@ +{ + "DEADLINE_REST_URL": "http://localhost:8082" +} diff --git a/pype/settings/defaults/system_settings/environments/ftrack.json b/pype/settings/defaults/system_settings/environments/ftrack.json new file mode 100644 index 0000000000..4f25de027b --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/ftrack.json @@ -0,0 +1,18 @@ +{ + "FTRACK_SERVER": "https://pype.ftrackapp.com", + "FTRACK_ACTIONS_PATH": [ + "{PYPE_MODULE_ROOT}/pype/modules/ftrack/actions" + ], + "FTRACK_EVENTS_PATH": [ + "{PYPE_MODULE_ROOT}/pype/modules/ftrack/events" + ], + "PYTHONPATH": [ + "{PYPE_MODULE_ROOT}/pype/vendor", + "{PYTHONPATH}" + ], + "PYBLISHPLUGINPATH": [ + "{PYPE_MODULE_ROOT}/pype/plugins/ftrack/publish" + ], + "FTRACK_EVENTS_MONGO_DB": "pype", + "FTRACK_EVENTS_MONGO_COL": "ftrack_events" +} diff --git a/pype/settings/defaults/system_settings/environments/global.json b/pype/settings/defaults/system_settings/environments/global.json new file mode 100644 index 0000000000..ef528e6857 --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/global.json @@ -0,0 +1,44 @@ +{ + "PYPE_STUDIO_NAME": "Studio Name", + "PYPE_STUDIO_CODE": "stu", + "PYPE_APP_ROOT": "{PYPE_SETUP_PATH}/pypeapp", + "PYPE_MODULE_ROOT": "{PYPE_SETUP_PATH}/repos/pype", + "PYPE_PROJECT_PLUGINS": "", + "STUDIO_SOFT": "{PYP_SETUP_ROOT}/soft", + "FFMPEG_PATH": { + "windows": "{VIRTUAL_ENV}/localized/ffmpeg_exec/windows/bin;{PYPE_SETUP_PATH}/vendor/ffmpeg_exec/windows/bin", + "darwin": "{VIRTUAL_ENV}/localized/ffmpeg_exec/darwin/bin:{PYPE_SETUP_PATH}/vendor/ffmpeg_exec/darwin/bin", + "linux": "{VIRTUAL_ENV}/localized/ffmpeg_exec/linux:{PYPE_SETUP_PATH}/vendor/ffmpeg_exec/linux" + }, + "DJV_PATH": { + "windows": [ + "C:/Program Files/djv-1.1.0-Windows-64/bin/djv_view.exe", + "C:/Program Files/DJV/bin/djv_view.exe", + "{STUDIO_SOFT}/djv/windows/bin/djv_view.exe" + ], + "linux": [ + "usr/local/djv/djv_view", + "{STUDIO_SOFT}/djv/linux/bin/djv_view" + ], + "darwin": "Application/DJV.app/Contents/MacOS/DJV" + }, + "PATH": [ + "{PYPE_CONFIG}/launchers", + "{PYPE_APP_ROOT}", + "{FFMPEG_PATH}", + "{PATH}" + ], + "PYPE_OCIO_CONFIG": "{STUDIO_SOFT}/OpenColorIO-Configs", + "PYTHONPATH": { + "windows": "{VIRTUAL_ENV}/Lib/site-packages;{PYPE_MODULE_ROOT}/pype/tools;{PYTHONPATH}", + "linux": "{VIRTUAL_ENV}/lib/python{PYTHON_VERSION}/site-packages:{PYPE_MODULE_ROOT}/pype/tools:{PYTHONPATH}", + "darwin": "{VIRTUAL_ENV}/lib/python{PYTHON_VERSION}/site-packages:{PYPE_MODULE_ROOT}/pype/tools:{PYTHONPATH}" + }, + "PYPE_PROJECT_CONFIGS": "{PYPE_SETUP_PATH}/../studio-project-configs", + "PYPE_PYTHON_EXE": { + "windows": "{VIRTUAL_ENV}/Scripts/python.exe", + "linux": "{VIRTUAL_ENV}/Scripts/python", + "darwin": "{VIRTUAL_ENV}/bin/python" + }, + "PYBLISH_GUI": "pyblish_pype" +} diff --git a/pype/settings/defaults/system_settings/environments/harmony.json b/pype/settings/defaults/system_settings/environments/harmony.json new file mode 100644 index 0000000000..d394343935 --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/harmony.json @@ -0,0 +1,4 @@ +{ + "AVALON_HARMONY_WORKFILES_ON_LAUNCH": "1", + "PYBLISH_GUI_ALWAYS_EXEC": "1" +} diff --git a/pype/settings/defaults/system_settings/environments/houdini.json b/pype/settings/defaults/system_settings/environments/houdini.json new file mode 100644 index 0000000000..95c7d19088 --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/houdini.json @@ -0,0 +1,12 @@ +{ + "HOUDINI_PATH": { + "darwin": "{PYPE_MODULE_ROOT}/setup/houdini:&", + "linux": "{PYPE_MODULE_ROOT}/setup/houdini:&", + "windows": "{PYPE_MODULE_ROOT}/setup/houdini;&" + }, + "HOUDINI_MENU_PATH": { + "darwin": "{PYPE_MODULE_ROOT}/setup/houdini:&", + "linux": "{PYPE_MODULE_ROOT}/setup/houdini:&", + "windows": "{PYPE_MODULE_ROOT}/setup/houdini;&" + } +} diff --git a/pype/settings/defaults/system_settings/environments/maya.json b/pype/settings/defaults/system_settings/environments/maya.json new file mode 100644 index 0000000000..7785b108f7 --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/maya.json @@ -0,0 +1,14 @@ +{ + "PYTHONPATH": [ + "{PYPE_SETUP_PATH}/repos/avalon-core/setup/maya", + "{PYPE_SETUP_PATH}/repos/maya-look-assigner", + "{PYTHON_ENV}/python2/Lib/site-packages", + "{PYTHONPATH}" + ], + "MAYA_DISABLE_CLIC_IPM": "Yes", + "MAYA_DISABLE_CIP": "Yes", + "MAYA_DISABLE_CER": "Yes", + "PYMEL_SKIP_MEL_INIT": "Yes", + "LC_ALL": "C", + "PYPE_LOG_NO_COLORS": "Yes" +} diff --git a/pype/settings/defaults/system_settings/environments/maya_2018.json b/pype/settings/defaults/system_settings/environments/maya_2018.json new file mode 100644 index 0000000000..72a0c57ce3 --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/maya_2018.json @@ -0,0 +1,11 @@ +{ + "MAYA_VERSION": "2018", + "MAYA_LOCATION": { + "darwin": "/Applications/Autodesk/maya{MAYA_VERSION}/Maya.app/Contents", + "linux": "/usr/autodesk/maya{MAYA_VERSION}", + "windows": "C:/Program Files/Autodesk/Maya{MAYA_VERSION}" + }, + "DYLD_LIBRARY_PATH": { + "darwin": "{MAYA_LOCATION}/MacOS" + } +} diff --git a/pype/settings/defaults/system_settings/environments/maya_2020.json b/pype/settings/defaults/system_settings/environments/maya_2020.json new file mode 100644 index 0000000000..efd0250bc8 --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/maya_2020.json @@ -0,0 +1,11 @@ +{ + "MAYA_VERSION": "2020", + "MAYA_LOCATION": { + "darwin": "/Applications/Autodesk/maya{MAYA_VERSION}/Maya.app/Contents", + "linux": "/usr/autodesk/maya{MAYA_VERSION}", + "windows": "C:/Program Files/Autodesk/Maya{MAYA_VERSION}" + }, + "DYLD_LIBRARY_PATH": { + "darwin": "{MAYA_LOCATION}/MacOS" + } +} diff --git a/pype/settings/defaults/system_settings/environments/mayabatch.json b/pype/settings/defaults/system_settings/environments/mayabatch.json new file mode 100644 index 0000000000..7785b108f7 --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/mayabatch.json @@ -0,0 +1,14 @@ +{ + "PYTHONPATH": [ + "{PYPE_SETUP_PATH}/repos/avalon-core/setup/maya", + "{PYPE_SETUP_PATH}/repos/maya-look-assigner", + "{PYTHON_ENV}/python2/Lib/site-packages", + "{PYTHONPATH}" + ], + "MAYA_DISABLE_CLIC_IPM": "Yes", + "MAYA_DISABLE_CIP": "Yes", + "MAYA_DISABLE_CER": "Yes", + "PYMEL_SKIP_MEL_INIT": "Yes", + "LC_ALL": "C", + "PYPE_LOG_NO_COLORS": "Yes" +} diff --git a/pype/settings/defaults/system_settings/environments/mayabatch_2019.json b/pype/settings/defaults/system_settings/environments/mayabatch_2019.json new file mode 100644 index 0000000000..aa7360a943 --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/mayabatch_2019.json @@ -0,0 +1,11 @@ +{ + "MAYA_VERSION": "2019", + "MAYA_LOCATION": { + "darwin": "/Applications/Autodesk/maya{MAYA_VERSION}/Maya.app/Contents", + "linux": "/usr/autodesk/maya{MAYA_VERSION}", + "windows": "C:/Program Files/Autodesk/Maya{MAYA_VERSION}" + }, + "DYLD_LIBRARY_PATH": { + "darwin": "{MAYA_LOCATION}/MacOS" + } +} diff --git a/pype/settings/defaults/system_settings/environments/mtoa_3.1.1.json b/pype/settings/defaults/system_settings/environments/mtoa_3.1.1.json new file mode 100644 index 0000000000..f7b9f94d4e --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/mtoa_3.1.1.json @@ -0,0 +1,23 @@ +{ + "MTOA": "{PYPE_STUDIO_SOFTWARE}/arnold/mtoa_{MAYA_VERSION}_{MTOA_VERSION}", + "MTOA_VERSION": "3.1.1", + "MAYA_RENDER_DESC_PATH": "{MTOA}", + "MAYA_MODULE_PATH": "{MTOA}", + "ARNOLD_PLUGIN_PATH": "{MTOA}/shaders", + "MTOA_EXTENSIONS_PATH": { + "darwin": "{MTOA}/extensions", + "linux": "{MTOA}/extensions", + "windows": "{MTOA}/extensions" + }, + "MTOA_EXTENSIONS": { + "darwin": "{MTOA}/extensions", + "linux": "{MTOA}/extensions", + "windows": "{MTOA}/extensions" + }, + "DYLD_LIBRARY_PATH": { + "darwin": "{MTOA}/bin" + }, + "PATH": { + "windows": "{PATH};{MTOA}/bin" + } +} diff --git a/pype/settings/defaults/system_settings/environments/muster.json b/pype/settings/defaults/system_settings/environments/muster.json new file mode 100644 index 0000000000..26f311146a --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/muster.json @@ -0,0 +1,3 @@ +{ + "MUSTER_REST_URL": "http://127.0.0.1:9890" +} diff --git a/pype/settings/defaults/system_settings/environments/nuke.json b/pype/settings/defaults/system_settings/environments/nuke.json new file mode 100644 index 0000000000..50dd31ac91 --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/nuke.json @@ -0,0 +1,15 @@ +{ + "NUKE_PATH": [ + "{PYPE_SETUP_PATH}/repos/avalon-core/setup/nuke/nuke_path", + "{PYPE_MODULE_ROOT}/setup/nuke/nuke_path", + "{PYPE_STUDIO_PLUGINS}/nuke" + ], + "PATH": { + "windows": "C:/Program Files (x86)/QuickTime/QTSystem/;{PATH}" + }, + "PYPE_LOG_NO_COLORS": "True", + "PYTHONPATH": { + "windows": "{VIRTUAL_ENV}/Lib/site-packages;{PYTHONPATH}", + "linux": "{VIRTUAL_ENV}/lib/python3.6/site-packages:{PYTHONPATH}" + } +} diff --git a/pype/settings/defaults/system_settings/environments/nukestudio.json b/pype/settings/defaults/system_settings/environments/nukestudio.json new file mode 100644 index 0000000000..b05e2411f0 --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/nukestudio.json @@ -0,0 +1,11 @@ +{ + "HIERO_PLUGIN_PATH": [ + "{PYPE_MODULE_ROOT}/setup/nukestudio/hiero_plugin_path" + ], + "PATH": { + "windows": "C:/Program Files (x86)/QuickTime/QTSystem/;{PATH}" + }, + "WORKFILES_STARTUP": "0", + "TAG_ASSETBUILD_STARTUP": "0", + "PYPE_LOG_NO_COLORS": "True" +} diff --git a/pype/settings/defaults/system_settings/environments/nukestudio_10.0.json b/pype/settings/defaults/system_settings/environments/nukestudio_10.0.json new file mode 100644 index 0000000000..9bdcef53c9 --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/nukestudio_10.0.json @@ -0,0 +1,4 @@ +{ + "PYPE_LOG_NO_COLORS": "Yes", + "QT_PREFERRED_BINDING": "PySide" +} \ No newline at end of file diff --git a/pype/settings/defaults/system_settings/environments/nukex.json b/pype/settings/defaults/system_settings/environments/nukex.json new file mode 100644 index 0000000000..2b77f44076 --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/nukex.json @@ -0,0 +1,10 @@ +{ + "NUKE_PATH": [ + "{PYPE_SETUP_PATH}/repos/avalon-core/setup/nuke/nuke_path", + "{PYPE_MODULE_ROOT}/setup/nuke/nuke_path", + "{PYPE_STUDIO_PLUGINS}/nuke" + ], + "PATH": { + "windows": "C:/Program Files (x86)/QuickTime/QTSystem/;{PATH}" + } +} diff --git a/pype/settings/defaults/system_settings/environments/nukex_10.0.json b/pype/settings/defaults/system_settings/environments/nukex_10.0.json new file mode 100644 index 0000000000..9bdcef53c9 --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/nukex_10.0.json @@ -0,0 +1,4 @@ +{ + "PYPE_LOG_NO_COLORS": "Yes", + "QT_PREFERRED_BINDING": "PySide" +} \ No newline at end of file diff --git a/pype/settings/defaults/system_settings/environments/photoshop.json b/pype/settings/defaults/system_settings/environments/photoshop.json new file mode 100644 index 0000000000..2208a88665 --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/photoshop.json @@ -0,0 +1,4 @@ +{ + "AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH": "1", + "PYTHONPATH": "{PYTHONPATH}" +} diff --git a/pype/settings/defaults/system_settings/environments/premiere.json b/pype/settings/defaults/system_settings/environments/premiere.json new file mode 100644 index 0000000000..27dc5c564b --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/premiere.json @@ -0,0 +1,11 @@ +{ + "EXTENSIONS_PATH": { + "windows": "{USERPROFILE}/AppData/Roaming/Adobe/CEP/extensions", + "darvin": "{USER}/Library/Application Support/Adobe/CEP/extensions" + }, + "EXTENSIONS_CACHE_PATH": { + "windows": "{USERPROFILE}/AppData/Local/Temp/cep_cache", + "darvin": "{USER}/Library/Application Support/Adobe/CEP/cep_cache" + }, + "installed_zxp": "" +} diff --git a/pype/settings/defaults/system_settings/environments/resolve.json b/pype/settings/defaults/system_settings/environments/resolve.json new file mode 100644 index 0000000000..1ff197dd5a --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/resolve.json @@ -0,0 +1,40 @@ +{ + "RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR": [ + "{STUDIO_SOFT}/davinci_resolve/scripts/python" + ], + "RESOLVE_SCRIPT_API": { + "windows": "{PROGRAMDATA}/Blackmagic Design/DaVinci Resolve/Support/Developer/Scripting", + "darvin": "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting", + "linux": "/opt/resolve/Developer/Scripting" + }, + "RESOLVE_SCRIPT_LIB": { + "windows": "C:/Program Files/Blackmagic Design/DaVinci Resolve/fusionscript.dll", + "darvin": "/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so", + "linux": "/opt/resolve/libs/Fusion/fusionscript.so" + }, + "RESOLVE_UTILITY_SCRIPTS_DIR": { + "windows": "{PROGRAMDATA}/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Comp", + "darvin": "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Comp", + "linux": "/opt/resolve/Fusion/Scripts/Comp" + }, + "PYTHON36_RESOLVE": { + "windows": "{LOCALAPPDATA}/Programs/Python/Python36", + "darvin": "~/Library/Python/3.6/bin", + "linux": "/opt/Python/3.6/bin" + }, + "PYTHONPATH": [ + "{PYTHON36_RESOLVE}/Lib/site-packages", + "{VIRTUAL_ENV}/Lib/site-packages", + "{PYTHONPATH}", + "{RESOLVE_SCRIPT_API}/Modules", + "{PYTHONPATH}" + ], + "PATH": [ + "{PYTHON36_RESOLVE}", + "{PYTHON36_RESOLVE}/Scripts", + "{PATH}" + ], + "PRE_PYTHON_SCRIPT": "{PYPE_MODULE_ROOT}/pype/resolve/preload_console.py", + "PYPE_LOG_NO_COLORS": "True", + "RESOLVE_DEV": "True" +} diff --git a/pype/settings/defaults/system_settings/environments/storyboardpro.json b/pype/settings/defaults/system_settings/environments/storyboardpro.json new file mode 100644 index 0000000000..581ad4db45 --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/storyboardpro.json @@ -0,0 +1,4 @@ +{ + "AVALON_TOONBOOM_WORKFILES_ON_LAUNCH": "1", + "PYBLISH_LITE_ALWAYS_EXEC": "1" +} diff --git a/pype/settings/defaults/system_settings/environments/unreal_4.24.json b/pype/settings/defaults/system_settings/environments/unreal_4.24.json new file mode 100644 index 0000000000..8feeb0230f --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/unreal_4.24.json @@ -0,0 +1,5 @@ +{ + "AVALON_UNREAL_PLUGIN": "{PYPE_SETUP_PATH}/repos/avalon-unreal-integration", + "PYPE_LOG_NO_COLORS": "True", + "QT_PREFERRED_BINDING": "PySide" +} diff --git a/pype/settings/defaults/system_settings/environments/vray_4300.json b/pype/settings/defaults/system_settings/environments/vray_4300.json new file mode 100644 index 0000000000..3212188441 --- /dev/null +++ b/pype/settings/defaults/system_settings/environments/vray_4300.json @@ -0,0 +1,15 @@ +{ + "VRAY_VERSION": "43001", + "VRAY_ROOT": "C:/vray/vray_{VRAY_VERSION}", + "MAYA_RENDER_DESC_PATH": "{VRAY_ROOT}/maya_root/bin/rendererDesc", + "VRAY_FOR_MAYA2019_MAIN": "{VRAY_ROOT}/maya_vray", + "VRAY_FOR_MAYA2019_PLUGINS": "{VRAY_ROOT}/maya_vray/vrayplugins", + "VRAY_PLUGINS": "{VRAY_ROOT}/maya_vray/vrayplugins", + "VRAY_OSL_PATH_MAYA2019": "{VRAY_ROOT}/vray/opensl", + "PATH": "{VRAY_ROOT}/maya_root/bin;{PATH}", + "MAYA_PLUG_IN_PATH": "{VRAY_ROOT}/maya_vray/plug-ins", + "MAYA_SCRIPT_PATH": "{VRAY_ROOT}/maya_vray/scripts", + "PYTHONPATH": "{VRAY_ROOT}/maya_vray/scripts;{PYTHONPATH}", + "XBMLANGPATH": "{VRAY_ROOT}/maya_vray/icons;{XBMLANGPATH}", + "VRAY_AUTH_CLIENT_FILE_PATH": "{VRAY_ROOT}" +} diff --git a/pype/settings/defaults/system_settings/global/applications.json b/pype/settings/defaults/system_settings/global/applications.json new file mode 100644 index 0000000000..e85e5864d9 --- /dev/null +++ b/pype/settings/defaults/system_settings/global/applications.json @@ -0,0 +1,34 @@ +{ + "blender_2.80": false, + "blender_2.81": false, + "blender_2.82": false, + "blender_2.83": true, + "celaction_local": true, + "celaction_remote": true, + "harmony_17": true, + "maya_2017": false, + "maya_2018": false, + "maya_2019": true, + "maya_2020": true, + "nuke_10.0": false, + "nuke_11.2": false, + "nuke_11.3": true, + "nuke_12.0": true, + "nukex_10.0": false, + "nukex_11.2": false, + "nukex_11.3": true, + "nukex_12.0": true, + "nukestudio_10.0": false, + "nukestudio_11.2": false, + "nukestudio_11.3": true, + "nukestudio_12.0": true, + "houdini_16": false, + "houdini_16.5": false, + "houdini_17": false, + "houdini_18": true, + "premiere_2019": false, + "premiere_2020": true, + "resolve_16": true, + "storyboardpro_7": true, + "unreal_4.24": true +} \ No newline at end of file diff --git a/pype/settings/defaults/system_settings/global/general.json b/pype/settings/defaults/system_settings/global/general.json new file mode 100644 index 0000000000..bd501b06eb --- /dev/null +++ b/pype/settings/defaults/system_settings/global/general.json @@ -0,0 +1,4 @@ +{ + "studio_name": "", + "studio_code": "" +} \ No newline at end of file diff --git a/pype/settings/defaults/system_settings/global/intent.json b/pype/settings/defaults/system_settings/global/intent.json new file mode 100644 index 0000000000..844bd1b518 --- /dev/null +++ b/pype/settings/defaults/system_settings/global/intent.json @@ -0,0 +1,8 @@ +{ + "items": { + "wip": "WIP", + "test": "TEST", + "final": "FINAL" + }, + "default": "wip" +} \ No newline at end of file diff --git a/pype/settings/defaults/system_settings/global/modules.json b/pype/settings/defaults/system_settings/global/modules.json new file mode 100644 index 0000000000..9bd46602cf --- /dev/null +++ b/pype/settings/defaults/system_settings/global/modules.json @@ -0,0 +1,88 @@ +{ + "Avalon": { + "AVALON_MONGO": "mongodb://localhost:2707", + "AVALON_DB_DATA": "{PYPE_SETUP_PATH}/../mongo_db_data", + "AVALON_THUMBNAIL_ROOT": "{PYPE_SETUP_PATH}/../avalon_thumails" + }, + "Ftrack": { + "enabled": true, + "ftrack_server": "https://pype.ftrackapp.com", + "ftrack_actions_path": [], + "ftrack_events_path": [], + "FTRACK_EVENTS_MONGO_DB": "pype", + "FTRACK_EVENTS_MONGO_COL": "ftrack_events", + "sync_to_avalon": { + "statuses_name_change": [ + "ready", + "not ready" + ] + }, + "status_version_to_task": {}, + "status_update": { + "Ready": [ + "Not Ready" + ], + "In Progress": [ + "_any_" + ] + }, + "intent": { + "items": { + "-": "-", + "wip": "WIP", + "final": "Final", + "test": "Test" + }, + "default": "-" + } + }, + "Rest Api": { + "default_port": 8021, + "exclude_ports": [] + }, + "Timers Manager": { + "enabled": true, + "full_time": 15.0, + "message_time": 0.5 + }, + "Clockify": { + "enabled": true, + "workspace_name": "studio name" + }, + "Deadline": { + "enabled": true, + "DEADLINE_REST_URL": "http://localhost:8082" + }, + "Muster": { + "enabled": false, + "MUSTER_REST_URL": "", + "templates_mapping": { + "file_layers": 7, + "mentalray": 2, + "mentalray_sf": 6, + "redshift": 55, + "renderman": 29, + "software": 1, + "software_sf": 5, + "turtle": 10, + "vector": 4, + "vray": 37, + "ffmpeg": 48 + } + }, + "Logging": { + "enabled": true + }, + "Adobe Communicator": { + "enabled": true + }, + "User setting": { + "enabled": true + }, + "Standalone Publish": { + "enabled": true + }, + "Idle Manager": { + "enabled": true + } +} \ No newline at end of file diff --git a/pype/settings/defaults/system_settings/global/tools.json b/pype/settings/defaults/system_settings/global/tools.json new file mode 100644 index 0000000000..1f8c2ad1ea --- /dev/null +++ b/pype/settings/defaults/system_settings/global/tools.json @@ -0,0 +1,6 @@ +{ + "mtoa_3.0.1": false, + "mtoa_3.1.1": false, + "mtoa_3.2.0": true, + "yeti_2.1.2": true +} \ No newline at end of file diff --git a/pype/settings/defaults/system_settings/launchers/blender_2.80.toml b/pype/settings/defaults/system_settings/launchers/blender_2.80.toml new file mode 100644 index 0000000000..5fea78b7b0 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/blender_2.80.toml @@ -0,0 +1,7 @@ +application_dir = "blender" +executable = "blender_2.80" +schema = "avalon-core:application-1.0" +label = "Blender 2.80" +ftrack_label = "Blender" +icon ="blender" +ftrack_icon = '{}/app_icons/blender.png' diff --git a/pype/settings/defaults/system_settings/launchers/blender_2.81.toml b/pype/settings/defaults/system_settings/launchers/blender_2.81.toml new file mode 100644 index 0000000000..4f85ee5558 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/blender_2.81.toml @@ -0,0 +1,7 @@ +application_dir = "blender" +executable = "blender_2.81" +schema = "avalon-core:application-1.0" +label = "Blender 2.81" +ftrack_label = "Blender" +icon ="blender" +ftrack_icon = '{}/app_icons/blender.png' diff --git a/pype/settings/defaults/system_settings/launchers/blender_2.82.toml b/pype/settings/defaults/system_settings/launchers/blender_2.82.toml new file mode 100644 index 0000000000..840001452e --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/blender_2.82.toml @@ -0,0 +1,7 @@ +application_dir = "blender" +executable = "blender_2.82" +schema = "avalon-core:application-1.0" +label = "Blender 2.82" +ftrack_label = "Blender" +icon ="blender" +ftrack_icon = '{}/app_icons/blender.png' diff --git a/pype/settings/defaults/system_settings/launchers/blender_2.83.toml b/pype/settings/defaults/system_settings/launchers/blender_2.83.toml new file mode 100644 index 0000000000..7fc8bf87b9 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/blender_2.83.toml @@ -0,0 +1,7 @@ +application_dir = "blender" +executable = "blender_2.83" +schema = "avalon-core:application-1.0" +label = "Blender 2.83" +ftrack_label = "Blender" +icon ="blender" +ftrack_icon = '{}/app_icons/blender.png' diff --git a/pype/settings/defaults/system_settings/launchers/celaction_local.toml b/pype/settings/defaults/system_settings/launchers/celaction_local.toml new file mode 100644 index 0000000000..aef3548e08 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/celaction_local.toml @@ -0,0 +1,8 @@ +executable = "celaction_local" +schema = "avalon-core:application-1.0" +application_dir = "celaction" +label = "CelAction2D" +ftrack_label = "CelAction2D" +icon ="celaction_local" +launch_hook = "pype/hooks/celaction/prelaunch.py/CelactionPrelaunchHook" +ftrack_icon = '{}/app_icons/celaction_local.png' diff --git a/pype/settings/defaults/system_settings/launchers/celaction_publish.toml b/pype/settings/defaults/system_settings/launchers/celaction_publish.toml new file mode 100644 index 0000000000..86f4ae39e7 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/celaction_publish.toml @@ -0,0 +1,7 @@ +schema = "avalon-core:application-1.0" +application_dir = "shell" +executable = "celaction_publish" +label = "Shell" + +[environment] +CREATE_NEW_CONSOLE = "Yes" diff --git a/pype/settings/defaults/system_settings/launchers/darwin/blender_2.82 b/pype/settings/defaults/system_settings/launchers/darwin/blender_2.82 new file mode 100644 index 0000000000..8254411ea2 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/darwin/blender_2.82 @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +open -a blender $@ diff --git a/pype/settings/defaults/system_settings/launchers/darwin/harmony_17 b/pype/settings/defaults/system_settings/launchers/darwin/harmony_17 new file mode 100644 index 0000000000..b7eba2c2d0 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/darwin/harmony_17 @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +DIRNAME="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +set >~/environment.tmp +if [ $? -ne -0 ] ; then + echo "ERROR: cannot write to '~/environment.tmp'!" + read -n 1 -s -r -p "Press any key to exit" + return +fi +open -a Terminal.app "$DIRNAME/harmony_17_launch" diff --git a/pype/settings/defaults/system_settings/launchers/darwin/harmony_17_launch b/pype/settings/defaults/system_settings/launchers/darwin/harmony_17_launch new file mode 100644 index 0000000000..5dcf5db57e --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/darwin/harmony_17_launch @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +source ~/environment.tmp +export $(cut -d= -f1 ~/environment.tmp) +exe="/Applications/Toon Boom Harmony 17 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium" +$PYPE_PYTHON_EXE -c "import avalon.harmony;avalon.harmony.launch('$exe')" diff --git a/pype/settings/defaults/system_settings/launchers/darwin/python3 b/pype/settings/defaults/system_settings/launchers/darwin/python3 new file mode 100644 index 0000000000..c2b82c7638 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/darwin/python3 @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +open /usr/bin/python3 --args $@ diff --git a/pype/settings/defaults/system_settings/launchers/harmony_17.toml b/pype/settings/defaults/system_settings/launchers/harmony_17.toml new file mode 100644 index 0000000000..dbb76444a7 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/harmony_17.toml @@ -0,0 +1,8 @@ +application_dir = "harmony" +label = "Harmony 17" +ftrack_label = "Harmony" +schema = "avalon-core:application-1.0" +executable = "harmony_17" +description = "" +icon ="harmony_icon" +ftrack_icon = '{}/app_icons/harmony.png' diff --git a/pype/settings/defaults/system_settings/launchers/houdini_16.toml b/pype/settings/defaults/system_settings/launchers/houdini_16.toml new file mode 100644 index 0000000000..e29fa74cad --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/houdini_16.toml @@ -0,0 +1,7 @@ +executable = "houdini_16" +schema = "avalon-core:application-1.0" +application_dir = "houdini" +label = "Houdini 16" +ftrack_label = "Houdini" +icon = "houdini_icon" +ftrack_icon = '{}/app_icons/houdini.png' diff --git a/pype/settings/defaults/system_settings/launchers/houdini_17.toml b/pype/settings/defaults/system_settings/launchers/houdini_17.toml new file mode 100644 index 0000000000..5d01364330 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/houdini_17.toml @@ -0,0 +1,7 @@ +executable = "houdini_17" +schema = "avalon-core:application-1.0" +application_dir = "houdini" +label = "Houdini 17.0" +ftrack_label = "Houdini" +icon = "houdini_icon" +ftrack_icon = '{}/app_icons/houdini.png' diff --git a/pype/settings/defaults/system_settings/launchers/houdini_18.toml b/pype/settings/defaults/system_settings/launchers/houdini_18.toml new file mode 100644 index 0000000000..93b9a3334d --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/houdini_18.toml @@ -0,0 +1,7 @@ +executable = "houdini_18" +schema = "avalon-core:application-1.0" +application_dir = "houdini" +label = "Houdini 18" +ftrack_label = "Houdini" +icon = "houdini_icon" +ftrack_icon = '{}/app_icons/houdini.png' diff --git a/pype/settings/defaults/system_settings/launchers/linux/maya2016 b/pype/settings/defaults/system_settings/launchers/linux/maya2016 new file mode 100644 index 0000000000..98424304b1 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/linux/maya2016 @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +maya_path = "/usr/autodesk/maya2016/bin/maya" + +if [[ -z $PYPE_LOG_NO_COLORS ]]; then + $maya_path -file "$AVALON_LAST_WORKFILE" $@ +else + $maya_path $@ diff --git a/pype/settings/defaults/system_settings/launchers/linux/maya2017 b/pype/settings/defaults/system_settings/launchers/linux/maya2017 new file mode 100644 index 0000000000..7a2662a55e --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/linux/maya2017 @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +maya_path = "/usr/autodesk/maya2017/bin/maya" + +if [[ -z $AVALON_LAST_WORKFILE ]]; then + $maya_path -file "$AVALON_LAST_WORKFILE" $@ +else + $maya_path $@ diff --git a/pype/settings/defaults/system_settings/launchers/linux/maya2018 b/pype/settings/defaults/system_settings/launchers/linux/maya2018 new file mode 100644 index 0000000000..db832b3fe7 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/linux/maya2018 @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +maya_path = "/usr/autodesk/maya2018/bin/maya" + +if [[ -z $AVALON_LAST_WORKFILE ]]; then + $maya_path -file "$AVALON_LAST_WORKFILE" $@ +else + $maya_path $@ diff --git a/pype/settings/defaults/system_settings/launchers/linux/maya2019 b/pype/settings/defaults/system_settings/launchers/linux/maya2019 new file mode 100644 index 0000000000..8398734ab9 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/linux/maya2019 @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +maya_path = "/usr/autodesk/maya2019/bin/maya" + +if [[ -z $AVALON_LAST_WORKFILE ]]; then + $maya_path -file "$AVALON_LAST_WORKFILE" $@ +else + $maya_path $@ diff --git a/pype/settings/defaults/system_settings/launchers/linux/maya2020 b/pype/settings/defaults/system_settings/launchers/linux/maya2020 new file mode 100644 index 0000000000..18a1edd598 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/linux/maya2020 @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +maya_path = "/usr/autodesk/maya2020/bin/maya" + +if [[ -z $AVALON_LAST_WORKFILE ]]; then + $maya_path -file "$AVALON_LAST_WORKFILE" $@ +else + $maya_path $@ diff --git a/pype/settings/defaults/system_settings/launchers/linux/nuke11.3 b/pype/settings/defaults/system_settings/launchers/linux/nuke11.3 new file mode 100644 index 0000000000..b1c9a90d74 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/linux/nuke11.3 @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +gnome-terminal -e '/usr/local/Nuke11.3v5/Nuke11.3' diff --git a/pype/settings/defaults/system_settings/launchers/linux/nuke12.0 b/pype/settings/defaults/system_settings/launchers/linux/nuke12.0 new file mode 100644 index 0000000000..99ea1a6b0c --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/linux/nuke12.0 @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +gnome-terminal -e '/usr/local/Nuke12.0v1/Nuke12.0' diff --git a/pype/settings/defaults/system_settings/launchers/linux/nukestudio11.3 b/pype/settings/defaults/system_settings/launchers/linux/nukestudio11.3 new file mode 100644 index 0000000000..750d54a7d5 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/linux/nukestudio11.3 @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +gnome-terminal -e '/usr/local/Nuke11.3v5/Nuke11.3 --studio' diff --git a/pype/settings/defaults/system_settings/launchers/linux/nukestudio12.0 b/pype/settings/defaults/system_settings/launchers/linux/nukestudio12.0 new file mode 100644 index 0000000000..ba5cf654a8 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/linux/nukestudio12.0 @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +gnome-terminal -e '/usr/local/Nuke12.0v1/Nuke12.0 --studio' diff --git a/pype/settings/defaults/system_settings/launchers/linux/nukex11.3 b/pype/settings/defaults/system_settings/launchers/linux/nukex11.3 new file mode 100644 index 0000000000..d913e4b961 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/linux/nukex11.3 @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +gnome-terminal -e '/usr/local/Nuke11.3v5/Nuke11.3 -nukex' diff --git a/pype/settings/defaults/system_settings/launchers/linux/nukex12.0 b/pype/settings/defaults/system_settings/launchers/linux/nukex12.0 new file mode 100644 index 0000000000..da2721c48b --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/linux/nukex12.0 @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +gnome-terminal -e '/usr/local/Nuke12.0v1/Nuke12.0 -nukex' diff --git a/pype/settings/defaults/system_settings/launchers/maya_2016.toml b/pype/settings/defaults/system_settings/launchers/maya_2016.toml new file mode 100644 index 0000000000..d69c4effaf --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/maya_2016.toml @@ -0,0 +1,26 @@ +application_dir = "maya" +default_dirs = [ + "scenes", + "data", + "renderData/shaders", + "images" +] +label = "Autodesk Maya 2016x64" +ftrack_label = "Maya" +schema = "avalon-core:application-1.0" +executable = "maya2016" +description = "" +icon ="maya_icon" +ftrack_icon = '{}/app_icons/maya.png' + +[copy] +"{PYPE_MODULE_ROOT}/pype/resources/maya/workspace.mel" = "workspace.mel" + +[environment] +MAYA_DISABLE_CLIC_IPM = "Yes" # Disable the AdSSO process +MAYA_DISABLE_CIP = "Yes" # Shorten time to boot +MAYA_DISABLE_CER = "Yes" +PYTHONPATH = [ + "{AVALON_CORE}/setup/maya", + "{PYTHONPATH}" +] diff --git a/pype/settings/defaults/system_settings/launchers/maya_2017.toml b/pype/settings/defaults/system_settings/launchers/maya_2017.toml new file mode 100644 index 0000000000..2d1c35b530 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/maya_2017.toml @@ -0,0 +1,28 @@ +application_dir = "maya" +default_dirs = [ + "scenes", + "data", + "renderData/shaders", + "images" +] +label = "Autodesk Maya 2017" +ftrack_label = "Maya" +schema = "avalon-core:application-1.0" +executable = "maya2017" +description = "" +icon ="maya_icon" +ftrack_icon = '{}/app_icons/maya.png' + +[copy] +"{PYPE_MODULE_ROOT}/pype/resources/maya/workspace.mel" = "workspace.mel" + +[environment] +MAYA_DISABLE_CLIC_IPM = "Yes" # Disable the AdSSO process +MAYA_DISABLE_CIP = "Yes" # Shorten time to boot +MAYA_DISABLE_CER = "Yes" +PYMEL_SKIP_MEL_INIT = "Yes" +LC_ALL= "C" # Mute color management warnings +PYTHONPATH = [ + "{AVALON_CORE}/setup/maya", + "{PYTHONPATH}" +] diff --git a/pype/settings/defaults/system_settings/launchers/maya_2018.toml b/pype/settings/defaults/system_settings/launchers/maya_2018.toml new file mode 100644 index 0000000000..f180263fa2 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/maya_2018.toml @@ -0,0 +1,14 @@ +application_dir = "maya" +default_dirs = [ + "renders" +] +label = "Autodesk Maya 2018" +ftrack_label = "Maya" +schema = "avalon-core:application-1.0" +executable = "maya2018" +description = "" +icon ="maya_icon" +ftrack_icon = '{}/app_icons/maya.png' + +[copy] +"{PYPE_MODULE_ROOT}/pype/resources/maya/workspace.mel" = "workspace.mel" diff --git a/pype/settings/defaults/system_settings/launchers/maya_2019.toml b/pype/settings/defaults/system_settings/launchers/maya_2019.toml new file mode 100644 index 0000000000..7ec2cbcedd --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/maya_2019.toml @@ -0,0 +1,14 @@ +application_dir = "maya" +default_dirs = [ + "renders" +] +label = "Autodesk Maya 2019" +ftrack_label = "Maya" +schema = "avalon-core:application-1.0" +executable = "maya2019" +description = "" +icon ="maya_icon" +ftrack_icon = '{}/app_icons/maya.png' + +[copy] +"{PYPE_MODULE_ROOT}/pype/resources/maya/workspace.mel" = "workspace.mel" diff --git a/pype/settings/defaults/system_settings/launchers/maya_2020.toml b/pype/settings/defaults/system_settings/launchers/maya_2020.toml new file mode 100644 index 0000000000..49d84ef9a0 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/maya_2020.toml @@ -0,0 +1,14 @@ +application_dir = "maya" +default_dirs = [ + "renders" +] +label = "Autodesk Maya 2020" +ftrack_label = "Maya" +schema = "avalon-core:application-1.0" +executable = "maya2020" +description = "" +icon ="maya_icon" +ftrack_icon = '{}/app_icons/maya.png' + +[copy] +"{PYPE_MODULE_ROOT}/pype/resources/maya/workspace.mel" = "workspace.mel" diff --git a/pype/settings/defaults/system_settings/launchers/mayabatch_2019.toml b/pype/settings/defaults/system_settings/launchers/mayabatch_2019.toml new file mode 100644 index 0000000000..a928618d2b --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/mayabatch_2019.toml @@ -0,0 +1,17 @@ +application_dir = "maya" +default_dirs = [ + "scenes", + "data", + "renderData/shaders", + "images" +] +label = "Autodesk Maya 2019x64" +schema = "avalon-core:application-1.0" +executable = "mayabatch2019" +description = "" + +[environment] +PYTHONPATH = [ + "{AVALON_CORE}/setup/maya", + "{PYTHONPATH}" +] diff --git a/pype/settings/defaults/system_settings/launchers/mayabatch_2020.toml b/pype/settings/defaults/system_settings/launchers/mayabatch_2020.toml new file mode 100644 index 0000000000..cd1e1e4474 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/mayabatch_2020.toml @@ -0,0 +1,17 @@ +application_dir = "maya" +default_dirs = [ + "scenes", + "data", + "renderData/shaders", + "images" +] +label = "Autodesk Maya 2020x64" +schema = "avalon-core:application-1.0" +executable = "mayabatch2020" +description = "" + +[environment] +PYTHONPATH = [ + "{AVALON_CORE}/setup/maya", + "{PYTHONPATH}" +] diff --git a/pype/settings/defaults/system_settings/launchers/mayapy2016.toml b/pype/settings/defaults/system_settings/launchers/mayapy2016.toml new file mode 100644 index 0000000000..ad1e3dee86 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/mayapy2016.toml @@ -0,0 +1,17 @@ +application_dir = "maya" +default_dirs = [ + "scenes", + "data", + "renderData/shaders", + "images" +] +label = "Autodesk Maya 2016x64" +schema = "avalon-core:application-1.0" +executable = "mayapy2016" +description = "" + +[environment] +PYTHONPATH = [ + "{AVALON_CORE}/setup/maya", + "{PYTHONPATH}" +] diff --git a/pype/settings/defaults/system_settings/launchers/mayapy2017.toml b/pype/settings/defaults/system_settings/launchers/mayapy2017.toml new file mode 100644 index 0000000000..8d2095ff47 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/mayapy2017.toml @@ -0,0 +1,17 @@ +application_dir = "maya" +default_dirs = [ + "scenes", + "data", + "renderData/shaders", + "images" +] +label = "Autodesk Maya 2017x64" +schema = "avalon-core:application-1.0" +executable = "mayapy2017" +description = "" + +[environment] +PYTHONPATH = [ + "{AVALON_CORE}/setup/maya", + "{PYTHONPATH}" +] diff --git a/pype/settings/defaults/system_settings/launchers/mayapy2018.toml b/pype/settings/defaults/system_settings/launchers/mayapy2018.toml new file mode 100644 index 0000000000..597744fd85 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/mayapy2018.toml @@ -0,0 +1,17 @@ +application_dir = "maya" +default_dirs = [ + "scenes", + "data", + "renderData/shaders", + "images" +] +label = "Autodesk Maya 2018x64" +schema = "avalon-core:application-1.0" +executable = "mayapy2017" +description = "" + +[environment] +PYTHONPATH = [ + "{AVALON_CORE}/setup/maya", + "{PYTHONPATH}" +] diff --git a/pype/settings/defaults/system_settings/launchers/mayapy2019.toml b/pype/settings/defaults/system_settings/launchers/mayapy2019.toml new file mode 100644 index 0000000000..3c8a9860f9 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/mayapy2019.toml @@ -0,0 +1,17 @@ +application_dir = "maya" +default_dirs = [ + "scenes", + "data", + "renderData/shaders", + "images" +] +label = "Autodesk Maya 2019x64" +schema = "avalon-core:application-1.0" +executable = "mayapy2019" +description = "" + +[environment] +PYTHONPATH = [ + "{AVALON_CORE}/setup/maya", + "{PYTHONPATH}" +] diff --git a/pype/settings/defaults/system_settings/launchers/mayapy2020.toml b/pype/settings/defaults/system_settings/launchers/mayapy2020.toml new file mode 100644 index 0000000000..8f2d2e4a67 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/mayapy2020.toml @@ -0,0 +1,17 @@ +application_dir = "maya" +default_dirs = [ + "scenes", + "data", + "renderData/shaders", + "images" +] +label = "Autodesk Maya 2020x64" +schema = "avalon-core:application-1.0" +executable = "mayapy2020" +description = "" + +[environment] +PYTHONPATH = [ + "{AVALON_CORE}/setup/maya", + "{PYTHONPATH}" +] diff --git a/pype/settings/defaults/system_settings/launchers/myapp.toml b/pype/settings/defaults/system_settings/launchers/myapp.toml new file mode 100644 index 0000000000..21da0d52b2 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/myapp.toml @@ -0,0 +1,5 @@ +executable = "python" +schema = "avalon-core:application-1.0" +application_dir = "myapp" +label = "My App" +arguments = [ "-c", "import sys; from Qt import QtWidgets; if __name__ == '__main__':;\n app = QtWidgets.QApplication(sys.argv);\n window = QtWidgets.QWidget();\n window.setWindowTitle(\"My App\");\n window.resize(400, 300);\n window.show();\n app.exec_();\n",] \ No newline at end of file diff --git a/pype/settings/defaults/system_settings/launchers/nuke_10.0.toml b/pype/settings/defaults/system_settings/launchers/nuke_10.0.toml new file mode 100644 index 0000000000..2195fd3e82 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/nuke_10.0.toml @@ -0,0 +1,7 @@ +executable = "nuke10.0" +schema = "avalon-core:application-1.0" +application_dir = "nuke" +label = "Nuke 10.0v4" +ftrack_label = "Nuke" +icon ="nuke_icon" +ftrack_icon = '{}/app_icons/nuke.png' diff --git a/pype/settings/defaults/system_settings/launchers/nuke_11.0.toml b/pype/settings/defaults/system_settings/launchers/nuke_11.0.toml new file mode 100644 index 0000000000..0c981b479a --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/nuke_11.0.toml @@ -0,0 +1,7 @@ +executable = "nuke11.0" +schema = "avalon-core:application-1.0" +application_dir = "nuke" +label = "Nuke 11.0" +ftrack_label = "Nuke" +icon ="nuke_icon" +ftrack_icon = '{}/app_icons/nuke.png' diff --git a/pype/settings/defaults/system_settings/launchers/nuke_11.2.toml b/pype/settings/defaults/system_settings/launchers/nuke_11.2.toml new file mode 100644 index 0000000000..57c962d126 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/nuke_11.2.toml @@ -0,0 +1,7 @@ +executable = "nuke11.2" +schema = "avalon-core:application-1.0" +application_dir = "nuke" +label = "Nuke 11.2" +ftrack_label = "Nuke" +icon ="nuke_icon" +ftrack_icon = '{}/app_icons/nuke.png' diff --git a/pype/settings/defaults/system_settings/launchers/nuke_11.3.toml b/pype/settings/defaults/system_settings/launchers/nuke_11.3.toml new file mode 100644 index 0000000000..87f769c23b --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/nuke_11.3.toml @@ -0,0 +1,7 @@ +executable = "nuke11.3" +schema = "avalon-core:application-1.0" +application_dir = "nuke" +label = "Nuke 11.3" +ftrack_label = "Nuke" +icon ="nuke_icon" +ftrack_icon = '{}/app_icons/nuke.png' diff --git a/pype/settings/defaults/system_settings/launchers/nuke_12.0.toml b/pype/settings/defaults/system_settings/launchers/nuke_12.0.toml new file mode 100644 index 0000000000..62936b4cdb --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/nuke_12.0.toml @@ -0,0 +1,7 @@ +executable = "nuke12.0" +schema = "avalon-core:application-1.0" +application_dir = "nuke" +label = "Nuke 12.0" +ftrack_label = "Nuke" +icon ="nuke_icon" +ftrack_icon = '{}/app_icons/nuke.png' diff --git a/pype/settings/defaults/system_settings/launchers/nukestudio_10.0.toml b/pype/settings/defaults/system_settings/launchers/nukestudio_10.0.toml new file mode 100644 index 0000000000..41601e4d40 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/nukestudio_10.0.toml @@ -0,0 +1,7 @@ +executable = "nukestudio10.0" +schema = "avalon-core:application-1.0" +application_dir = "nukestudio" +label = "NukeStudio 10.0" +ftrack_label = "NukeStudio" +icon ="nuke_icon" +ftrack_icon = '{}/app_icons/nuke.png' diff --git a/pype/settings/defaults/system_settings/launchers/nukestudio_11.0.toml b/pype/settings/defaults/system_settings/launchers/nukestudio_11.0.toml new file mode 100644 index 0000000000..7a9d84707a --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/nukestudio_11.0.toml @@ -0,0 +1,7 @@ +executable = "nukestudio11.0" +schema = "avalon-core:application-1.0" +application_dir = "nukestudio" +label = "NukeStudio 11.0" +ftrack_label = "NukeStudio" +icon ="nuke_icon" +ftrack_icon = '{}/app_icons/nuke.png' diff --git a/pype/settings/defaults/system_settings/launchers/nukestudio_11.2.toml b/pype/settings/defaults/system_settings/launchers/nukestudio_11.2.toml new file mode 100644 index 0000000000..21557033ca --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/nukestudio_11.2.toml @@ -0,0 +1,7 @@ +executable = "nukestudio11.2" +schema = "avalon-core:application-1.0" +application_dir = "nukestudio" +label = "NukeStudio 11.2" +ftrack_label = "NukeStudio" +icon ="nuke_icon" +ftrack_icon = '{}/app_icons/nuke.png' diff --git a/pype/settings/defaults/system_settings/launchers/nukestudio_11.3.toml b/pype/settings/defaults/system_settings/launchers/nukestudio_11.3.toml new file mode 100644 index 0000000000..1946ad6c3b --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/nukestudio_11.3.toml @@ -0,0 +1,7 @@ +executable = "nukestudio11.3" +schema = "avalon-core:application-1.0" +application_dir = "nukestudio" +label = "NukeStudio 11.3" +ftrack_label = "NukeStudio" +icon ="nuke_icon" +ftrack_icon = '{}/app_icons/nuke.png' diff --git a/pype/settings/defaults/system_settings/launchers/nukestudio_12.0.toml b/pype/settings/defaults/system_settings/launchers/nukestudio_12.0.toml new file mode 100644 index 0000000000..4ce7f9b538 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/nukestudio_12.0.toml @@ -0,0 +1,7 @@ +executable = "nukestudio12.0" +schema = "avalon-core:application-1.0" +application_dir = "nukestudio" +label = "NukeStudio 12.0" +ftrack_label = "NukeStudio" +icon ="nuke_icon" +ftrack_icon = '{}/app_icons/nuke.png' diff --git a/pype/settings/defaults/system_settings/launchers/nukex_10.0.toml b/pype/settings/defaults/system_settings/launchers/nukex_10.0.toml new file mode 100644 index 0000000000..7dee22996d --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/nukex_10.0.toml @@ -0,0 +1,7 @@ +executable = "nukex10.0" +schema = "avalon-core:application-1.0" +application_dir = "nuke" +label = "NukeX 10.0" +ftrack_label = "NukeX" +icon ="nuke_icon" +ftrack_icon = '{}/app_icons/nukex.png' diff --git a/pype/settings/defaults/system_settings/launchers/nukex_11.0.toml b/pype/settings/defaults/system_settings/launchers/nukex_11.0.toml new file mode 100644 index 0000000000..c2b4970a26 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/nukex_11.0.toml @@ -0,0 +1,7 @@ +executable = "nukex11.0" +schema = "avalon-core:application-1.0" +application_dir = "nuke" +label = "NukeX 11.2" +ftrack_label = "NukeX" +icon ="nuke_icon" +ftrack_icon = '{}/app_icons/nukex.png' diff --git a/pype/settings/defaults/system_settings/launchers/nukex_11.2.toml b/pype/settings/defaults/system_settings/launchers/nukex_11.2.toml new file mode 100644 index 0000000000..3857b9995c --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/nukex_11.2.toml @@ -0,0 +1,7 @@ +executable = "nukex11.2" +schema = "avalon-core:application-1.0" +application_dir = "nuke" +label = "NukeX 11.2" +ftrack_label = "NukeX" +icon ="nuke_icon" +ftrack_icon = '{}/app_icons/nukex.png' diff --git a/pype/settings/defaults/system_settings/launchers/nukex_11.3.toml b/pype/settings/defaults/system_settings/launchers/nukex_11.3.toml new file mode 100644 index 0000000000..56428470eb --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/nukex_11.3.toml @@ -0,0 +1,7 @@ +executable = "nukex11.3" +schema = "avalon-core:application-1.0" +application_dir = "nuke" +label = "NukeX 11.3" +ftrack_label = "NukeX" +icon ="nuke_icon" +ftrack_icon = '{}/app_icons/nukex.png' diff --git a/pype/settings/defaults/system_settings/launchers/nukex_12.0.toml b/pype/settings/defaults/system_settings/launchers/nukex_12.0.toml new file mode 100644 index 0000000000..33d7fddb88 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/nukex_12.0.toml @@ -0,0 +1,7 @@ +executable = "nukex12.0" +schema = "avalon-core:application-1.0" +application_dir = "nuke" +label = "NukeX 12.0" +ftrack_label = "NukeX" +icon ="nuke_icon" +ftrack_icon = '{}/app_icons/nukex.png' diff --git a/pype/settings/defaults/system_settings/launchers/photoshop_2020.toml b/pype/settings/defaults/system_settings/launchers/photoshop_2020.toml new file mode 100644 index 0000000000..117b668232 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/photoshop_2020.toml @@ -0,0 +1,8 @@ +executable = "photoshop_2020" +schema = "avalon-core:application-1.0" +application_dir = "photoshop" +label = "Adobe Photoshop 2020" +icon ="photoshop_icon" +ftrack_label = "Photoshop" +ftrack_icon = '{}/app_icons/photoshop.png' +launch_hook = "pype/hooks/photoshop/prelaunch.py/PhotoshopPrelaunch" diff --git a/pype/settings/defaults/system_settings/launchers/premiere_2019.toml b/pype/settings/defaults/system_settings/launchers/premiere_2019.toml new file mode 100644 index 0000000000..f4c19c62cb --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/premiere_2019.toml @@ -0,0 +1,8 @@ +executable = "premiere_pro_2019" +schema = "avalon-core:application-1.0" +application_dir = "premiere" +label = "Adobe Premiere Pro CC 2019" +icon ="premiere_icon" + +ftrack_label = "Premiere" +ftrack_icon = '{}/app_icons/premiere.png' diff --git a/pype/settings/defaults/system_settings/launchers/premiere_2020.toml b/pype/settings/defaults/system_settings/launchers/premiere_2020.toml new file mode 100644 index 0000000000..4d721c898f --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/premiere_2020.toml @@ -0,0 +1,9 @@ +executable = "premiere_pro_2020" +schema = "avalon-core:application-1.0" +application_dir = "premiere" +label = "Adobe Premiere Pro CC 2020" +launch_hook = "pype/hooks/premiere/prelaunch.py/PremierePrelaunch" +icon ="premiere_icon" + +ftrack_label = "Premiere" +ftrack_icon = '{}/app_icons/premiere.png' diff --git a/pype/settings/defaults/system_settings/launchers/python_2.toml b/pype/settings/defaults/system_settings/launchers/python_2.toml new file mode 100644 index 0000000000..e9e8dd7899 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/python_2.toml @@ -0,0 +1,10 @@ +schema = "avalon-core:application-1.0" +application_dir = "python" +executable = "python" +label = "Python 2" +ftrack_label = "Python" +icon ="python_icon" +ftrack_icon = '{}/app_icons/python.png' + +[environment] +CREATE_NEW_CONSOLE = "Yes" diff --git a/pype/settings/defaults/system_settings/launchers/python_3.toml b/pype/settings/defaults/system_settings/launchers/python_3.toml new file mode 100644 index 0000000000..5cbd8b2943 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/python_3.toml @@ -0,0 +1,10 @@ +schema = "avalon-core:application-1.0" +application_dir = "python" +executable = "python3" +label = "Python 3" +ftrack_label = "Python" +icon ="python_icon" +ftrack_icon = '{}/app_icons/python.png' + +[environment] +CREATE_NEW_CONSOLE = "Yes" diff --git a/pype/settings/defaults/system_settings/launchers/resolve_16.toml b/pype/settings/defaults/system_settings/launchers/resolve_16.toml new file mode 100644 index 0000000000..430fd1a638 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/resolve_16.toml @@ -0,0 +1,9 @@ +executable = "resolve_16" +schema = "avalon-core:application-1.0" +application_dir = "resolve" +label = "BM DaVinci Resolve 16" +launch_hook = "pype/hooks/resolve/prelaunch.py/ResolvePrelaunch" +icon ="resolve" + +ftrack_label = "BM DaVinci Resolve" +ftrack_icon = '{}/app_icons/resolve.png' diff --git a/pype/settings/defaults/system_settings/launchers/shell.toml b/pype/settings/defaults/system_settings/launchers/shell.toml new file mode 100644 index 0000000000..959ad392ea --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/shell.toml @@ -0,0 +1,7 @@ +schema = "avalon-core:application-1.0" +application_dir = "shell" +executable = "shell" +label = "Shell" + +[environment] +CREATE_NEW_CONSOLE = "Yes" \ No newline at end of file diff --git a/pype/settings/defaults/system_settings/launchers/storyboardpro_7.toml b/pype/settings/defaults/system_settings/launchers/storyboardpro_7.toml new file mode 100644 index 0000000000..ce8e96a49d --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/storyboardpro_7.toml @@ -0,0 +1,8 @@ +application_dir = "storyboardpro" +label = "Storyboard Pro 7" +ftrack_label = "Storyboard Pro" +schema = "avalon-core:application-1.0" +executable = "storyboardpro_7" +description = "" +icon ="storyboardpro_icon" +ftrack_icon = '{}/app_icons/storyboardpro.png' diff --git a/pype/settings/defaults/system_settings/launchers/unreal_4.24.toml b/pype/settings/defaults/system_settings/launchers/unreal_4.24.toml new file mode 100644 index 0000000000..0a799e5dcb --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/unreal_4.24.toml @@ -0,0 +1,8 @@ +executable = "unreal" +schema = "avalon-core:application-1.0" +application_dir = "unreal" +label = "Unreal Editor 4.24" +ftrack_label = "UnrealEditor" +icon ="ue4_icon" +launch_hook = "pype/hooks/unreal/unreal_prelaunch.py/UnrealPrelaunch" +ftrack_icon = '{}/app_icons/ue4.png' diff --git a/pype/settings/defaults/system_settings/launchers/windows/blender_2.80.bat b/pype/settings/defaults/system_settings/launchers/windows/blender_2.80.bat new file mode 100644 index 0000000000..5b8a37356b --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/blender_2.80.bat @@ -0,0 +1,11 @@ +set __app__="Blender" +set __exe__="C:\Program Files\Blender Foundation\Blender 2.80\blender.exe" +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/blender_2.81.bat b/pype/settings/defaults/system_settings/launchers/windows/blender_2.81.bat new file mode 100644 index 0000000000..a900b18eda --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/blender_2.81.bat @@ -0,0 +1,11 @@ +set __app__="Blender" +set __exe__="C:\Program Files\Blender Foundation\Blender 2.81\blender.exe" +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/blender_2.82.bat b/pype/settings/defaults/system_settings/launchers/windows/blender_2.82.bat new file mode 100644 index 0000000000..7105c1efe1 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/blender_2.82.bat @@ -0,0 +1,11 @@ +set __app__="Blender" +set __exe__="C:\Program Files\Blender Foundation\Blender 2.82\blender.exe" --python-use-system-env +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/blender_2.83.bat b/pype/settings/defaults/system_settings/launchers/windows/blender_2.83.bat new file mode 100644 index 0000000000..671952f0d7 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/blender_2.83.bat @@ -0,0 +1,11 @@ +set __app__="Blender" +set __exe__="C:\Program Files\Blender Foundation\Blender 2.83\blender.exe" --python-use-system-env +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/celaction_local.bat b/pype/settings/defaults/system_settings/launchers/windows/celaction_local.bat new file mode 100644 index 0000000000..8f2171617e --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/celaction_local.bat @@ -0,0 +1,19 @@ +set __app__="CelAction2D" +set __app_dir__="C:\Program Files (x86)\CelAction\" +set __exe__="C:\Program Files (x86)\CelAction\CelAction2D.exe" + +if not exist %__exe__% goto :missing_app + +pushd %__app_dir__% + +if "%PYPE_CELACTION_PROJECT_FILE%"=="" ( + start %__app__% %__exe__% %* +) else ( + start %__app__% %__exe__% "%PYPE_CELACTION_PROJECT_FILE%" %* +) + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/celaction_publish.bat b/pype/settings/defaults/system_settings/launchers/windows/celaction_publish.bat new file mode 100644 index 0000000000..77ec2ac24e --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/celaction_publish.bat @@ -0,0 +1,3 @@ +echo %* + +%PYPE_PYTHON_EXE% "%PYPE_MODULE_ROOT%\pype\hosts\celaction\cli.py" %* diff --git a/pype/settings/defaults/system_settings/launchers/windows/harmony_17.bat b/pype/settings/defaults/system_settings/launchers/windows/harmony_17.bat new file mode 100644 index 0000000000..0822650875 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/harmony_17.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="Harmony 17" +set __exe__="C:/Program Files (x86)/Toon Boom Animation/Toon Boom Harmony 17 Premium/win64/bin/HarmonyPremium.exe" +if not exist %__exe__% goto :missing_app + +start %__app__% cmd.exe /k "python -c ^"import avalon.harmony;avalon.harmony.launch("%__exe__%")^"" + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/houdini_16.bat b/pype/settings/defaults/system_settings/launchers/windows/houdini_16.bat new file mode 100644 index 0000000000..018ba08b4c --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/houdini_16.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="Houdini 16.0" +set __exe__="C:\Program Files\Side Effects Software\Houdini 16.0.621\bin\houdini.exe" +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/houdini_17.bat b/pype/settings/defaults/system_settings/launchers/windows/houdini_17.bat new file mode 100644 index 0000000000..950a599623 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/houdini_17.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="Houdini 17.0" +set __exe__="C:\Program Files\Side Effects Software\Houdini 17.0.459\bin\houdini.exe" +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/houdini_18.bat b/pype/settings/defaults/system_settings/launchers/windows/houdini_18.bat new file mode 100644 index 0000000000..3d6b1ae258 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/houdini_18.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="Houdini 18.0" +set __exe__="C:\Program Files\Side Effects Software\Houdini 18.0.287\bin\houdini.exe" +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/maya2016.bat b/pype/settings/defaults/system_settings/launchers/windows/maya2016.bat new file mode 100644 index 0000000000..54f15cf269 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/maya2016.bat @@ -0,0 +1,17 @@ +@echo off + +set __app__="Maya 2016" +set __exe__="C:\Program Files\Autodesk\Maya2016\bin\maya.exe" +if not exist %__exe__% goto :missing_app + +if "%AVALON_LAST_WORKFILE%"=="" ( + start %__app__% %__exe__% %* +) else ( + start %__app__% %__exe__% -file "%AVALON_LAST_WORKFILE%" %* +) + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/maya2017.bat b/pype/settings/defaults/system_settings/launchers/windows/maya2017.bat new file mode 100644 index 0000000000..5c2aeb495c --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/maya2017.bat @@ -0,0 +1,17 @@ +@echo off + +set __app__="Maya 2017" +set __exe__="C:\Program Files\Autodesk\Maya2017\bin\maya.exe" +if not exist %__exe__% goto :missing_app + +if "%AVALON_LAST_WORKFILE%"=="" ( + start %__app__% %__exe__% %* +) else ( + start %__app__% %__exe__% -file "%AVALON_LAST_WORKFILE%" %* +) + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/maya2018.bat b/pype/settings/defaults/system_settings/launchers/windows/maya2018.bat new file mode 100644 index 0000000000..28cf776c77 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/maya2018.bat @@ -0,0 +1,17 @@ +@echo off + +set __app__="Maya 2018" +set __exe__="C:\Program Files\Autodesk\Maya2018\bin\maya.exe" +if not exist %__exe__% goto :missing_app + +if "%AVALON_LAST_WORKFILE%"=="" ( + start %__app__% %__exe__% %* +) else ( + start %__app__% %__exe__% -file "%AVALON_LAST_WORKFILE%" %* +) + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/maya2019.bat b/pype/settings/defaults/system_settings/launchers/windows/maya2019.bat new file mode 100644 index 0000000000..7e80dd2557 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/maya2019.bat @@ -0,0 +1,17 @@ +@echo off + +set __app__="Maya 2019" +set __exe__="C:\Program Files\Autodesk\Maya2019\bin\maya.exe" +if not exist %__exe__% goto :missing_app + +if "%AVALON_LAST_WORKFILE%"=="" ( + start %__app__% %__exe__% %* +) else ( + start %__app__% %__exe__% -file "%AVALON_LAST_WORKFILE%" %* +) + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/maya2020.bat b/pype/settings/defaults/system_settings/launchers/windows/maya2020.bat new file mode 100644 index 0000000000..b2acb5df5a --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/maya2020.bat @@ -0,0 +1,17 @@ +@echo off + +set __app__="Maya 2020" +set __exe__="C:\Program Files\Autodesk\maya2020\bin\maya.exe" +if not exist %__exe__% goto :missing_app + +if "%AVALON_LAST_WORKFILE%"=="" ( + start %__app__% %__exe__% %* +) else ( + start %__app__% %__exe__% -file "%AVALON_LAST_WORKFILE%" %* +) + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/mayabatch2019.bat b/pype/settings/defaults/system_settings/launchers/windows/mayabatch2019.bat new file mode 100644 index 0000000000..ddd9b9b956 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/mayabatch2019.bat @@ -0,0 +1,14 @@ +@echo off + +set __app__="Maya Batch 2019" +set __exe__="C:\Program Files\Autodesk\Maya2019\bin\mayabatch.exe" +if not exist %__exe__% goto :missing_app + +echo "running maya : %*" +%__exe__% %* +echo "done." +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/mayabatch2020.bat b/pype/settings/defaults/system_settings/launchers/windows/mayabatch2020.bat new file mode 100644 index 0000000000..b1cbc6dbb6 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/mayabatch2020.bat @@ -0,0 +1,14 @@ +@echo off + +set __app__="Maya Batch 2020" +set __exe__="C:\Program Files\Autodesk\Maya2020\bin\mayabatch.exe" +if not exist %__exe__% goto :missing_app + +echo "running maya : %*" +%__exe__% %* +echo "done." +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/mayapy2016.bat b/pype/settings/defaults/system_settings/launchers/windows/mayapy2016.bat new file mode 100644 index 0000000000..205991fd3d --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/mayapy2016.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="Mayapy 2016" +set __exe__="C:\Program Files\Autodesk\Maya2016\bin\mayapy.exe" +if not exist %__exe__% goto :missing_app + +call %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found at %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/mayapy2017.bat b/pype/settings/defaults/system_settings/launchers/windows/mayapy2017.bat new file mode 100644 index 0000000000..14aacc5a7f --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/mayapy2017.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="Mayapy 2017" +set __exe__="C:\Program Files\Autodesk\Maya2017\bin\mayapy.exe" +if not exist %__exe__% goto :missing_app + +call %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found at %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/mayapy2018.bat b/pype/settings/defaults/system_settings/launchers/windows/mayapy2018.bat new file mode 100644 index 0000000000..c47c472f46 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/mayapy2018.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="Mayapy 2018" +set __exe__="C:\Program Files\Autodesk\Maya2018\bin\mayapy.exe" +if not exist %__exe__% goto :missing_app + +call %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found at %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/mayapy2019.bat b/pype/settings/defaults/system_settings/launchers/windows/mayapy2019.bat new file mode 100644 index 0000000000..73ca5b2d40 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/mayapy2019.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="Mayapy 2019" +set __exe__="C:\Program Files\Autodesk\Maya2019\bin\mayapy.exe" +if not exist %__exe__% goto :missing_app + +call %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found at %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/mayapy2020.bat b/pype/settings/defaults/system_settings/launchers/windows/mayapy2020.bat new file mode 100644 index 0000000000..770a03dcf5 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/mayapy2020.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="Mayapy 2020" +set __exe__="C:\Program Files\Autodesk\Maya2020\bin\mayapy.exe" +if not exist %__exe__% goto :missing_app + +call %__exe__% %* + +goto :eofS + +:missing_app + echo ERROR: %__app__% not found at %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/nuke10.0.bat b/pype/settings/defaults/system_settings/launchers/windows/nuke10.0.bat new file mode 100644 index 0000000000..a47cbdfb20 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/nuke10.0.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="Nuke10.0v4" +set __exe__="C:\Program Files\Nuke10.0v4\Nuke10.0.exe" +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/nuke11.0.bat b/pype/settings/defaults/system_settings/launchers/windows/nuke11.0.bat new file mode 100644 index 0000000000..a374c5cf5b --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/nuke11.0.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="Nuke11.0v4" +set __exe__="C:\Program Files\Nuke11.0v4\Nuke11.0.exe" +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/nuke11.2.bat b/pype/settings/defaults/system_settings/launchers/windows/nuke11.2.bat new file mode 100644 index 0000000000..4c777ac28c --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/nuke11.2.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="Nuke11.2v3" +set __exe__="C:\Program Files\Nuke11.2v3\Nuke11.2.exe" +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/nuke11.3.bat b/pype/settings/defaults/system_settings/launchers/windows/nuke11.3.bat new file mode 100644 index 0000000000..a023f5f46f --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/nuke11.3.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="Nuke11.3v1" +set __exe__="C:\Program Files\Nuke11.3v1\Nuke11.3.exe" +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/nuke12.0.bat b/pype/settings/defaults/system_settings/launchers/windows/nuke12.0.bat new file mode 100644 index 0000000000..d8fb5772bb --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/nuke12.0.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="Nuke12.0v1" +set __exe__="C:\Program Files\Nuke12.0v1\Nuke12.0.exe" +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/nukestudio10.0.bat b/pype/settings/defaults/system_settings/launchers/windows/nukestudio10.0.bat new file mode 100644 index 0000000000..82f833667c --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/nukestudio10.0.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="NukeStudio10.0v4" +set __exe__="C:\Program Files\Nuke10.0v4\Nuke10.0.exe" --studio +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/nukestudio11.0.bat b/pype/settings/defaults/system_settings/launchers/windows/nukestudio11.0.bat new file mode 100644 index 0000000000..b66797727e --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/nukestudio11.0.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="NukeStudio11.0v4" +set __exe__="C:\Program Files\Nuke11.0v4\Nuke11.0.exe" -studio +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/nukestudio11.2.bat b/pype/settings/defaults/system_settings/launchers/windows/nukestudio11.2.bat new file mode 100644 index 0000000000..a653d816b4 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/nukestudio11.2.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="NukeStudio11.2v3" +set __exe__="C:\Program Files\Nuke11.2v3\Nuke11.2.exe" -studio +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/nukestudio11.3.bat b/pype/settings/defaults/system_settings/launchers/windows/nukestudio11.3.bat new file mode 100644 index 0000000000..62c8718873 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/nukestudio11.3.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="NukeStudio11.3v1" +set __exe__="C:\Program Files\Nuke11.3v1\Nuke11.3.exe" --studio +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/nukestudio12.0.bat b/pype/settings/defaults/system_settings/launchers/windows/nukestudio12.0.bat new file mode 100644 index 0000000000..488232bcbf --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/nukestudio12.0.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="NukeStudio12.0v1" +set __exe__="C:\Program Files\Nuke12.0v1\Nuke12.0.exe" --studio +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/nukex10.0.bat b/pype/settings/defaults/system_settings/launchers/windows/nukex10.0.bat new file mode 100644 index 0000000000..1759706a7b --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/nukex10.0.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="NukeX10.0v4" +set __exe__="C:\Program Files\Nuke10.0v4\Nuke10.0.exe" -nukex +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/nukex11.0.bat b/pype/settings/defaults/system_settings/launchers/windows/nukex11.0.bat new file mode 100644 index 0000000000..b554a7b6fa --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/nukex11.0.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="NukeX11.0v4" +set __exe__="C:\Program Files\Nuke11.0v4\Nuke11.0.exe" --nukex +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/nukex11.2.bat b/pype/settings/defaults/system_settings/launchers/windows/nukex11.2.bat new file mode 100644 index 0000000000..a4cb5dec5c --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/nukex11.2.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="NukeX11.2v3" +set __exe__="C:\Program Files\Nuke11.2v3\Nuke11.2.exe" --nukex +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/nukex11.3.bat b/pype/settings/defaults/system_settings/launchers/windows/nukex11.3.bat new file mode 100644 index 0000000000..490b55cf4c --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/nukex11.3.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="NukeX11.3v1" +set __exe__="C:\Program Files\Nuke11.3v1\Nuke11.3.exe" --nukex +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/nukex12.0.bat b/pype/settings/defaults/system_settings/launchers/windows/nukex12.0.bat new file mode 100644 index 0000000000..26adf0d3f1 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/nukex12.0.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="NukeX12.0v1" +set __exe__="C:\Program Files\Nuke12.0v1\Nuke12.0.exe" --nukex +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/photoshop_2020.bat b/pype/settings/defaults/system_settings/launchers/windows/photoshop_2020.bat new file mode 100644 index 0000000000..6b90922ef6 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/photoshop_2020.bat @@ -0,0 +1,15 @@ +@echo off + +set __app__="Photoshop 2020" +set __exe__="C:\Program Files\Adobe\Adobe Photoshop 2020\Photoshop.exe" +if not exist %__exe__% goto :missing_app + +start %__app__% cmd.exe /k "%PYPE_PYTHON_EXE% -c ^"import avalon.photoshop;avalon.photoshop.launch("%__exe__%")^"" + +goto :eof + +pause + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/premiere_pro_2019.bat b/pype/settings/defaults/system_settings/launchers/windows/premiere_pro_2019.bat new file mode 100644 index 0000000000..4886737d2f --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/premiere_pro_2019.bat @@ -0,0 +1,14 @@ +@echo off + +set __app__="Adobe Premiere Pro" +set __exe__="C:\Program Files\Adobe\Adobe Premiere Pro CC 2019\Adobe Premiere Pro.exe" +if not exist %__exe__% goto :missing_app + +python -u %PREMIERA_PATH%\init.py +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/premiere_pro_2020.bat b/pype/settings/defaults/system_settings/launchers/windows/premiere_pro_2020.bat new file mode 100644 index 0000000000..14662d3be3 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/premiere_pro_2020.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="Adobe Premiere Pro" +set __exe__="C:\Program Files\Adobe\Adobe Premiere Pro 2020\Adobe Premiere Pro.exe" +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/python3.bat b/pype/settings/defaults/system_settings/launchers/windows/python3.bat new file mode 100644 index 0000000000..c7c116fe72 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/python3.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="Python36" +set __exe__="C:\Python36\python.exe" +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/resolve_16.bat b/pype/settings/defaults/system_settings/launchers/windows/resolve_16.bat new file mode 100644 index 0000000000..1a5d964e6b --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/resolve_16.bat @@ -0,0 +1,17 @@ +@echo off + +set __app__="Resolve" +set __appy__="Resolve Python Console" +set __exe__="C:/Program Files/Blackmagic Design/DaVinci Resolve/Resolve.exe" +set __py__="%PYTHON36_RESOLVE%/python.exe" + +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %* +IF "%RESOLVE_DEV%"=="True" (start %__appy__% %__py__% -i %PRE_PYTHON_SCRIPT%) + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/shell.bat b/pype/settings/defaults/system_settings/launchers/windows/shell.bat new file mode 100644 index 0000000000..eb0895364f --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/shell.bat @@ -0,0 +1,2 @@ +@echo off +start cmd diff --git a/pype/settings/defaults/system_settings/launchers/windows/storyboardpro_7.bat b/pype/settings/defaults/system_settings/launchers/windows/storyboardpro_7.bat new file mode 100644 index 0000000000..122edac572 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/storyboardpro_7.bat @@ -0,0 +1,13 @@ +@echo off + +set __app__="Storyboard Pro 7" +set __exe__="C:/Program Files (x86)/Toon Boom Animation/Toon Boom Storyboard Pro 7/win64/bin/StoryboardPro.exe" +if not exist %__exe__% goto :missing_app + +start %__app__% cmd.exe /k "python -c ^"import avalon.storyboardpro;avalon.storyboardpro.launch("%__exe__%")^"" + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/launchers/windows/unreal.bat b/pype/settings/defaults/system_settings/launchers/windows/unreal.bat new file mode 100644 index 0000000000..7771aaa5a5 --- /dev/null +++ b/pype/settings/defaults/system_settings/launchers/windows/unreal.bat @@ -0,0 +1,11 @@ +set __app__="Unreal Editor" +set __exe__="%AVALON_CURRENT_UNREAL_ENGINE%\Engine\Binaries\Win64\UE4Editor.exe" +if not exist %__exe__% goto :missing_app + +start %__app__% %__exe__% %PYPE_UNREAL_PROJECT_FILE% %* + +goto :eof + +:missing_app + echo ERROR: %__app__% not found in %__exe__% + exit /B 1 diff --git a/pype/settings/defaults/system_settings/muster/templates_mapping.json b/pype/settings/defaults/system_settings/muster/templates_mapping.json new file mode 100644 index 0000000000..0c09113515 --- /dev/null +++ b/pype/settings/defaults/system_settings/muster/templates_mapping.json @@ -0,0 +1,19 @@ +{ + "3delight": 41, + "arnold": 46, + "arnold_sf": 57, + "gelato": 30, + "harware": 3, + "krakatoa": 51, + "file_layers": 7, + "mentalray": 2, + "mentalray_sf": 6, + "redshift": 55, + "renderman": 29, + "software": 1, + "software_sf": 5, + "turtle": 10, + "vector": 4, + "vray": 37, + "ffmpeg": 48 +} \ No newline at end of file diff --git a/pype/settings/defaults/system_settings/standalone_publish/families.json b/pype/settings/defaults/system_settings/standalone_publish/families.json new file mode 100644 index 0000000000..d05941cc26 --- /dev/null +++ b/pype/settings/defaults/system_settings/standalone_publish/families.json @@ -0,0 +1,90 @@ +{ + "create_look": { + "name": "look", + "label": "Look", + "family": "look", + "icon": "paint-brush", + "defaults": ["Main"], + "help": "Shader connections defining shape look" + }, + "create_model": { + "name": "model", + "label": "Model", + "family": "model", + "icon": "cube", + "defaults": ["Main", "Proxy", "Sculpt"], + "help": "Polygonal static geometry" + }, + "create_workfile": { + "name": "workfile", + "label": "Workfile", + "family": "workfile", + "icon": "cube", + "defaults": ["Main"], + "help": "Working scene backup" + }, + "create_camera": { + "name": "camera", + "label": "Camera", + "family": "camera", + "icon": "video-camera", + "defaults": ["Main"], + "help": "Single baked camera" + }, + "create_pointcache": { + "name": "pointcache", + "label": "Pointcache", + "family": "pointcache", + "icon": "gears", + "defaults": ["Main"], + "help": "Alembic pointcache for animated data" + }, + "create_rig": { + "name": "rig", + "label": "Rig", + "family": "rig", + "icon": "wheelchair", + "defaults": ["Main"], + "help": "Artist-friendly rig with controls" + }, + "create_layout": { + "name": "layout", + "label": "Layout", + "family": "layout", + "icon": "cubes", + "defaults": ["Main"], + "help": "Simple scene for animators with camera" + }, + "create_plate": { + "name": "plate", + "label": "Plate", + "family": "plate", + "icon": "camera", + "defaults": ["Main", "BG", "Reference"], + "help": "Plates for compositors" + }, + "create_matchmove": { + "name": "matchmove", + "label": "Matchmove script", + "family": "matchmove", + "icon": "empire", + "defaults": ["Camera", "Object", "Mocap"], + "help": "Script exported from matchmoving application" + }, + "create_images": { + "name": "image", + "label": "Image file", + "family": "image", + "icon": "image", + "defaults": ["ConceptArt", "Reference", "Texture", "MattePaint"], + "help": "Holder for all kinds of image data" + }, + "create_editorial": { + "name": "editorial", + "label": "Editorial", + "family": "editorial", + "icon": "image", + "defaults": ["Main"], + "help": "Editorial files to generate shots." + } +} diff --git a/pype/settings/lib.py b/pype/settings/lib.py new file mode 100644 index 0000000000..388557ca9b --- /dev/null +++ b/pype/settings/lib.py @@ -0,0 +1,258 @@ +import os +import json +import logging +import copy + +log = logging.getLogger(__name__) + +# Metadata keys for work with studio and project overrides +OVERRIDEN_KEY = "__overriden_keys__" +# NOTE key popping not implemented yet +POP_KEY = "__pop_key__" + +# Folder where studio overrides are stored +STUDIO_OVERRIDES_PATH = os.environ["PYPE_PROJECT_CONFIGS"] + +# File where studio's system overrides are stored +SYSTEM_SETTINGS_KEY = "system_settings" +SYSTEM_SETTINGS_PATH = os.path.join( + STUDIO_OVERRIDES_PATH, SYSTEM_SETTINGS_KEY + ".json" +) + +# File where studio's default project overrides are stored +PROJECT_SETTINGS_KEY = "project_settings" +PROJECT_SETTINGS_FILENAME = PROJECT_SETTINGS_KEY + ".json" +PROJECT_SETTINGS_PATH = os.path.join( + STUDIO_OVERRIDES_PATH, PROJECT_SETTINGS_FILENAME +) + +PROJECT_ANATOMY_KEY = "project_anatomy" +PROJECT_ANATOMY_FILENAME = PROJECT_ANATOMY_KEY + ".json" +PROJECT_ANATOMY_PATH = os.path.join( + STUDIO_OVERRIDES_PATH, PROJECT_ANATOMY_FILENAME +) + +# Path to default settings +DEFAULTS_DIR = os.path.join(os.path.dirname(__file__), "defaults") + +# Variable where cache of default settings are stored +_DEFAULT_SETTINGS = None + + +def reset_default_settings(): + global _DEFAULT_SETTINGS + _DEFAULT_SETTINGS = None + + +def default_settings(): + global _DEFAULT_SETTINGS + if _DEFAULT_SETTINGS is None: + _DEFAULT_SETTINGS = load_jsons_from_dir(DEFAULTS_DIR) + return _DEFAULT_SETTINGS + + +def load_json(fpath): + # Load json data + with open(fpath, "r") as opened_file: + lines = opened_file.read().splitlines() + + # prepare json string + standard_json = "" + for line in lines: + # Remove all whitespace on both sides + line = line.strip() + + # Skip blank lines + if len(line) == 0: + continue + + standard_json += line + + # Check if has extra commas + extra_comma = False + if ",]" in standard_json or ",}" in standard_json: + extra_comma = True + standard_json = standard_json.replace(",]", "]") + standard_json = standard_json.replace(",}", "}") + + if extra_comma: + log.error("Extra comma in json file: \"{}\"".format(fpath)) + + # return empty dict if file is empty + if standard_json == "": + return {} + + # Try to parse string + try: + return json.loads(standard_json) + + except json.decoder.JSONDecodeError: + # Return empty dict if it is first time that decode error happened + return {} + + # Repreduce the exact same exception but traceback contains better + # information about position of error in the loaded json + try: + with open(fpath, "r") as opened_file: + json.load(opened_file) + + except json.decoder.JSONDecodeError: + log.warning( + "File has invalid json format \"{}\"".format(fpath), + exc_info=True + ) + + return {} + + +def subkey_merge(_dict, value, keys): + key = keys.pop(0) + if not keys: + _dict[key] = value + return _dict + + if key not in _dict: + _dict[key] = {} + _dict[key] = subkey_merge(_dict[key], value, keys) + + return _dict + + +def load_jsons_from_dir(path, *args, **kwargs): + output = {} + + path = os.path.normpath(path) + if not os.path.exists(path): + # TODO warning + return output + + sub_keys = list(kwargs.pop("subkeys", args)) + for sub_key in tuple(sub_keys): + _path = os.path.join(path, sub_key) + if not os.path.exists(_path): + break + + path = _path + sub_keys.pop(0) + + base_len = len(path) + 1 + for base, _directories, filenames in os.walk(path): + base_items_str = base[base_len:] + if not base_items_str: + base_items = [] + else: + base_items = base_items_str.split(os.path.sep) + + for filename in filenames: + basename, ext = os.path.splitext(filename) + if ext == ".json": + full_path = os.path.join(base, filename) + value = load_json(full_path) + dict_keys = base_items + [basename] + output = subkey_merge(output, value, dict_keys) + + for sub_key in sub_keys: + output = output[sub_key] + return output + + +def studio_system_settings(): + if os.path.exists(SYSTEM_SETTINGS_PATH): + return load_json(SYSTEM_SETTINGS_PATH) + return {} + + +def studio_project_settings(): + if os.path.exists(PROJECT_SETTINGS_PATH): + return load_json(PROJECT_SETTINGS_PATH) + return {} + + +def studio_project_anatomy(): + if os.path.exists(PROJECT_ANATOMY_PATH): + return load_json(PROJECT_ANATOMY_PATH) + return {} + + +def path_to_project_overrides(project_name): + return os.path.join( + STUDIO_OVERRIDES_PATH, + project_name, + PROJECT_SETTINGS_FILENAME + ) + + +def path_to_project_anatomy(project_name): + return os.path.join( + STUDIO_OVERRIDES_PATH, + project_name, + PROJECT_ANATOMY_FILENAME + ) + + +def project_settings_overrides(project_name): + if not project_name: + return {} + + path_to_json = path_to_project_overrides(project_name) + if not os.path.exists(path_to_json): + return {} + return load_json(path_to_json) + + +def project_anatomy_overrides(project_name): + if not project_name: + return {} + + path_to_json = path_to_project_anatomy(project_name) + if not os.path.exists(path_to_json): + return {} + return load_json(path_to_json) + + +def merge_overrides(global_dict, override_dict): + if OVERRIDEN_KEY in override_dict: + overriden_keys = set(override_dict.pop(OVERRIDEN_KEY)) + else: + overriden_keys = set() + + for key, value in override_dict.items(): + if value == POP_KEY: + global_dict.pop(key) + + elif ( + key in overriden_keys + or key not in global_dict + ): + global_dict[key] = value + + elif isinstance(value, dict) and isinstance(global_dict[key], dict): + global_dict[key] = merge_overrides(global_dict[key], value) + + else: + global_dict[key] = value + return global_dict + + +def apply_overrides(source_data, override_data): + if not override_data: + return source_data + _source_data = copy.deepcopy(source_data) + return merge_overrides(_source_data, override_data) + + +def system_settings(): + default_values = default_settings()[SYSTEM_SETTINGS_KEY] + studio_values = studio_system_settings() + return apply_overrides(default_values, studio_values) + + +def project_settings(project_name): + default_values = default_settings()[PROJECT_SETTINGS_KEY] + studio_values = studio_project_settings() + + studio_overrides = apply_overrides(default_values, studio_values) + + project_overrides = project_settings_overrides(project_name) + + return apply_overrides(studio_overrides, project_overrides) diff --git a/pype/tools/launcher/window.py b/pype/tools/launcher/window.py index 13b4abee6e..7c680a927b 100644 --- a/pype/tools/launcher/window.py +++ b/pype/tools/launcher/window.py @@ -4,7 +4,7 @@ import logging from Qt import QtWidgets, QtCore, QtGui from avalon import style -from pype.modules.ftrack.lib.io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB from pype.api import resources from avalon.tools import lib as tools_lib @@ -251,7 +251,7 @@ class LauncherWindow(QtWidgets.QDialog): self.log = logging.getLogger( ".".join([__name__, self.__class__.__name__]) ) - self.dbcon = DbConnector() + self.dbcon = AvalonMongoDB() self.setWindowTitle("Launcher") self.setFocusPolicy(QtCore.Qt.StrongFocus) diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index 3c9d4806ac..1482ff85b0 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -1147,48 +1147,52 @@ class TerminalModel(QtGui.QStandardItemModel): return prepared_records - def append(self, record_item): - record_type = record_item["type"] + def append(self, record_items): + all_record_items = [] + for record_item in record_items: + record_type = record_item["type"] - terminal_item_type = None - if record_type == "record": - for level, _type in self.level_to_record: - if level > record_item["levelno"]: - break - terminal_item_type = _type + terminal_item_type = None + if record_type == "record": + for level, _type in self.level_to_record: + if level > record_item["levelno"]: + break + terminal_item_type = _type - else: - terminal_item_type = record_type + else: + terminal_item_type = record_type - icon_color = self.item_icon_colors.get(terminal_item_type) - icon_name = self.item_icon_name.get(record_type) + icon_color = self.item_icon_colors.get(terminal_item_type) + icon_name = self.item_icon_name.get(record_type) - top_item_icon = None - if icon_color and icon_name: - top_item_icon = QAwesomeIconFactory.icon(icon_name, icon_color) + top_item_icon = None + if icon_color and icon_name: + top_item_icon = QAwesomeIconFactory.icon(icon_name, icon_color) - label = record_item["label"].split("\n")[0] + label = record_item["label"].split("\n")[0] - top_item = QtGui.QStandardItem() - top_item.setData(TerminalLabelType, Roles.TypeRole) - top_item.setData(terminal_item_type, Roles.TerminalItemTypeRole) - top_item.setData(label, QtCore.Qt.DisplayRole) - top_item.setFlags( - QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled - ) + top_item = QtGui.QStandardItem() + all_record_items.append(top_item) - if top_item_icon: - top_item.setData(top_item_icon, QtCore.Qt.DecorationRole) + detail_item = TerminalDetailItem(record_item) + top_item.appendRow(detail_item) - self.appendRow(top_item) + top_item.setData(TerminalLabelType, Roles.TypeRole) + top_item.setData(terminal_item_type, Roles.TerminalItemTypeRole) + top_item.setData(label, QtCore.Qt.DisplayRole) + top_item.setFlags( + QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled + ) - detail_item = TerminalDetailItem(record_item) - detail_item.setData(TerminalDetailType, Roles.TypeRole) - top_item.appendRow(detail_item) + if top_item_icon: + top_item.setData(top_item_icon, QtCore.Qt.DecorationRole) + + detail_item.setData(TerminalDetailType, Roles.TypeRole) + + self.invisibleRootItem().appendRows(all_record_items) def update_with_result(self, result): - for record in result["records"]: - self.append(record) + self.append(result["records"]) class TerminalProxy(QtCore.QSortFilterProxyModel): diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 76f31e2442..2a037ba4bc 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -1087,10 +1087,10 @@ class Window(QtWidgets.QDialog): info.setText(message) # Include message in terminal - self.terminal_model.append({ + self.terminal_model.append([{ "label": message, "type": "info" - }) + }]) self.animation_info_msg.stop() self.animation_info_msg.start() diff --git a/pype/tools/settings/__init__.py b/pype/tools/settings/__init__.py new file mode 100644 index 0000000000..7df121f06e --- /dev/null +++ b/pype/tools/settings/__init__.py @@ -0,0 +1,7 @@ +from settings import style, MainWidget + + +__all__ = ( + "style", + "MainWidget" +) diff --git a/pype/tools/settings/__main__.py b/pype/tools/settings/__main__.py new file mode 100644 index 0000000000..55a38b3604 --- /dev/null +++ b/pype/tools/settings/__main__.py @@ -0,0 +1,18 @@ +import sys + +import settings +from Qt import QtWidgets, QtGui + + +if __name__ == "__main__": + app = QtWidgets.QApplication(sys.argv) + + stylesheet = settings.style.load_stylesheet() + app.setStyleSheet(stylesheet) + app.setWindowIcon(QtGui.QIcon(settings.style.app_icon_path())) + + develop = "-d" in sys.argv or "--develop" in sys.argv + widget = settings.MainWidget(develop) + widget.show() + + sys.exit(app.exec_()) diff --git a/pype/tools/settings/settings/README.md b/pype/tools/settings/settings/README.md new file mode 100644 index 0000000000..47bbf28ba5 --- /dev/null +++ b/pype/tools/settings/settings/README.md @@ -0,0 +1,336 @@ +# Creating GUI schemas + +## Basic rules +- configurations does not define GUI, but GUI defines configurations! +- output is always json (yaml is not needed for anatomy templates anymore) +- GUI schema has multiple input types, all inputs are represented by a dictionary +- each input may have "input modifiers" (keys in dictionary) that are required or optional + - only required modifier for all input items is key `"type"` which says what type of item it is +- there are special keys across all inputs + - `"is_file"` - this key is for storing pype defaults in `pype` repo + - reasons of existence: developing new schemas does not require to create defaults manually + - key is validated, must be once in hierarchy else it won't be possible to store pype defaults + - `"is_group"` - define that all values under key in hierarchy will be overriden if any value is modified, this information is also stored to overrides + - this keys is not allowed for all inputs as they may have not reason for that + - key is validated, can be only once in hierarchy but is not required +- currently there are `system configurations` and `project configurations` + +## Inner schema +- GUI schemas are huge json files, to be able to split whole configuration into multiple schema there's type `schema` +- system configuration schemas are stored in `~/tools/settings/settings/gui_schemas/system_schema/` and project configurations in `~/tools/settings/settings/gui_schemas/projects_schema/` +- each schema name is filename of json file except extension (without ".json") + +### schema +- can have only key `"children"` which is list of strings, each string should represent another schema (order matters) string represebts name of the schema +- will just paste schemas from other schema file in order of "children" list + +``` +{ + "type": "schema", + "name": "my_schema_name" +} +``` + +## Basic Dictionary inputs +- these inputs wraps another inputs into {key: value} relation + +### dict-invisible +- this input gives ability to wrap another inputs but keep them in same widget without visible divider + - this is for example used as first input widget +- has required keys `"key"` and `"children"` + - "children" says what children inputs are underneath + - "key" is key under which will be stored value from it's children +- output is dictionary `{the "key": children values}` +- can't have `"is_group"` key set to True as it breaks visual override showing +``` +{ + "type": "dict-invisible", + "key": "global", + "children": [ + ...ITEMS... + ] +} +``` + +## dict +- this is another dictionary input wrapping more inputs but visually makes them different +- item may be used as widget (in `list` or `dict-modifiable`) + - in that case the only key modifier is `children` which is list of it's keys + - USAGE: e.g. List of dictionaries where each dictionary have same structure. +- item options if is not used as widget + - required keys are `"key"` under which will be stored and `"label"` which will be shown in GUI + - this input can be expandable + - that can be set with key `"expandable"` as `True`/`False` (Default: `True`) + - with key `"expanded"` as `True`/`False` can be set that is expanded when GUI is opened (Default: `False`) + - it is possible to add darker background with `"highlight_content"` (Default: `False`) + - darker background has limits of maximum applies after 3-4 nested highlighted items there is not difference in the color +``` +# Example +{ + "key": "applications", + "type": "dict", + "label": "Applications", + "expandable": true, + "highlight_content": true, + "is_group": true, + "is_file": true, + "children": [ + ...ITEMS... + ] +} + +# When used as widget +{ + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": "dict-item", + "input_modifiers": { + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, { + "key": "hosts", + "label": "Hosts", + "type": "list", + "object_type": "text" + } + ... + ] + } +} +``` + +## Inputs for setting any kind of value (`Pure` inputs) +- all these input must have defined `"key"` under which will be stored and `"label"` which will be shown next to input + - unless they are used in different types of inputs (later) "as widgets" in that case `"key"` and `"label"` are not required as there is not place where to set them + +### boolean +- simple checkbox, nothing more to set +``` +{ + "type": "boolean", + "key": "my_boolean_key", + "label": "Do you want to use Pype?" +} +``` + +### number +- number input, can be used for both integer and float + - key `"decimal"` defines how many decimal places will be used, 0 is for integer input (Default: `0`) + - key `"minimum"` as minimum allowed number to enter (Default: `-99999`) + - key `"maxium"` as maximum allowed number to enter (Default: `99999`) +``` +{ + "type": "number", + "key": "fps", + "label": "Frame rate (FPS)" + "decimal": 2, + "minimum": 1, + "maximum": 300000 +} +``` + +### text +- simple text input + - key `"multiline"` allows to enter multiple lines of text (Default: `False`) + - key `"placeholder"` allows to show text inside input when is empty (Default: `None`) + +``` +{ + "type": "text", + "key": "deadline_pool", + "label": "Deadline pool" +} +``` + +### path-input +- enhanced text input + - does not allow to enter backslash, is auto-converted to forward slash + - may be added another validations, like do not allow end path with slash +- this input is implemented to add additional features to text input +- this is meant to be used in proxy input `path-widget` + - DO NOT USE this input in schema please + +### raw-json +- a little bit enhanced text input for raw json +- has validations of json format + - empty value is invalid value, always must be at least `{}` of `[]` + +``` +{ + "type": "raw-json", + "key": "profiles", + "label": "Extract Review profiles" +} +``` + +## Inputs for setting value using Pure inputs +- these inputs also have required `"key"` and `"label"` +- they use Pure inputs "as widgets" + +### list +- output is list +- items can be added and removed +- items in list must be the same type + - type of items is defined with key `"object_type"` where Pure input name is entered (e.g. `number`) + - because Pure inputs may have modifiers (`number` input has `minimum`, `maximum` and `decimals`) you can set these in key `"input_modifiers"` + +``` +{ + "type": "list", + "object_type": "number", + "key": "exclude_ports", + "label": "Exclude ports", + "input_modifiers": { + "minimum": 1, + "maximum": 65535 + } +} +``` + +### dict-modifiable +- one of dictionary inputs, this is only used as value input +- items in this input can be removed and added same way as in `list` input +- value items in dictionary must be the same type + - type of items is defined with key `"object_type"` where Pure input name is entered (e.g. `number`) + - because Pure inputs may have modifiers (`number` input has `minimum`, `maximum` and `decimals`) you can set these in key `"input_modifiers"` +- this input can be expandable + - that can be set with key `"expandable"` as `True`/`False` (Default: `True`) + - with key `"expanded"` as `True`/`False` can be set that is expanded when GUI is opened (Default: `False`) + +``` +{ + "type": "dict-modifiable", + "object_type": "number", + "input_modifiers": { + "minimum": 0, + "maximum": 300 + }, + "is_group": true, + "key": "templates_mapping", + "label": "Muster - Templates mapping", + "is_file": true +} +``` + +### path-widget +- input for paths, use `path-input` internally +- has 2 input modifiers `"multiplatform"` and `"multipath"` + - `"multiplatform"` - adds `"windows"`, `"linux"` and `"darwin"` path inputs result is dictionary + - `"multipath"` - it is possible to enter multiple paths + - if both are enabled result is dictionary with lists + +``` +{ + "type": "path-widget", + "key": "ffmpeg_path", + "label": "FFmpeg path", + "multiplatform": true, + "multipath": true +} +``` + +### list-strict +- input for strict number of items in list +- each child item can be different type with different possible modifiers +- it is possible to display them in horizontal or vertical layout + - key `"horizontal"` as `True`/`False` (Default: `True`) +- each child may have defined `"label"` which is shown next to input + - label does not reflect modifications or overrides (TODO) +- children item are defined under key `"object_types"` which is list of dictionaries + - key `"children"` is not used because is used for hierarchy validations in schema +- USAGE: For colors, transformations, etc. Custom number and different modifiers + give ability to define if color is HUE or RGB, 0-255, 0-1, 0-100 etc. + +``` +{ + "type": "list-strict", + "key": "color", + "label": "Color", + "object_types": [ + { + "label": "Red", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Green", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Blue", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Alpha", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 6 + } + ] +} +``` + + +## Noninteractive widgets +- have nothing to do with data + +### label +- add label with note or explanations +- it is possible to use html tags inside the label + +``` +{ + "type": "label", + "label": "RED LABEL: Normal label" +} +``` + +### splitter +- visual splitter of items (more divider than splitter) + +``` +{ + "type": "splitter" +} +``` + +## Proxy wrappers +- should wraps multiple inputs only visually +- these does not have `"key"` key and do not allow to have `"is_file"` or `"is_group"` modifiers enabled + +### form +- DEPRECATED + - may be used only in `dict` and `dict-invisible` where is currently used grid layout so form is not needed + - item is kept as still may be used in specific cases +- wraps inputs into form look layout +- should be used only for Pure inputs + +``` +{ + "type": "dict-form", + "children": [ + { + "type": "text", + "key": "deadline_department", + "label": "Deadline apartment" + }, { + "type": "number", + "key": "deadline_priority", + "label": "Deadline priority" + }, { + ... + } + ] +} +``` diff --git a/pype/tools/settings/settings/__init__.py b/pype/tools/settings/settings/__init__.py new file mode 100644 index 0000000000..0c2fd6d4bb --- /dev/null +++ b/pype/tools/settings/settings/__init__.py @@ -0,0 +1,8 @@ +from . import style +from .widgets import MainWidget + + +__all__ = ( + "style", + "MainWidget" +) diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/0_project_gui_schema.json b/pype/tools/settings/settings/gui_schemas/projects_schema/0_project_gui_schema.json new file mode 100644 index 0000000000..cf95bf4c45 --- /dev/null +++ b/pype/tools/settings/settings/gui_schemas/projects_schema/0_project_gui_schema.json @@ -0,0 +1,30 @@ +{ + "key": "project", + "type": "dict-invisible", + "children": [ + { + "type": "anatomy", + "key": "project_anatomy", + "children": [ + { + "type": "anatomy_roots", + "key": "roots", + "is_file": true + }, { + "type": "anatomy_templates", + "key": "templates", + "is_file": true + } + ] + }, { + "type": "dict-invisible", + "key": "project_settings", + "children": [ + { + "type": "schema", + "name": "1_plugins_gui_schema" + } + ] + } + ] +} diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json b/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json new file mode 100644 index 0000000000..f357b51dc5 --- /dev/null +++ b/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json @@ -0,0 +1,745 @@ +{ + "type": "dict", + "collapsable": true, + "key": "plugins", + "label": "Plugins", + "children": [ + { + "type": "dict", + "collapsable": true, + "key": "celaction", + "label": "CelAction", + "children": [ + { + "type": "dict", + "collapsable": true, + "key": "publish", + "label": "Publish plugins", + "is_file": true, + "children": [ + { + "type": "dict", + "collapsable": true, + "checkbox_key": "enabled", + "key": "ExtractCelactionDeadline", + "label": "ExtractCelactionDeadline", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { + "type": "text", + "key": "deadline_department", + "label": "Deadline apartment" + }, { + "type": "number", + "key": "deadline_priority", + "label": "Deadline priority" + }, { + "type": "text", + "key": "deadline_pool", + "label": "Deadline pool" + }, { + "type": "text", + "key": "deadline_pool_secondary", + "label": "Deadline pool (secondary)" + }, { + "type": "text", + "key": "deadline_group", + "label": "Deadline Group" + }, { + "type": "number", + "key": "deadline_chunk_size", + "label": "Deadline Chunk size" + } + ] + } + ] + } + ] + }, { + "type": "dict", + "collapsable": true, + "key": "ftrack", + "label": "Ftrack", + "children": [ + { + "type": "dict", + "collapsable": true, + "key": "publish", + "label": "Publish plugins", + "is_file": true, + "children": [ + { + "type": "dict", + "collapsable": true, + "checkbox_key": "enabled", + "key": "IntegrateFtrackNote", + "label": "IntegrateFtrackNote", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { + "type": "text", + "key": "note_with_intent_template", + "label": "Note with intent template" + }, { + "type": "list", + "object_type": "text", + "key": "note_labels", + "label": "Note labels" + } + ] + } + ] + } + ] + }, { + "type": "dict", + "collapsable": true, + "key": "global", + "label": "Global", + "children": [ + { + "type": "dict", + "collapsable": true, + "key": "publish", + "label": "Publish plugins", + "is_file": true, + "children": [ + { + "type": "dict", + "collapsable": true, + "checkbox_key": "enabled", + "key": "IntegrateMasterVersion", + "label": "IntegrateMasterVersion", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] + }, { + "type": "dict", + "collapsable": true, + "checkbox_key": "enabled", + "key": "ExtractJpegEXR", + "label": "ExtractJpegEXR", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { + "type": "dict-invisible", + "key": "ffmpeg_args", + "children": [ + { + "type": "list", + "object_type": "text", + "key": "input", + "label": "FFmpeg input arguments" + }, { + "type": "list", + "object_type": "text", + "key": "output", + "label": "FFmpeg output arguments" + } + ] + } + ] + }, { + "type": "dict", + "collapsable": true, + "key": "ExtractReview", + "label": "ExtractReview", + "checkbox_key": "enabled", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": "dict", + "input_modifiers": { + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, { + "key": "hosts", + "label": "Hosts", + "type": "list", + "object_type": "text" + }, { + "type": "splitter" + }, { + "key": "outputs", + "label": "Output Definitions", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": "dict", + "input_modifiers": { + "children": [ + { + "key": "ext", + "label": "Output extension", + "type": "text" + }, { + "key": "tags", + "label": "Tags", + "type": "list", + "object_type": "text" + }, { + "key": "ffmpeg_args", + "label": "FFmpeg arguments", + "type": "dict", + "highlight_content": true, + "children": [ + { + "key": "video_filters", + "label": "Video filters", + "type": "list", + "object_type": "text" + }, { + "type": "splitter" + }, { + "key": "audio_filters", + "label": "Audio filters", + "type": "list", + "object_type": "text" + }, { + "type": "splitter" + }, { + "key": "input", + "label": "Input arguments", + "type": "list", + "object_type": "text" + }, { + "type": "splitter" + }, { + "key": "output", + "label": "Output arguments", + "type": "list", + "object_type": "text" + } + ] + }, { + "key": "filter", + "label": "Additional output filtering", + "type": "dict", + "highlight_content": true, + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + } + ] + } + ] + } + } + ] + } + } + ] + }, { + "type": "dict", + "collapsable": true, + "key": "ExtractBurnin", + "label": "ExtractBurnin", + "checkbox_key": "enabled", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { + "type": "dict", + "collapsable": true, + "key": "options", + "label": "Burnin formating options", + "children": [ + { + "type": "number", + "key": "font_size", + "label": "Font size" + }, { + "type": "number", + "key": "opacity", + "label": "Font opacity" + }, { + "type": "number", + "key": "bg_opacity", + "label": "Background opacity" + }, { + "type": "number", + "key": "x_offset", + "label": "X Offset" + }, { + "type": "number", + "key": "y_offset", + "label": "Y Offset" + }, { + "type": "number", + "key": "bg_padding", + "label": "Padding aroung text" + } + ] + }, { + "type": "raw-json", + "key": "profiles", + "label": "Burnin profiles" + } + ] + }, { + "type": "dict", + "collapsable": true, + "key": "IntegrateAssetNew", + "label": "IntegrateAssetNew", + "is_group": true, + "children": [ + { + "type": "raw-json", + "key": "template_name_profiles", + "label": "template_name_profiles" + } + ] + }, { + "type": "dict", + "collapsable": true, + "key": "ProcessSubmittedJobOnFarm", + "label": "ProcessSubmittedJobOnFarm", + "checkbox_key": "enabled", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { + "type": "text", + "key": "deadline_department", + "label": "Deadline department" + }, { + "type": "text", + "key": "deadline_pool", + "label": "Deadline Pool" + }, { + "type": "text", + "key": "deadline_group", + "label": "Deadline Group" + } + ] + } + ] + } + ] + }, { + "type": "dict", + "collapsable": true, + "key": "maya", + "label": "Maya", + "children": [ + { + "type": "dict", + "collapsable": true, + "key": "publish", + "label": "Publish plugins", + "is_file": true, + "children": [ + { + "type": "dict", + "collapsable": true, + "key": "ValidateModelName", + "label": "Validate Model Name", + "checkbox_key": "enabled", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { + "type": "text", + "key": "material_file", + "label": "Material File" + }, { + "type": "text", + "key": "regex", + "label": "Validation regex" + } + ] + }, { + "type": "dict", + "collapsable": true, + "key": "ValidateAssemblyName", + "label": "Validate Assembly Name", + "checkbox_key": "enabled", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] + }, { + "type": "dict", + "collapsable": true, + "key": "ValidateShaderName", + "label": "ValidateShaderName", + "checkbox_key": "enabled", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { + "type": "text", + "key": "regex", + "label": "Validation regex" + } + ] + }, { + "type": "dict", + "collapsable": true, + "key": "ValidateMeshHasOverlappingUVs", + "label": "ValidateMeshHasOverlappingUVs", + "checkbox_key": "enabled", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] + } + ] + }, { + "type": "raw-json", + "key": "workfile_build", + "label": "Workfile Build logic", + "is_file": true + } + ] + }, { + "type": "dict", + "collapsable": true, + "key": "nuke", + "label": "Nuke", + "children": [ + { + "type": "dict", + "collapsable": true, + "key": "create", + "label": "Create plugins", + "is_file": true, + "children": [ + { + "type": "dict", + "collapsable": false, + "key": "CreateWriteRender", + "label": "CreateWriteRender", + "is_group": true, + "children": [ + { + "type": "text", + "key": "fpath_template", + "label": "Path template" + } + ] + }, { + "type": "dict", + "collapsable": false, + "key": "CreateWritePrerender", + "label": "CreateWritePrerender", + "is_group": true, + "children": [ + { + "type": "text", + "key": "fpath_template", + "label": "Path template" + } + ] + } + ] + }, { + "type": "dict", + "collapsable": true, + "key": "publish", + "label": "Publish plugins", + "is_file": true, + "children": [ + { + "type": "dict", + "collapsable": true, + "checkbox_key": "enabled", + "key": "ExtractThumbnail", + "label": "ExtractThumbnail", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { + "type": "raw-json", + "key": "nodes", + "label": "Nodes" + } + ] + }, { + "type": "dict", + "collapsable": true, + "checkbox_key": "enabled", + "key": "ValidateNukeWriteKnobs", + "label": "ValidateNukeWriteKnobs", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { + "type": "raw-json", + "key": "knobs", + "label": "Knobs" + } + ] + }, { + "type": "dict", + "collapsable": true, + "checkbox_key": "enabled", + "key": "ExtractReviewDataLut", + "label": "ExtractReviewDataLut", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] + }, { + "type": "dict", + "collapsable": true, + "checkbox_key": "enabled", + "key": "ExtractReviewDataMov", + "label": "ExtractReviewDataMov", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { + "type": "boolean", + "key": "viewer_lut_raw", + "label": "Viewer LUT raw" + } + ] + }, { + "type": "dict", + "collapsable": true, + "key": "ExtractSlateFrame", + "label": "ExtractSlateFrame", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "viewer_lut_raw", + "label": "Viewer LUT raw" + } + ] + }, { + "type": "dict", + "collapsable": true, + "key": "NukeSubmitDeadline", + "label": "NukeSubmitDeadline", + "is_group": true, + "children": [ + { + "type": "number", + "key": "deadline_priority", + "label": "deadline_priority" + }, { + "type": "text", + "key": "deadline_pool", + "label": "deadline_pool" + }, { + "type": "text", + "key": "deadline_pool_secondary", + "label": "deadline_pool_secondary" + }, { + "type": "number", + "key": "deadline_chunk_size", + "label": "deadline_chunk_size" + } + ] + } + ] + }, { + "type": "raw-json", + "key": "workfile_build", + "label": "Workfile Build logic", + "is_file": true + } + ] + }, { + "type": "dict", + "collapsable": true, + "key": "nukestudio", + "label": "NukeStudio", + "children": [ + { + "type": "dict", + "collapsable": true, + "key": "publish", + "label": "Publish plugins", + "is_file": true, + "children": [ + { + "type": "dict", + "collapsable": true, + "checkbox_key": "enabled", + "key": "CollectInstanceVersion", + "label": "Collect Instance Version", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] + }, { + "type": "dict", + "collapsable": true, + "checkbox_key": "enabled", + "key": "ExtractReviewCutUpVideo", + "label": "Extract Review Cut Up Video", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { + "type": "list", + "object_type": "text", + "key": "tags_addition", + "label": "Tags addition" + } + ] + } + ] + } + ] + }, { + "type": "dict", + "collapsable": true, + "key": "resolve", + "label": "DaVinci Resolve", + "children": [ + { + "type": "dict", + "collapsable": true, + "key": "create", + "label": "Creator plugins", + "is_file": true, + "children": [ + { + "type": "dict", + "collapsable": true, + "key": "CreateShotClip", + "label": "Create Shot Clip", + "is_group": true, + "children": [ + { + "type": "text", + "key": "clipName", + "label": "Clip name template" + }, { + "type": "text", + "key": "folder", + "label": "Folder" + }, { + "type": "number", + "key": "steps", + "label": "Steps" + } + ] + } + + ] + } + ] + }, + { + "type": "dict", + "collapsable": true, + "key": "standalonepublisher", + "label": "Standalone Publisher", + "children": [ + { + "type": "dict", + "collapsable": true, + "key": "publish", + "label": "Publish plugins", + "is_file": true, + "children": [ + { + "type": "dict", + "collapsable": true, + "key": "ExtractThumbnailSP", + "label": "ExtractThumbnailSP", + "is_group": true, + "children": [ + { + "type": "dict", + "collapsable": false, + "key": "ffmpeg_args", + "label": "ffmpeg_args", + "children": [ + { + "type": "list", + "object_type": "text", + "key": "input", + "label": "input" + }, + { + "type": "list", + "object_type": "text", + "key": "output", + "label": "output" + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/0_system_gui_schema.json b/pype/tools/settings/settings/gui_schemas/system_schema/0_system_gui_schema.json new file mode 100644 index 0000000000..c5f229fc2f --- /dev/null +++ b/pype/tools/settings/settings/gui_schemas/system_schema/0_system_gui_schema.json @@ -0,0 +1,23 @@ +{ + "key": "system", + "type": "dict-invisible", + "children": [ + { + "type": "dict-invisible", + "key": "global", + "children": [{ + "type": "schema", + "name": "1_intents_gui_schema" + },{ + "type": "schema", + "name": "1_modules_gui_schema" + }, { + "type": "schema", + "name": "1_applications_gui_schema" + }, { + "type": "schema", + "name": "1_tools_gui_schema" + }] + } + ] +} diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_applications_gui_schema.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_applications_gui_schema.json new file mode 100644 index 0000000000..3427f98253 --- /dev/null +++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_applications_gui_schema.json @@ -0,0 +1,139 @@ +{ + "key": "applications", + "type": "dict", + "label": "Applications", + "collapsable": true, + "is_group": true, + "is_file": true, + "children": [ + { + "type": "boolean", + "key": "blender_2.80", + "label": "Blender 2.80" + }, { + "type": "boolean", + "key": "blender_2.81", + "label": "Blender 2.81" + }, { + "type": "boolean", + "key": "blender_2.82", + "label": "Blender 2.82" + }, { + "type": "boolean", + "key": "blender_2.83", + "label": "Blender 2.83" + }, { + "type": "boolean", + "key": "celaction_local", + "label": "Celaction Local" + }, { + "type": "boolean", + "key": "celaction_remote", + "label": "Celaction Remote" + }, { + "type": "boolean", + "key": "harmony_17", + "label": "Harmony 17" + }, { + "type": "boolean", + "key": "maya_2017", + "label": "Autodest Maya 2017" + }, { + "type": "boolean", + "key": "maya_2018", + "label": "Autodest Maya 2018" + }, { + "type": "boolean", + "key": "maya_2019", + "label": "Autodest Maya 2019" + }, { + "type": "boolean", + "key": "maya_2020", + "label": "Autodest Maya 2020" + }, { + "key": "nuke_10.0", + "type": "boolean", + "label": "Nuke 10.0" + }, { + "type": "boolean", + "key": "nuke_11.2", + "label": "Nuke 11.2" + }, { + "type": "boolean", + "key": "nuke_11.3", + "label": "Nuke 11.3" + }, { + "type": "boolean", + "key": "nuke_12.0", + "label": "Nuke 12.0" + }, { + "type": "boolean", + "key": "nukex_10.0", + "label": "NukeX 10.0" + }, { + "type": "boolean", + "key": "nukex_11.2", + "label": "NukeX 11.2" + }, { + "type": "boolean", + "key": "nukex_11.3", + "label": "NukeX 11.3" + }, { + "type": "boolean", + "key": "nukex_12.0", + "label": "NukeX 12.0" + }, { + "type": "boolean", + "key": "nukestudio_10.0", + "label": "NukeStudio 10.0" + }, { + "type": "boolean", + "key": "nukestudio_11.2", + "label": "NukeStudio 11.2" + }, { + "type": "boolean", + "key": "nukestudio_11.3", + "label": "NukeStudio 11.3" + }, { + "type": "boolean", + "key": "nukestudio_12.0", + "label": "NukeStudio 12.0" + }, { + "type": "boolean", + "key": "houdini_16", + "label": "Houdini 16" + }, { + "type": "boolean", + "key": "houdini_16.5", + "label": "Houdini 16.5" + }, { + "type": "boolean", + "key": "houdini_17", + "label": "Houdini 17" + }, { + "type": "boolean", + "key": "houdini_18", + "label": "Houdini 18" + }, { + "type": "boolean", + "key": "premiere_2019", + "label": "Premiere 2019" + }, { + "type": "boolean", + "key": "premiere_2020", + "label": "Premiere 2020" + }, { + "type": "boolean", + "key": "resolve_16", + "label": "BM DaVinci Resolve 16" + }, { + "type": "boolean", + "key": "storyboardpro_7", + "label": "Storyboard Pro 7" + }, { + "type": "boolean", + "key": "unreal_4.24", + "label": "Unreal Editor 4.24" + } + ] +} diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json new file mode 100644 index 0000000000..0578968508 --- /dev/null +++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json @@ -0,0 +1,372 @@ +{ + "key": "example_dict", + "label": "Examples", + "type": "dict", + "is_file": true, + "children": [ + { + "key": "dict_wrapper", + "type": "dict-invisible", + "children": [ + { + "type": "boolean", + "key": "bool", + "label": "Boolean checkbox" + }, { + "type": "label", + "label": "NOTE: This is label" + }, { + "type": "splitter" + }, { + "type": "number", + "key": "integer", + "label": "Integer", + "decimal": 0, + "minimum": 0, + "maximum": 10 + }, { + "type": "number", + "key": "float", + "label": "Float (2 decimals)", + "decimal": 2, + "minimum": -10, + "maximum": -5 + }, { + "type": "text", + "key": "singleline_text", + "label": "Singleline text" + }, { + "type": "text", + "key": "multiline_text", + "label": "Multiline text", + "multiline": true + }, { + "type": "raw-json", + "key": "raw_json", + "label": "Raw json input" + }, { + "type": "list", + "key": "list_item_of_multiline_texts", + "label": "List of multiline texts", + "object_type": "text", + "input_modifiers": { + "multiline": true + } + }, { + "type": "list", + "key": "list_item_of_floats", + "label": "List of floats", + "object_type": "number", + "input_modifiers": { + "decimal": 3, + "minimum": 1000, + "maximum": 2000 + } + }, { + "type": "dict-modifiable", + "key": "modifiable_dict_of_integers", + "label": "Modifiable dict of integers", + "object_type": "number", + "input_modifiers": { + "decimal": 0, + "minimum": 10, + "maximum": 100 + } + }, { + "type": "list-strict", + "key": "strict_list_labels_horizontal", + "label": "StrictList-labels-horizontal (color)", + "object_types": [ + { + "label": "Red", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Green", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Blue", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Alpha", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 6 + } + ] + }, { + "type": "list-strict", + "key": "strict_list_labels_vertical", + "label": "StrictList-labels-vertical (color)", + "horizontal": false, + "object_types": [ + { + "label": "Red", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Green", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Blue", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Alpha", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 6 + } + ] + }, { + "type": "list-strict", + "key": "strict_list_nolabels_horizontal", + "label": "StrictList-nolabels-horizontal (color)", + "object_types": [ + { + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 6 + } + ] + }, { + "type": "list-strict", + "key": "strict_list_nolabels_vertical", + "label": "StrictList-nolabels-vertical (color)", + "horizontal": false, + "object_types": [ + { + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 6 + } + ] + }, { + "type": "list", + "key": "dict_item", + "label": "DictItem in List", + "object_type": "dict-item", + "input_modifiers": { + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, { + "key": "hosts", + "label": "Hosts", + "type": "list", + "object_type": "text" + } + ] + } + }, { + "type": "path-widget", + "key": "single_path_input", + "label": "Single path input", + "multiplatform": false, + "multipath": false + }, { + "type": "path-widget", + "key": "multi_path_input", + "label": "Multi path input", + "multiplatform": false, + "multipath": true + }, { + "type": "path-widget", + "key": "single_os_specific_path_input", + "label": "Single OS specific path input", + "multiplatform": true, + "multipath": false + }, { + "type": "path-widget", + "key": "multi_os_specific_path_input", + "label": "Multi OS specific path input", + "multiplatform": true, + "multipath": true + }, { + "key": "collapsable", + "type": "dict", + "label": "collapsable dictionary", + "collapsable": true, + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "_nothing", + "label": "Exmaple input" + } + ] + }, { + "key": "collapsable_expanded", + "type": "dict", + "label": "collapsable dictionary, expanded on creation", + "collapsable": true, + "collapsed": false, + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "_nothing", + "label": "Exmaple input" + } + ] + }, { + "key": "not_collapsable", + "type": "dict", + "label": "Not collapsable", + "collapsable": false, + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "_nothing", + "label": "Exmaple input" + } + ] + }, { + "key": "nested_dict_lvl1", + "type": "dict", + "label": "Nested dictionary (level 1)", + "children": [ + { + "key": "nested_dict_lvl2", + "type": "dict", + "label": "Nested dictionary (level 2)", + "is_group": true, + "children": [ + { + "key": "nested_dict_lvl3", + "type": "dict", + "label": "Nested dictionary (level 3)", + "children": [ + { + "type": "boolean", + "key": "_nothing", + "label": "Exmaple input" + } + ] + }, { + "key": "nested_dict_lvl3_2", + "type": "dict", + "label": "Nested dictionary (level 3) (2)", + "children": [ + { + "type": "text", + "key": "_nothing", + "label": "Exmaple input" + }, { + "type": "text", + "key": "_nothing2", + "label": "Exmaple input 2" + } + ] + } + ] + } + ] + }, { + "key": "form_examples", + "type": "dict", + "label": "Form examples", + "children": [ + { + "key": "inputs_without_form_example", + "type": "dict", + "label": "Inputs without form", + "children": [ + { + "type": "text", + "key": "_nothing_1", + "label": "Example label" + }, { + "type": "text", + "key": "_nothing_2", + "label": "Example label ####" + }, { + "type": "text", + "key": "_nothing_3", + "label": "Example label ########" + } + ] + }, { + "key": "inputs_with_form_example", + "type": "dict", + "label": "Inputs with form", + "children": [ + { + "type": "form", + "children": [ + { + "type": "text", + "key": "_nothing_1", + "label": "Example label" + }, { + "type": "text", + "key": "_nothing_2", + "label": "Example label ####" + }, { + "type": "text", + "key": "_nothing_3", + "label": "Example label ########" + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_intents_gui_schema.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_intents_gui_schema.json new file mode 100644 index 0000000000..7f71da26cd --- /dev/null +++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_intents_gui_schema.json @@ -0,0 +1,16 @@ +{ + "key": "general", + "type": "dict", + "label": "General", + "collapsable": true, + "is_file": true, + "children": [{ + "key": "studio_name", + "type": "text", + "label": "Studio Name" + },{ + "key": "studio_code", + "type": "text", + "label": "Studio Short Code" + } +]} diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_modules_gui_schema.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_modules_gui_schema.json new file mode 100644 index 0000000000..7b3d8cdd13 --- /dev/null +++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_modules_gui_schema.json @@ -0,0 +1,283 @@ +{ + "key": "modules", + "type": "dict", + "label": "Modules", + "collapsable": true, + "is_file": true, + "children": [{ + "type": "dict", + "key": "Avalon", + "label": "Avalon", + "collapsable": true, + "children": [ + { + "type": "text", + "key": "AVALON_MONGO", + "label": "Avalon Mongo URL" + }, + { + "type": "text", + "key": "AVALON_DB_DATA", + "label": "Avalon Mongo Data Location" + }, + { + "type": "text", + "key": "AVALON_THUMBNAIL_ROOT", + "label": "Thumbnail Storage Location" + } + ] + },{ + "type": "dict", + "key": "Ftrack", + "label": "Ftrack", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "text", + "key": "ftrack_server", + "label": "Server" + }, + { + "type": "label", + "label": "Additional Ftrack paths" + }, + { + "type": "list", + "key": "ftrack_actions_path", + "label": "Action paths", + "object_type": "text" + }, + { + "type": "list", + "key": "ftrack_events_path", + "label": "Event paths", + "object_type": "text" + }, + { + "type": "label", + "label": "Ftrack event server advanced settings" + }, + { + "type": "text", + "key": "FTRACK_EVENTS_MONGO_DB", + "label": "Event Mongo DB" + }, + { + "type": "text", + "key": "FTRACK_EVENTS_MONGO_COL", + "label": "Events Mongo Collection" + }, + { + "type": "dict", + "key": "sync_to_avalon", + "label": "Sync to avalon", + "children": [{ + "type": "list", + "key": "statuses_name_change", + "label": "Status name change", + "object_type": "text", + "input_modifiers": { + "multiline": false + } + }] + }, + { + "type": "dict-modifiable", + "key": "status_version_to_task", + "label": "Version to Task status mapping", + "object_type": "text" + }, + { + "type": "dict-modifiable", + "key": "status_update", + "label": "Status Updates", + "object_type": "list", + "input_modifiers": { + "object_type": "text" + } + }, + { + "key": "intent", + "type": "dict-invisible", + "children": [ + { + "type": "dict-modifiable", + "object_type": "text", + "key": "items", + "label": "Intent Key/Label" + }, + { + "key": "default", + "type": "text", + "label": "Defautl Intent" + } + ] + } + ] + }, { + "type": "dict", + "key": "Rest Api", + "label": "Rest Api", + "collapsable": true, + "children": [{ + "type": "number", + "key": "default_port", + "label": "Default Port", + "minimum": 1, + "maximum": 65535 + }, + { + "type": "list", + "object_type": "number", + "key": "exclude_ports", + "label": "Exclude ports", + "input_modifiers": { + "minimum": 1, + "maximum": 65535 + } + } + ] + }, { + "type": "dict", + "key": "Timers Manager", + "label": "Timers Manager", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "number", + "decimal": 2, + "key": "full_time", + "label": "Max idle time" + }, { + "type": "number", + "decimal": 2, + "key": "message_time", + "label": "When dialog will show" + } + ] + }, { + "type": "dict", + "key": "Clockify", + "label": "Clockify", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "text", + "key": "workspace_name", + "label": "Workspace name" + } + ] + }, { + "type": "dict", + "key": "Deadline", + "label": "Deadline", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + },{ + "type": "text", + "key": "DEADLINE_REST_URL", + "label": "Deadline Resl URL" + }] + }, { + "type": "dict", + "key": "Muster", + "label": "Muster", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + },{ + "type": "text", + "key": "MUSTER_REST_URL", + "label": "Muster Resl URL" + },{ + "type": "dict-modifiable", + "object_type": "number", + "input_modifiers": { + "minimum": 0, + "maximum": 300 + }, + "is_group": true, + "key": "templates_mapping", + "label": "Templates mapping", + "is_file": true + }] + }, { + "type": "dict", + "key": "Logging", + "label": "Logging", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }] + }, { + "type": "dict", + "key": "Adobe Communicator", + "label": "Adobe Communicator", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }] + }, { + "type": "dict", + "key": "User setting", + "label": "User setting", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }] + }, { + "type": "dict", + "key": "Standalone Publish", + "label": "Standalone Publish", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }] + }, { + "type": "dict", + "key": "Idle Manager", + "label": "Idle Manager", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }] + } + ] +} diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_tools_gui_schema.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_tools_gui_schema.json new file mode 100644 index 0000000000..08b8d13d89 --- /dev/null +++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_tools_gui_schema.json @@ -0,0 +1,27 @@ +{ + "key": "tools", + "type": "dict", + "label": "Tools", + "collapsable": true, + "is_group": true, + "is_file": true, + "children": [ + { + "key": "mtoa_3.0.1", + "type": "boolean", + "label": "Arnold Maya 3.0.1" + }, { + "key": "mtoa_3.1.1", + "type": "boolean", + "label": "Arnold Maya 3.1.1" + }, { + "key": "mtoa_3.2.0", + "type": "boolean", + "label": "Arnold Maya 3.2.0" + }, { + "key": "yeti_2.1.2", + "type": "boolean", + "label": "Yeti 2.1.2" + } + ] +} diff --git a/pype/tools/settings/settings/style/__init__.py b/pype/tools/settings/settings/style/__init__.py new file mode 100644 index 0000000000..a8f202d97b --- /dev/null +++ b/pype/tools/settings/settings/style/__init__.py @@ -0,0 +1,12 @@ +import os + + +def load_stylesheet(): + style_path = os.path.join(os.path.dirname(__file__), "style.css") + with open(style_path, "r") as style_file: + stylesheet = style_file.read() + return stylesheet + + +def app_icon_path(): + return os.path.join(os.path.dirname(__file__), "pype_icon.png") diff --git a/pype/tools/settings/settings/style/pype_icon.png b/pype/tools/settings/settings/style/pype_icon.png new file mode 100644 index 0000000000..bfacf6eeed Binary files /dev/null and b/pype/tools/settings/settings/style/pype_icon.png differ diff --git a/pype/tools/settings/settings/style/style.css b/pype/tools/settings/settings/style/style.css new file mode 100644 index 0000000000..3f648abef8 --- /dev/null +++ b/pype/tools/settings/settings/style/style.css @@ -0,0 +1,321 @@ +QWidget { + color: #bfccd6; + background-color: #293742; + font-size: 12px; + border-radius: 0px; +} + +QMenu { + border: 1px solid #555555; + background-color: #1d272f; +} + +QMenu::item { + padding: 5px 10px 5px 10px; + border-left: 5px solid #313131; +} + +QMenu::item:selected { + border-left-color: #61839e; + background-color: #222d37; +} +QCheckBox { + spacing: 0px; +} +QCheckBox::indicator {} +QCheckBox::indicator:focus {} + +QLineEdit, QSpinBox, QDoubleSpinBox, QPlainTextEdit, QTextEdit { + border: 1px solid #aaaaaa; + border-radius: 3px; + background-color: #1d272f; +} + +QLineEdit:disabled, QSpinBox:disabled, QDoubleSpinBox:disabled, QPlainTextEdit:disabled, QTextEdit:disabled, QPushButton:disabled { + background-color: #4e6474; +} + +QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus, QPlainTextEdit:focus, QTextEdit:focus { + border: 1px solid #ffffff; +} +QToolButton { + background: transparent; +} + +QLabel { + background: transparent; + color: #7390a5; +} +QLabel:hover {color: #839caf;} + +QLabel[state="studio"] {color: #bfccd6;} +QLabel[state="studio"]:hover {color: #ffffff;} +QLabel[state="modified"] {color: #137cbd;} +QLabel[state="modified"]:hover {color: #1798e8;} +QLabel[state="overriden-modified"] {color: #137cbd;} +QLabel[state="overriden-modified"]:hover {color: #1798e8;} +QLabel[state="overriden"] {color: #ff8c1a;} +QLabel[state="overriden"]:hover {color: #ffa64d;} +QLabel[state="invalid"] {color: #ad2e2e;} +QLabel[state="invalid"]:hover {color: #ad2e2e;} + + +QWidget[input-state="studio"] {border-color: #bfccd6;} +QWidget[input-state="modified"] {border-color: #137cbd;} +QWidget[input-state="overriden-modified"] {border-color: #137cbd;} +QWidget[input-state="overriden"] {border-color: #ff8c1a;} +QWidget[input-state="invalid"] {border-color: #ad2e2e;} + +QPushButton { + border: 1px solid #aaaaaa; + border-radius: 3px; + padding: 5px; +} +QPushButton:hover { + background-color: #31424e; +} +QPushButton[btn-type="tool-item"] { + border: 1px solid #bfccd6; + border-radius: 10px; +} + +QPushButton[btn-type="tool-item"]:hover { + border-color: #137cbd; + color: #137cbd; + background-color: transparent; +} + +QPushButton[btn-type="expand-toggle"] { + background: #1d272f; +} + +#GroupWidget { + border-bottom: 1px solid #1d272f; +} + +#ProjectListWidget QListView { + border: 1px solid #aaaaaa; + background: #1d272f; +} +#ProjectListWidget QLabel { + background: transparent; + font-weight: bold; +} + +#DictKey[state="studio"] {border-color: #bfccd6;} +#DictKey[state="modified"] {border-color: #137cbd;} +#DictKey[state="overriden"] {border-color: #00f;} +#DictKey[state="overriden-modified"] {border-color: #0f0;} +#DictKey[state="invalid"] {border-color: #ad2e2e;} + +#DictLabel { + font-weight: bold; +} + +#ContentWidget { + background-color: transparent; +} +#ContentWidget[content_state="hightlighted"] { + background-color: rgba(19, 26, 32, 15%); +} + +#SideLineWidget { + background-color: #31424e; + border-style: solid; + border-color: #3b4f5e; + border-left-width: 3px; + border-bottom-width: 0px; + border-right-width: 0px; + border-top-width: 0px; +} + +#SideLineWidget:hover { + border-color: #58768d; +} + +#SideLineWidget[state="child-studio"] {border-color: #455c6e;} +#SideLineWidget[state="child-studio"]:hover {border-color: #62839d;} + +#SideLineWidget[state="child-modified"] {border-color: #106aa2;} +#SideLineWidget[state="child-modified"]:hover {border-color: #137cbd;} + +#SideLineWidget[state="child-invalid"] {border-color: #ad2e2e;} +#SideLineWidget[state="child-invalid"]:hover {border-color: #c93636;} + +#SideLineWidget[state="child-overriden"] {border-color: #e67300;} +#SideLineWidget[state="child-overriden"]:hover {border-color: #ff8c1a;} + +#SideLineWidget[state="child-overriden-modified"] {border-color: #106aa2;} +#SideLineWidget[state="child-overriden-modified"]:hover {border-color: #137cbd;} + +#MainWidget { + background: #141a1f; +} + +#DictAsWidgetBody{ + background: transparent; + border: 2px solid #cccccc; + border-radius: 5px; +} + +#SplitterItem { + background-color: #1d272f; +} + +QTabWidget::pane { + border-top-style: none; +} + +QTabBar { + background: transparent; +} + +QTabBar::tab { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + padding: 5px; +} + +QTabBar::tab:selected { + background: #293742; + border-color: #9B9B9B; + border-bottom-color: #C2C7CB; +} + +QTabBar::tab:!selected { + margin-top: 2px; + background: #1d272f; +} + +QTabBar::tab:!selected:hover { + background: #3b4f5e; +} + + + +QTabBar::tab:first:selected { + margin-left: 0; +} + +QTabBar::tab:last:selected { + margin-right: 0; +} + +QTabBar::tab:only-one { + margin: 0; +} + +QScrollBar:horizontal { + height: 15px; + margin: 3px 15px 3px 15px; + border: 1px transparent #1d272f; + border-radius: 4px; + background-color: #1d272f; +} + +QScrollBar::handle:horizontal { + background-color: #61839e; + min-width: 5px; + border-radius: 4px; +} + +QScrollBar::add-line:horizontal { + margin: 0px 3px 0px 3px; + border-image: url(:/qss_icons/rc/right_arrow_disabled.png); + width: 10px; + height: 10px; + subcontrol-position: right; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:horizontal { + margin: 0px 3px 0px 3px; + border-image: url(:/qss_icons/rc/left_arrow_disabled.png); + height: 10px; + width: 10px; + subcontrol-position: left; + subcontrol-origin: margin; +} + +QScrollBar::add-line:horizontal:hover,QScrollBar::add-line:horizontal:on { + border-image: url(:/qss_icons/rc/right_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: right; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:horizontal:hover, QScrollBar::sub-line:horizontal:on { + border-image: url(:/qss_icons/rc/left_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: left; + subcontrol-origin: margin; +} + +QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal { + background: none; +} + +QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { + background: none; +} + +QScrollBar:vertical { + background-color: #1d272f; + width: 15px; + margin: 15px 3px 15px 3px; + border: 1px transparent #1d272f; + border-radius: 4px; +} + +QScrollBar::handle:vertical { + background-color: #61839e; + min-height: 5px; + border-radius: 4px; +} + +QScrollBar::sub-line:vertical { + margin: 3px 0px 3px 0px; + border-image: url(:/qss_icons/rc/up_arrow_disabled.png); + height: 10px; + width: 10px; + subcontrol-position: top; + subcontrol-origin: margin; +} + +QScrollBar::add-line:vertical { + margin: 3px 0px 3px 0px; + border-image: url(:/qss_icons/rc/down_arrow_disabled.png); + height: 10px; + width: 10px; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:vertical:hover,QScrollBar::sub-line:vertical:on { + + border-image: url(:/qss_icons/rc/up_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: top; + subcontrol-origin: margin; +} + + +QScrollBar::add-line:vertical:hover, QScrollBar::add-line:vertical:on { + border-image: url(:/qss_icons/rc/down_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical { + background: none; +} + + +QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { + background: none; +} diff --git a/pype/tools/settings/settings/widgets/__init__.py b/pype/tools/settings/settings/widgets/__init__.py new file mode 100644 index 0000000000..361fd9d23d --- /dev/null +++ b/pype/tools/settings/settings/widgets/__init__.py @@ -0,0 +1,9 @@ +from .window import MainWidget +from . import item_types +from . import anatomy_types + +__all__ = [ + "MainWidget", + "item_types", + "anatomy_types" +] diff --git a/pype/tools/settings/settings/widgets/anatomy_types.py b/pype/tools/settings/settings/widgets/anatomy_types.py new file mode 100644 index 0000000000..6d7b3292ce --- /dev/null +++ b/pype/tools/settings/settings/widgets/anatomy_types.py @@ -0,0 +1,758 @@ +from Qt import QtWidgets, QtCore +from .widgets import ExpandingWidget +from .item_types import ( + SettingObject, ModifiableDict, PathWidget, RawJsonWidget +) +from .lib import NOT_SET, TypeToKlass, CHILD_OFFSET, METADATA_KEY + + +class AnatomyWidget(QtWidgets.QWidget, SettingObject): + value_changed = QtCore.Signal(object) + template_keys = ( + "project[name]", + "project[code]", + "asset", + "task", + "subset", + "family", + "version", + "ext", + "representation" + ) + default_exmaple_data = { + "project": { + "name": "ProjectPype", + "code": "pp", + }, + "asset": "sq01sh0010", + "task": "compositing", + "subset": "renderMain", + "family": "render", + "version": 1, + "ext": ".png", + "representation": "png" + } + + def __init__( + self, input_data, parent, as_widget=False, label_widget=None + ): + if as_widget: + raise TypeError( + "`AnatomyWidget` does not allow to be used as widget." + ) + super(AnatomyWidget, self).__init__(parent) + self.setObjectName("AnatomyWidget") + + self.initial_attributes(input_data, parent, as_widget) + + self.key = input_data["key"] + + children_data = input_data["children"] + roots_input_data = {} + templates_input_data = {} + for child in children_data: + if child["type"] == "anatomy_roots": + roots_input_data = child + elif child["type"] == "anatomy_templates": + templates_input_data = child + + self.root_widget = RootsWidget(roots_input_data, self) + self.templates_widget = TemplatesWidget(templates_input_data, self) + + self.setAttribute(QtCore.Qt.WA_StyledBackground) + + body_widget = ExpandingWidget("Anatomy", self) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(body_widget) + + content_widget = QtWidgets.QWidget(body_widget) + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0) + content_layout.setSpacing(5) + + content_layout.addWidget(self.root_widget) + content_layout.addWidget(self.templates_widget) + + body_widget.set_content_widget(content_widget) + + self.body_widget = body_widget + self.label_widget = body_widget.label_widget + + self.root_widget.value_changed.connect(self._on_value_change) + self.templates_widget.value_changed.connect(self._on_value_change) + + def update_default_values(self, parent_values): + self._state = None + self._child_state = None + + if isinstance(parent_values, dict): + value = parent_values.get(self.key, NOT_SET) + else: + value = NOT_SET + + self.root_widget.update_default_values(value) + self.templates_widget.update_default_values(value) + + def update_studio_values(self, parent_values): + self._state = None + self._child_state = None + + if isinstance(parent_values, dict): + value = parent_values.get(self.key, NOT_SET) + else: + value = NOT_SET + + self.root_widget.update_studio_values(value) + self.templates_widget.update_studio_values(value) + + def apply_overrides(self, parent_values): + # Make sure this is set to False + self._state = None + self._child_state = None + + value = NOT_SET + if parent_values is not NOT_SET: + value = parent_values.get(self.key, value) + + self.root_widget.apply_overrides(value) + self.templates_widget.apply_overrides(value) + + def set_value(self, value): + raise TypeError("AnatomyWidget does not allow to use `set_value`") + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + self.hierarchical_style_update() + + self.value_changed.emit(self) + + def update_style(self, is_overriden=None): + child_has_studio_override = self.child_has_studio_override + child_modified = self.child_modified + child_invalid = self.child_invalid + child_state = self.style_state( + child_has_studio_override, + child_invalid, + self.child_overriden, + child_modified + ) + if child_state: + child_state = "child-{}".format(child_state) + + if child_state != self._child_state: + self.body_widget.side_line_widget.setProperty("state", child_state) + self.body_widget.side_line_widget.style().polish( + self.body_widget.side_line_widget + ) + self._child_state = child_state + + def hierarchical_style_update(self): + self.root_widget.hierarchical_style_update() + self.templates_widget.hierarchical_style_update() + self.update_style() + + @property + def child_has_studio_override(self): + return ( + self.root_widget.child_has_studio_override + or self.templates_widget.child_has_studio_override + ) + + @property + def child_modified(self): + return ( + self.root_widget.child_modified + or self.templates_widget.child_modified + ) + + @property + def child_overriden(self): + return ( + self.root_widget.child_overriden + or self.templates_widget.child_overriden + ) + + @property + def child_invalid(self): + return ( + self.root_widget.child_invalid + or self.templates_widget.child_invalid + ) + + def set_as_overriden(self): + self.root_widget.set_as_overriden() + self.templates_widget.set_as_overriden() + + def remove_overrides(self): + self.root_widget.remove_overrides() + self.templates_widget.remove_overrides() + + def reset_to_pype_default(self): + self.root_widget.reset_to_pype_default() + self.templates_widget.reset_to_pype_default() + + def set_studio_default(self): + self.root_widget.set_studio_default() + self.templates_widget.set_studio_default() + + def discard_changes(self): + self.root_widget.discard_changes() + self.templates_widget.discard_changes() + + def overrides(self): + if self.child_overriden: + return self.config_value(), True + return NOT_SET, False + + def item_value(self): + output = {} + output.update(self.root_widget.config_value()) + output.update(self.templates_widget.config_value()) + return output + + def studio_overrides(self): + if ( + self.root_widget.child_has_studio_override + or self.templates_widget.child_has_studio_override + ): + groups = [self.root_widget.key, self.templates_widget.key] + value = self.config_value() + value[self.key][METADATA_KEY] = {"groups": groups} + return value, True + return NOT_SET, False + + def config_value(self): + return {self.key: self.item_value()} + + +class RootsWidget(QtWidgets.QWidget, SettingObject): + value_changed = QtCore.Signal(object) + + def __init__(self, input_data, parent): + super(RootsWidget, self).__init__(parent) + self.setObjectName("RootsWidget") + + input_data["is_group"] = True + self.initial_attributes(input_data, parent, False) + + self.key = input_data["key"] + + self._multiroot_state = None + self.default_is_multiroot = False + self.studio_is_multiroot = False + self.was_multiroot = NOT_SET + + checkbox_widget = QtWidgets.QWidget(self) + multiroot_label = QtWidgets.QLabel( + "Use multiple roots", checkbox_widget + ) + multiroot_checkbox = QtWidgets.QCheckBox(checkbox_widget) + + checkbox_layout = QtWidgets.QHBoxLayout(checkbox_widget) + checkbox_layout.addWidget(multiroot_label, 0) + checkbox_layout.addWidget(multiroot_checkbox, 1) + + body_widget = ExpandingWidget("Roots", self) + content_widget = QtWidgets.QWidget(body_widget) + + path_widget_data = { + "key": self.key, + "multipath": False, + "multiplatform": True + } + singleroot_widget = PathWidget( + path_widget_data, self, + as_widget=True, parent_widget=content_widget + ) + multiroot_data = { + "key": self.key, + "object_type": "path-widget", + "expandable": False, + "input_modifiers": { + "multiplatform": True + } + } + multiroot_widget = ModifiableDict( + multiroot_data, self, + as_widget=True, parent_widget=content_widget + ) + + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.addWidget(checkbox_widget) + content_layout.addWidget(singleroot_widget) + content_layout.addWidget(multiroot_widget) + + body_widget.set_content_widget(content_widget) + self.label_widget = body_widget.label_widget + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(body_widget) + + self.body_widget = body_widget + self.multiroot_label = multiroot_label + self.multiroot_checkbox = multiroot_checkbox + self.singleroot_widget = singleroot_widget + self.multiroot_widget = multiroot_widget + + multiroot_checkbox.stateChanged.connect(self._on_multiroot_checkbox) + singleroot_widget.value_changed.connect(self._on_value_change) + multiroot_widget.value_changed.connect(self._on_value_change) + + self._on_multiroot_checkbox() + + @property + def is_multiroot(self): + return self.multiroot_checkbox.isChecked() + + def update_default_values(self, parent_values): + self._state = None + self._multiroot_state = None + self._is_modified = False + + if isinstance(parent_values, dict): + value = parent_values.get(self.key, NOT_SET) + else: + value = NOT_SET + + is_multiroot = False + if isinstance(value, dict): + for _value in value.values(): + if isinstance(_value, dict): + is_multiroot = True + break + + self.default_is_multiroot = is_multiroot + self.was_multiroot = is_multiroot + self.set_multiroot(is_multiroot) + + self._has_studio_override = False + self._had_studio_override = False + if is_multiroot: + for _value in value.values(): + singleroot_value = _value + break + + multiroot_value = value + else: + singleroot_value = value + multiroot_value = {"": value} + + self.singleroot_widget.update_default_values(singleroot_value) + self.multiroot_widget.update_default_values(multiroot_value) + + def update_studio_values(self, parent_values): + self._state = None + self._multiroot_state = None + self._is_modified = False + + if isinstance(parent_values, dict): + value = parent_values.get(self.key, NOT_SET) + else: + value = NOT_SET + + if value is NOT_SET: + is_multiroot = self.default_is_multiroot + self.studio_is_multiroot = NOT_SET + self._has_studio_override = False + self._had_studio_override = False + else: + is_multiroot = False + if isinstance(value, dict): + for _value in value.values(): + if isinstance(_value, dict): + is_multiroot = True + break + self.studio_is_multiroot = is_multiroot + self._has_studio_override = True + self._had_studio_override = True + + self.was_multiroot = is_multiroot + self.set_multiroot(is_multiroot) + + if is_multiroot: + self.multiroot_widget.update_studio_values(value) + else: + self.singleroot_widget.update_studio_values(value) + + def apply_overrides(self, parent_values): + # Make sure this is set to False + self._state = None + self._multiroot_state = None + self._is_modified = False + + value = NOT_SET + if parent_values is not NOT_SET: + value = parent_values.get(self.key, value) + + if value is NOT_SET: + is_multiroot = self.studio_is_multiroot + if is_multiroot is NOT_SET: + is_multiroot = self.default_is_multiroot + else: + is_multiroot = False + if isinstance(value, dict): + for _value in value.values(): + if isinstance(_value, dict): + is_multiroot = True + break + + self.was_multiroot = is_multiroot + self.set_multiroot(is_multiroot) + + if is_multiroot: + self._is_overriden = value is not NOT_SET + self._was_overriden = bool(self._is_overriden) + self.multiroot_widget.apply_overrides(value) + else: + self._is_overriden = value is not NOT_SET + self._was_overriden = bool(self._is_overriden) + self.singleroot_widget.apply_overrides(value) + + def hierarchical_style_update(self): + self.singleroot_widget.hierarchical_style_update() + self.multiroot_widget.hierarchical_style_update() + self.update_style() + + def update_style(self): + multiroot_state = self.style_state( + self.has_studio_override, + False, + False, + self.was_multiroot != self.is_multiroot + ) + if multiroot_state != self._multiroot_state: + self.multiroot_label.setProperty("state", multiroot_state) + self.multiroot_label.style().polish(self.multiroot_label) + self._multiroot_state = multiroot_state + + state = self.style_state( + self.has_studio_override, + self.child_invalid, + self.is_overriden, + self.is_modified + ) + if self._state == state: + return + + if state: + child_state = "child-{}".format(state) + else: + child_state = "" + + self.body_widget.side_line_widget.setProperty("state", child_state) + self.body_widget.side_line_widget.style().polish( + self.body_widget.side_line_widget + ) + + self.label_widget.setProperty("state", state) + self.label_widget.style().polish(self.label_widget) + + self._state = state + + def _on_multiroot_checkbox(self): + self.set_multiroot() + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + if item is not None and ( + (self.is_multiroot and item != self.multiroot_widget) + or (not self.is_multiroot and item != self.singleroot_widget) + ): + return + + if self.is_group and self.is_overidable: + self._is_overriden = True + + self._is_modified = ( + self.was_multiroot != self.is_multiroot + or self.child_modified + ) + + self.update_style() + + self.value_changed.emit(self) + + def _from_single_to_multi(self): + single_value = self.singleroot_widget.item_value() + mutli_value = self.multiroot_widget.item_value() + first_key = None + for key in mutli_value.keys(): + first_key = key + break + + if first_key is None: + first_key = "" + + mutli_value[first_key] = single_value + + self.multiroot_widget.set_value(mutli_value) + + def _from_multi_to_single(self): + mutli_value = self.multiroot_widget.all_item_values() + for value in mutli_value.values(): + single_value = value + break + + self.singleroot_widget.set_value(single_value) + + def set_multiroot(self, is_multiroot=None): + if is_multiroot is None: + is_multiroot = self.is_multiroot + if is_multiroot: + self._from_single_to_multi() + else: + self._from_multi_to_single() + + if is_multiroot != self.is_multiroot: + self.multiroot_checkbox.setChecked(is_multiroot) + + self.singleroot_widget.setVisible(not is_multiroot) + self.multiroot_widget.setVisible(is_multiroot) + + self._on_value_change() + + @property + def child_has_studio_override(self): + if self.is_multiroot: + return self.multiroot_widget.has_studio_override + else: + return self.singleroot_widget.has_studio_override + + @property + def child_modified(self): + if self.is_multiroot: + return self.multiroot_widget.child_modified + else: + return self.singleroot_widget.child_modified + + @property + def child_overriden(self): + if self.is_multiroot: + return ( + self.multiroot_widget.is_overriden + or self.multiroot_widget.child_overriden + ) + else: + return ( + self.singleroot_widget.is_overriden + or self.singleroot_widget.child_overriden + ) + + @property + def child_invalid(self): + if self.is_multiroot: + return self.multiroot_widget.child_invalid + else: + return self.singleroot_widget.child_invalid + + def remove_overrides(self): + self._is_overriden = False + self._is_modified = False + + if self.studio_is_multiroot is NOT_SET: + self.set_multiroot(self.default_is_multiroot) + else: + self.set_multiroot(self.studio_is_multiroot) + + if self.is_multiroot: + self.multiroot_widget.remove_overrides() + else: + self.singleroot_widget.remove_overrides() + + def reset_to_pype_default(self): + self.set_multiroot(self.default_is_multiroot) + if self.is_multiroot: + self.multiroot_widget.reset_to_pype_default() + else: + self.singleroot_widget.reset_to_pype_default() + self._has_studio_override = False + + def set_studio_default(self): + if self.is_multiroot: + self.multiroot_widget.reset_to_pype_default() + else: + self.singleroot_widget.reset_to_pype_default() + self._has_studio_override = True + + def discard_changes(self): + self._is_overriden = self._was_overriden + self._is_modified = False + if self._is_overriden: + self.set_multiroot(self.was_multiroot) + else: + if self.studio_is_multiroot is NOT_SET: + self.set_multiroot(self.default_is_multiroot) + else: + self.set_multiroot(self.studio_is_multiroot) + + if self.is_multiroot: + self.multiroot_widget.discard_changes() + else: + self.singleroot_widget.discard_changes() + + self._is_modified = self.child_modified + self._has_studio_override = self._had_studio_override + + def set_as_overriden(self): + self._is_overriden = True + self.singleroot_widget.set_as_overriden() + self.multiroot_widget.set_as_overriden() + + def item_value(self): + if self.is_multiroot: + return self.multiroot_widget.item_value() + else: + return self.singleroot_widget.item_value() + + def config_value(self): + return {self.key: self.item_value()} + + +class TemplatesWidget(QtWidgets.QWidget, SettingObject): + value_changed = QtCore.Signal(object) + + def __init__(self, input_data, parent): + super(TemplatesWidget, self).__init__(parent) + + input_data["is_group"] = True + self.initial_attributes(input_data, parent, False) + + self.key = input_data["key"] + + body_widget = ExpandingWidget("Templates", self) + content_widget = QtWidgets.QWidget(body_widget) + body_widget.set_content_widget(content_widget) + content_layout = QtWidgets.QVBoxLayout(content_widget) + + template_input_data = { + "key": self.key + } + self.body_widget = body_widget + self.label_widget = body_widget.label_widget + self.value_input = RawJsonWidget( + template_input_data, self, + label_widget=self.label_widget + ) + content_layout.addWidget(self.value_input) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + layout.addWidget(body_widget) + + self.value_input.value_changed.connect(self._on_value_change) + + def _on_value_change(self, item): + self.update_style() + + self.value_changed.emit(self) + + def update_default_values(self, values): + self._state = None + self.value_input.update_default_values(values) + + def update_studio_values(self, values): + self._state = None + self.value_input.update_studio_values(values) + + def apply_overrides(self, parent_values): + self._state = None + self.value_input.apply_overrides(parent_values) + + def hierarchical_style_update(self): + self.value_input.hierarchical_style_update() + self.update_style() + + def update_style(self): + state = self.style_state( + self.has_studio_override, + self.child_invalid, + self.child_overriden, + self.child_modified + ) + if self._state == state: + return + + if state: + child_state = "child-{}".format(state) + else: + child_state = "" + + self.body_widget.side_line_widget.setProperty("state", child_state) + self.body_widget.side_line_widget.style().polish( + self.body_widget.side_line_widget + ) + + self.label_widget.setProperty("state", state) + self.label_widget.style().polish(self.label_widget) + + self._state = state + + @property + def is_modified(self): + return self.value_input.is_modified + + @property + def is_overriden(self): + return self._is_overriden + + @property + def has_studio_override(self): + return self.value_input._has_studio_override + + @property + def child_has_studio_override(self): + return self.value_input.child_has_studio_override + + @property + def child_modified(self): + return self.value_input.child_modified + + @property + def child_overriden(self): + return self.value_input.child_overriden + + @property + def child_invalid(self): + return self.value_input.child_invalid + + def remove_overrides(self): + self.value_input.remove_overrides() + + def reset_to_pype_default(self): + self.value_input.reset_to_pype_default() + + def set_studio_default(self): + self.value_input.set_studio_default() + + def discard_changes(self): + self.value_input.discard_changes() + + def set_as_overriden(self): + self.value_input.set_as_overriden() + + def overrides(self): + if not self.child_overriden: + return NOT_SET, False + return self.config_value(), True + + def item_value(self): + return self.value_input.item_value() + + def config_value(self): + return self.value_input.config_value() + + +TypeToKlass.types["anatomy"] = AnatomyWidget +TypeToKlass.types["anatomy_roots"] = AnatomyWidget +TypeToKlass.types["anatomy_templates"] = AnatomyWidget diff --git a/pype/tools/settings/settings/widgets/base.py b/pype/tools/settings/settings/widgets/base.py new file mode 100644 index 0000000000..423380d54c --- /dev/null +++ b/pype/tools/settings/settings/widgets/base.py @@ -0,0 +1,739 @@ +import os +import json +from Qt import QtWidgets, QtCore, QtGui +from pype.settings.lib import ( + SYSTEM_SETTINGS_KEY, + SYSTEM_SETTINGS_PATH, + PROJECT_SETTINGS_KEY, + PROJECT_SETTINGS_PATH, + PROJECT_ANATOMY_KEY, + PROJECT_ANATOMY_PATH, + + DEFAULTS_DIR, + + reset_default_settings, + default_settings, + + studio_system_settings, + studio_project_settings, + studio_project_anatomy, + + project_settings_overrides, + project_anatomy_overrides, + + path_to_project_overrides, + path_to_project_anatomy +) +from .widgets import UnsavedChangesDialog +from . import lib +from avalon import io +from avalon.vendor import qtawesome + + +class SystemWidget(QtWidgets.QWidget): + is_overidable = False + has_studio_override = _has_studio_override = False + is_overriden = _is_overriden = False + as_widget = _as_widget = False + any_parent_as_widget = _any_parent_as_widget = False + is_group = _is_group = False + any_parent_is_group = _any_parent_is_group = False + + def __init__(self, develop_mode, parent=None): + super(SystemWidget, self).__init__(parent) + + self.develop_mode = develop_mode + self._hide_studio_overrides = False + self._ignore_value_changes = False + + self.input_fields = [] + + scroll_widget = QtWidgets.QScrollArea(self) + scroll_widget.setObjectName("GroupWidget") + content_widget = QtWidgets.QWidget(scroll_widget) + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setContentsMargins(3, 3, 3, 3) + content_layout.setSpacing(0) + content_layout.setAlignment(QtCore.Qt.AlignTop) + content_widget.setLayout(content_layout) + + scroll_widget.setWidgetResizable(True) + scroll_widget.setWidget(content_widget) + + self.scroll_widget = scroll_widget + self.content_layout = content_layout + self.content_widget = content_widget + + footer_widget = QtWidgets.QWidget() + footer_layout = QtWidgets.QHBoxLayout(footer_widget) + + if self.develop_mode: + save_as_default_btn = QtWidgets.QPushButton("Save as Default") + save_as_default_btn.clicked.connect(self._save_as_defaults) + + refresh_icon = qtawesome.icon("fa.refresh", color="white") + refresh_button = QtWidgets.QPushButton() + refresh_button.setIcon(refresh_icon) + refresh_button.clicked.connect(self._on_refresh) + + hide_studio_overrides = QtWidgets.QCheckBox() + hide_studio_overrides.setChecked(self._hide_studio_overrides) + hide_studio_overrides.stateChanged.connect( + self._on_hide_studio_overrides + ) + + hide_studio_overrides_widget = QtWidgets.QWidget() + hide_studio_overrides_layout = QtWidgets.QHBoxLayout( + hide_studio_overrides_widget + ) + _label_widget = QtWidgets.QLabel( + "Hide studio overrides", hide_studio_overrides_widget + ) + hide_studio_overrides_layout.addWidget(_label_widget) + hide_studio_overrides_layout.addWidget(hide_studio_overrides) + + footer_layout.addWidget(save_as_default_btn, 0) + footer_layout.addWidget(refresh_button, 0) + footer_layout.addWidget(hide_studio_overrides_widget, 0) + + save_btn = QtWidgets.QPushButton("Save") + spacer_widget = QtWidgets.QWidget() + footer_layout.addWidget(spacer_widget, 1) + footer_layout.addWidget(save_btn, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + self.setLayout(layout) + + layout.addWidget(scroll_widget, 1) + layout.addWidget(footer_widget, 0) + + save_btn.clicked.connect(self._save) + + self.reset() + + def any_parent_overriden(self): + return False + + @property + def ignore_value_changes(self): + return self._ignore_value_changes + + @ignore_value_changes.setter + def ignore_value_changes(self, value): + self._ignore_value_changes = value + if value is False: + self.hierarchical_style_update() + + def hierarchical_style_update(self): + for input_field in self.input_fields: + input_field.hierarchical_style_update() + + def reset(self): + reset_default_settings() + + if self.content_layout.count() != 0: + for widget in self.input_fields: + self.content_layout.removeWidget(widget) + widget.deleteLater() + self.input_fields.clear() + + self.schema = lib.gui_schema("system_schema", "0_system_gui_schema") + self.keys = self.schema.get("keys", []) + self.add_children_gui(self.schema) + self._update_values() + self.hierarchical_style_update() + + def _save(self): + has_invalid = False + for item in self.input_fields: + if item.child_invalid: + has_invalid = True + + if has_invalid: + invalid_items = [] + for item in self.input_fields: + invalid_items.extend(item.get_invalid()) + msg_box = QtWidgets.QMessageBox( + QtWidgets.QMessageBox.Warning, + "Invalid input", + "There is invalid value in one of inputs." + " Please lead red color and fix them." + ) + msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok) + msg_box.exec_() + + first_invalid_item = invalid_items[0] + self.scroll_widget.ensureWidgetVisible(first_invalid_item) + if first_invalid_item.isVisible(): + first_invalid_item.setFocus(True) + return + + _data = {} + for input_field in self.input_fields: + value, is_group = input_field.studio_overrides() + if value is not lib.NOT_SET: + _data.update(value) + + values = lib.convert_gui_data_to_overrides(_data.get("system", {})) + + dirpath = os.path.dirname(SYSTEM_SETTINGS_PATH) + if not os.path.exists(dirpath): + os.makedirs(dirpath) + + print("Saving data to:", SYSTEM_SETTINGS_PATH) + with open(SYSTEM_SETTINGS_PATH, "w") as file_stream: + json.dump(values, file_stream, indent=4) + + self._update_values() + + def _on_refresh(self): + self.reset() + + def _on_hide_studio_overrides(self, state): + self._hide_studio_overrides = (state == QtCore.Qt.Checked) + self._update_values() + self.hierarchical_style_update() + + def _save_as_defaults(self): + output = {} + for item in self.input_fields: + output.update(item.config_value()) + + for key in reversed(self.keys): + _output = {key: output} + output = _output + + all_values = {} + for item in self.input_fields: + all_values.update(item.config_value()) + + for key in reversed(self.keys): + _all_values = {key: all_values} + all_values = _all_values + + # Skip first key + all_values = all_values["system"] + + prject_defaults_dir = os.path.join( + DEFAULTS_DIR, SYSTEM_SETTINGS_KEY + ) + keys_to_file = lib.file_keys_from_schema(self.schema) + for key_sequence in keys_to_file: + # Skip first key + key_sequence = key_sequence[1:] + subpath = "/".join(key_sequence) + ".json" + + new_values = all_values + for key in key_sequence: + new_values = new_values[key] + + output_path = os.path.join(prject_defaults_dir, subpath) + dirpath = os.path.dirname(output_path) + if not os.path.exists(dirpath): + os.makedirs(dirpath) + + print("Saving data to: ", subpath) + with open(output_path, "w") as file_stream: + json.dump(new_values, file_stream, indent=4) + + reset_default_settings() + + self._update_values() + self.hierarchical_style_update() + + def _update_values(self): + self.ignore_value_changes = True + + default_values = { + "system": default_settings()[SYSTEM_SETTINGS_KEY] + } + for input_field in self.input_fields: + input_field.update_default_values(default_values) + + if self._hide_studio_overrides: + system_values = lib.NOT_SET + else: + system_values = {"system": studio_system_settings()} + for input_field in self.input_fields: + input_field.update_studio_values(system_values) + + self.ignore_value_changes = False + + def add_children_gui(self, child_configuration): + item_type = child_configuration["type"] + klass = lib.TypeToKlass.types.get(item_type) + item = klass(child_configuration, self) + self.input_fields.append(item) + self.content_layout.addWidget(item) + + +class ProjectListView(QtWidgets.QListView): + left_mouse_released_at = QtCore.Signal(QtCore.QModelIndex) + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + index = self.indexAt(event.pos()) + self.left_mouse_released_at.emit(index) + super(ProjectListView, self).mouseReleaseEvent(event) + + +class ProjectListWidget(QtWidgets.QWidget): + default = "< Default >" + project_changed = QtCore.Signal() + + def __init__(self, parent): + self._parent = parent + + self.current_project = None + + super(ProjectListWidget, self).__init__(parent) + self.setObjectName("ProjectListWidget") + + label_widget = QtWidgets.QLabel("Projects") + label_widget.setProperty("state", "studio") + project_list = ProjectListView(self) + project_list.setModel(QtGui.QStandardItemModel()) + + # Do not allow editing + project_list.setEditTriggers( + QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers + ) + # Do not automatically handle selection + project_list.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) + + layout = QtWidgets.QVBoxLayout(self) + layout.setSpacing(3) + layout.addWidget(label_widget, 0) + layout.addWidget(project_list, 1) + + project_list.left_mouse_released_at.connect(self.on_item_clicked) + + self.project_list = project_list + + self.refresh() + + def on_item_clicked(self, new_index): + new_project_name = new_index.data(QtCore.Qt.DisplayRole) + if new_project_name is None: + return + + if self.current_project == new_project_name: + return + + save_changes = False + change_project = False + if self.validate_context_change(): + change_project = True + + else: + dialog = UnsavedChangesDialog(self) + result = dialog.exec_() + if result == 1: + save_changes = True + change_project = True + + elif result == 2: + change_project = True + + if save_changes: + self._parent._save() + + if change_project: + self.select_project(new_project_name) + self.current_project = new_project_name + self.project_changed.emit() + else: + self.select_project(self.current_project) + + def validate_context_change(self): + # TODO add check if project can be changed (is modified) + for item in self._parent.input_fields: + is_modified = item.child_modified + if is_modified: + return False + return True + + def project_name(self): + if self.current_project == self.default: + return None + return self.current_project + + def select_project(self, project_name): + model = self.project_list.model() + found_items = model.findItems(project_name) + if not found_items: + found_items = model.findItems(self.default) + + index = model.indexFromItem(found_items[0]) + self.project_list.selectionModel().clear() + self.project_list.selectionModel().setCurrentIndex( + index, QtCore.QItemSelectionModel.SelectionFlag.SelectCurrent + ) + + def refresh(self): + selected_project = None + for index in self.project_list.selectedIndexes(): + selected_project = index.data(QtCore.Qt.DisplayRole) + break + + model = self.project_list.model() + model.clear() + items = [self.default] + io.install() + for project_doc in tuple(io.projects()): + items.append(project_doc["name"]) + + for item in items: + model.appendRow(QtGui.QStandardItem(item)) + + self.select_project(selected_project) + + self.current_project = self.project_list.currentIndex().data( + QtCore.Qt.DisplayRole + ) + + +class ProjectWidget(QtWidgets.QWidget): + has_studio_override = _has_studio_override = False + is_overriden = _is_overriden = False + as_widget = _as_widget = False + any_parent_as_widget = _any_parent_as_widget = False + is_group = _is_group = False + any_parent_is_group = _any_parent_is_group = False + + def __init__(self, develop_mode, parent=None): + super(ProjectWidget, self).__init__(parent) + + self.develop_mode = develop_mode + self._hide_studio_overrides = False + + self.is_overidable = False + self._ignore_value_changes = False + self.project_name = None + + self.input_fields = [] + + scroll_widget = QtWidgets.QScrollArea(self) + scroll_widget.setObjectName("GroupWidget") + content_widget = QtWidgets.QWidget(scroll_widget) + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setContentsMargins(3, 3, 3, 3) + content_layout.setSpacing(0) + content_layout.setAlignment(QtCore.Qt.AlignTop) + content_widget.setLayout(content_layout) + + scroll_widget.setWidgetResizable(True) + scroll_widget.setWidget(content_widget) + + project_list_widget = ProjectListWidget(self) + content_layout.addWidget(project_list_widget) + + footer_widget = QtWidgets.QWidget() + footer_layout = QtWidgets.QHBoxLayout(footer_widget) + + if self.develop_mode: + save_as_default_btn = QtWidgets.QPushButton("Save as Default") + save_as_default_btn.clicked.connect(self._save_as_defaults) + + refresh_icon = qtawesome.icon("fa.refresh", color="white") + refresh_button = QtWidgets.QPushButton() + refresh_button.setIcon(refresh_icon) + refresh_button.clicked.connect(self._on_refresh) + + hide_studio_overrides = QtWidgets.QCheckBox() + hide_studio_overrides.setChecked(self._hide_studio_overrides) + hide_studio_overrides.stateChanged.connect( + self._on_hide_studio_overrides + ) + + hide_studio_overrides_widget = QtWidgets.QWidget() + hide_studio_overrides_layout = QtWidgets.QHBoxLayout( + hide_studio_overrides_widget + ) + _label_widget = QtWidgets.QLabel( + "Hide studio overrides", hide_studio_overrides_widget + ) + hide_studio_overrides_layout.addWidget(_label_widget) + hide_studio_overrides_layout.addWidget(hide_studio_overrides) + + footer_layout.addWidget(save_as_default_btn, 0) + footer_layout.addWidget(refresh_button, 0) + footer_layout.addWidget(hide_studio_overrides_widget, 0) + + save_btn = QtWidgets.QPushButton("Save") + spacer_widget = QtWidgets.QWidget() + footer_layout.addWidget(spacer_widget, 1) + footer_layout.addWidget(save_btn, 0) + + configurations_widget = QtWidgets.QWidget() + configurations_layout = QtWidgets.QVBoxLayout(configurations_widget) + configurations_layout.setContentsMargins(0, 0, 0, 0) + configurations_layout.setSpacing(0) + + configurations_layout.addWidget(scroll_widget, 1) + configurations_layout.addWidget(footer_widget, 0) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + self.setLayout(layout) + + layout.addWidget(project_list_widget, 0) + layout.addWidget(configurations_widget, 1) + + save_btn.clicked.connect(self._save) + project_list_widget.project_changed.connect(self._on_project_change) + + self.project_list_widget = project_list_widget + self.scroll_widget = scroll_widget + self.content_layout = content_layout + self.content_widget = content_widget + + self.reset() + + def any_parent_overriden(self): + return False + + @property + def ignore_value_changes(self): + return self._ignore_value_changes + + @ignore_value_changes.setter + def ignore_value_changes(self, value): + self._ignore_value_changes = value + if value is False: + self.hierarchical_style_update() + + def hierarchical_style_update(self): + for input_field in self.input_fields: + input_field.hierarchical_style_update() + + def reset(self): + if self.content_layout.count() != 0: + for widget in self.input_fields: + self.content_layout.removeWidget(widget) + widget.deleteLater() + self.input_fields.clear() + + self.schema = lib.gui_schema("projects_schema", "0_project_gui_schema") + self.keys = self.schema.get("keys", []) + self.add_children_gui(self.schema) + self._update_values() + self.hierarchical_style_update() + + def add_children_gui(self, child_configuration): + item_type = child_configuration["type"] + klass = lib.TypeToKlass.types.get(item_type) + item = klass(child_configuration, self) + self.input_fields.append(item) + self.content_layout.addWidget(item) + + def _on_project_change(self): + project_name = self.project_list_widget.project_name() + if project_name is None: + _project_overrides = lib.NOT_SET + _project_anatomy = lib.NOT_SET + self.is_overidable = False + else: + _project_overrides = project_settings_overrides(project_name) + _project_anatomy = project_anatomy_overrides(project_name) + self.is_overidable = True + + overrides = {"project": { + PROJECT_SETTINGS_KEY: lib.convert_overrides_to_gui_data( + _project_overrides + ), + PROJECT_ANATOMY_KEY: lib.convert_overrides_to_gui_data( + _project_anatomy + ) + }} + self.project_name = project_name + self.ignore_value_changes = True + for item in self.input_fields: + item.apply_overrides(overrides) + self.ignore_value_changes = False + + def _save_as_defaults(self): + output = {} + for item in self.input_fields: + output.update(item.config_value()) + + for key in reversed(self.keys): + _output = {key: output} + output = _output + + all_values = {} + for item in self.input_fields: + all_values.update(item.config_value()) + + for key in reversed(self.keys): + _all_values = {key: all_values} + all_values = _all_values + + # Skip first key + all_values = all_values["project"] + + keys_to_file = lib.file_keys_from_schema(self.schema) + for key_sequence in keys_to_file: + # Skip first key + key_sequence = key_sequence[1:] + subpath = "/".join(key_sequence) + ".json" + + new_values = all_values + for key in key_sequence: + new_values = new_values[key] + + output_path = os.path.join(DEFAULTS_DIR, subpath) + dirpath = os.path.dirname(output_path) + if not os.path.exists(dirpath): + os.makedirs(dirpath) + + print("Saving data to: ", subpath) + with open(output_path, "w") as file_stream: + json.dump(new_values, file_stream, indent=4) + + reset_default_settings() + + self._update_values() + self.hierarchical_style_update() + + def _save(self): + has_invalid = False + for item in self.input_fields: + if item.child_invalid: + has_invalid = True + + if has_invalid: + invalid_items = [] + for item in self.input_fields: + invalid_items.extend(item.get_invalid()) + msg_box = QtWidgets.QMessageBox( + QtWidgets.QMessageBox.Warning, + "Invalid input", + "There is invalid value in one of inputs." + " Please lead red color and fix them." + ) + msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok) + msg_box.exec_() + + first_invalid_item = invalid_items[0] + self.scroll_widget.ensureWidgetVisible(first_invalid_item) + if first_invalid_item.isVisible(): + first_invalid_item.setFocus(True) + return + + if self.project_name is None: + self._save_studio_overrides() + else: + self._save_overrides() + + def _on_refresh(self): + self.reset() + + def _on_hide_studio_overrides(self, state): + self._hide_studio_overrides = (state == QtCore.Qt.Checked) + self._update_values() + self.hierarchical_style_update() + + def _save_overrides(self): + data = {} + for item in self.input_fields: + value, is_group = item.overrides() + if value is not lib.NOT_SET: + data.update(value) + + output_data = lib.convert_gui_data_to_overrides( + data.get("project") or {} + ) + + # Saving overrides data + project_overrides_data = output_data.get( + PROJECT_SETTINGS_KEY, {} + ) + project_overrides_json_path = path_to_project_overrides( + self.project_name + ) + dirpath = os.path.dirname(project_overrides_json_path) + if not os.path.exists(dirpath): + os.makedirs(dirpath) + + print("Saving data to:", project_overrides_json_path) + with open(project_overrides_json_path, "w") as file_stream: + json.dump(project_overrides_data, file_stream, indent=4) + + # Saving anatomy data + project_anatomy_data = output_data.get( + PROJECT_ANATOMY_KEY, {} + ) + project_anatomy_json_path = path_to_project_anatomy( + self.project_name + ) + dirpath = os.path.dirname(project_anatomy_json_path) + if not os.path.exists(dirpath): + os.makedirs(dirpath) + + print("Saving data to:", project_anatomy_json_path) + with open(project_anatomy_json_path, "w") as file_stream: + json.dump(project_anatomy_data, file_stream, indent=4) + + # Refill values with overrides + self._on_project_change() + + def _save_studio_overrides(self): + data = {} + for input_field in self.input_fields: + value, is_group = input_field.studio_overrides() + if value is not lib.NOT_SET: + data.update(value) + + output_data = lib.convert_gui_data_to_overrides( + data.get("project", {}) + ) + + # Project overrides data + project_overrides_data = output_data.get( + PROJECT_SETTINGS_KEY, {} + ) + dirpath = os.path.dirname(PROJECT_SETTINGS_PATH) + if not os.path.exists(dirpath): + os.makedirs(dirpath) + + print("Saving data to:", PROJECT_SETTINGS_PATH) + with open(PROJECT_SETTINGS_PATH, "w") as file_stream: + json.dump(project_overrides_data, file_stream, indent=4) + + # Project Anatomy data + project_anatomy_data = output_data.get( + PROJECT_ANATOMY_KEY, {} + ) + dirpath = os.path.dirname(PROJECT_ANATOMY_PATH) + if not os.path.exists(dirpath): + os.makedirs(dirpath) + + print("Saving data to:", PROJECT_ANATOMY_PATH) + with open(PROJECT_ANATOMY_PATH, "w") as file_stream: + json.dump(project_anatomy_data, file_stream, indent=4) + + # Update saved values + self._update_values() + + def _update_values(self): + self.ignore_value_changes = True + + default_values = {"project": default_settings()} + for input_field in self.input_fields: + input_field.update_default_values(default_values) + + if self._hide_studio_overrides: + studio_values = lib.NOT_SET + else: + studio_values = {"project": { + PROJECT_SETTINGS_KEY: studio_project_settings(), + PROJECT_ANATOMY_KEY: studio_project_anatomy() + }} + for input_field in self.input_fields: + input_field.update_studio_values(studio_values) + + self.ignore_value_changes = False diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py new file mode 100644 index 0000000000..e589b89c0f --- /dev/null +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -0,0 +1,3506 @@ +import json +import logging +import collections +from Qt import QtWidgets, QtCore, QtGui +from .widgets import ( + ExpandingWidget, + NumberSpinBox, + PathInput, + GridLabelWidget +) +from .lib import NOT_SET, METADATA_KEY, TypeToKlass, CHILD_OFFSET +from avalon.vendor import qtawesome + + +class SettingObject: + """Partially abstract class for Setting's item type workflow.""" + # `is_input_type` attribute says if has implemented item type methods + is_input_type = True + # Each input must have implemented default value for development + # when defaults are not filled yet. + default_input_value = NOT_SET + # Will allow to show actions for the item type (disabled for proxies) else + # item is skipped and try to trigger actions on it's parent. + allow_actions = True + # All item types must have implemented Qt signal which is emitted when + # it's or it's children value has changed, + value_changed = None + # Item will expand to full width in grid layout + expand_in_grid = False + + def _set_default_attributes(self): + """Create and reset attributes required for all item types. + + They may not be used in the item but are required to be set. + """ + # Default input attributes + self._has_studio_override = False + self._had_studio_override = False + + self._is_overriden = False + self._was_overriden = False + + self._is_modified = False + self._is_invalid = False + + self._is_nullable = False + self._as_widget = False + self._is_group = False + + self._any_parent_as_widget = None + self._any_parent_is_group = None + + # Parent input + self._parent = None + + # States of inputs + self._state = None + self._child_state = None + + # Attributes where values are stored + self.default_value = NOT_SET + self.studio_value = NOT_SET + self.override_value = NOT_SET + + # Log object + self._log = None + + # Only for develop mode + self.defaults_not_set = False + + def initial_attributes(self, input_data, parent, as_widget): + """Prepare attributes based on entered arguments. + + This method should be same for each item type. Few item types + may require to extend with specific attributes for their case. + """ + self._set_default_attributes() + + self._parent = parent + self._as_widget = as_widget + + self._is_group = input_data.get("is_group", False) + # TODO not implemented yet + self._is_nullable = input_data.get("is_nullable", False) + + any_parent_as_widget = parent.as_widget + if not any_parent_as_widget: + any_parent_as_widget = parent.any_parent_as_widget + + self._any_parent_as_widget = any_parent_as_widget + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + self._any_parent_is_group = any_parent_is_group + + @property + def develop_mode(self): + """Tool is in develop mode or not. + + Returns: + bool + + """ + return self._parent.develop_mode + + @property + def log(self): + """Auto created logger for debugging.""" + if self._log is None: + self._log = logging.getLogger(self.__class__.__name__) + return self._log + + @property + def had_studio_override(self): + """Item had studio overrides on refresh. + + Use attribute `_had_studio_override` which should be changed only + during methods `update_studio_values` and `update_default_values`. + + Returns: + bool + + """ + return self._had_studio_override + + @property + def has_studio_override(self): + """Item has studio override at the moment. + + With combination of `had_studio_override` is possible to know if item + is modified (not value change). + + Returns: + bool + + """ + return self._has_studio_override or self._parent.has_studio_override + + @property + def as_widget(self): + """Item is used as widget in parent item. + + Returns: + bool + + """ + return self._as_widget + + @property + def any_parent_as_widget(self): + """Any parent of item is used as widget. + + Attribute holding this information is set during creation and + stored to `_any_parent_as_widget`. + + Why is this information useful: If any parent is used as widget then + modifications and override are not important for whole part. + + Returns: + bool + + """ + if self._any_parent_as_widget is None: + return super(SettingObject, self).any_parent_as_widget + return self._any_parent_as_widget + + @property + def is_group(self): + """Item represents key that can be overriden. + + Attribute `is_group` can be set to True only once in item hierarchy. + + Returns: + bool + + """ + return self._is_group + + @property + def any_parent_is_group(self): + """Any parent of item is group. + + Attribute holding this information is set during creation and + stored to `_any_parent_is_group`. + + Why is this information useful: If any parent is group and + the parent is set as overriden, this item is overriden too. + + Returns: + bool + + """ + if self._any_parent_is_group is None: + return super(SettingObject, self).any_parent_is_group + return self._any_parent_is_group + + @property + def is_modified(self): + """Has object any changes that require saving.""" + if self.any_parent_as_widget: + return self._is_modified + + if self._is_modified or self.defaults_not_set: + return True + + if self.is_overidable: + return self.was_overriden != self.is_overriden + else: + return self.has_studio_override != self.had_studio_override + + @property + def is_overriden(self): + """Is object overriden so should be saved to overrides.""" + return self._is_overriden or self._parent.is_overriden + + @property + def was_overriden(self): + """Item had set value of project overrides on project change.""" + if self._as_widget: + return self._parent.was_overriden + return self._was_overriden + + @property + def is_invalid(self): + """Value set in is not valid.""" + return self._is_invalid + + @property + def is_nullable(self): + """Value of item can be set to None. + + NOT IMPLEMENTED! + """ + return self._is_nullable + + @property + def is_overidable(self): + """ care about overrides.""" + + return self._parent.is_overidable + + def any_parent_overriden(self): + """Any of parent objects up to top hiearchy item is overriden. + + Returns: + bool + + """ + + if self._parent._is_overriden: + return True + return self._parent.any_parent_overriden() + + @property + def ignore_value_changes(self): + """Most of attribute changes are ignored on value change when True.""" + return self._parent.ignore_value_changes + + @ignore_value_changes.setter + def ignore_value_changes(self, value): + """Setter for global parent item to apply changes for all inputs.""" + self._parent.ignore_value_changes = value + + def config_value(self): + """Output for saving changes or overrides.""" + return {self.key: self.item_value()} + + @classmethod + def style_state( + cls, has_studio_override, is_invalid, is_overriden, is_modified + ): + """Return stylesheet state by intered booleans.""" + items = [] + if is_invalid: + items.append("invalid") + else: + if is_overriden: + items.append("overriden") + if is_modified: + items.append("modified") + + if not items and has_studio_override: + items.append("studio") + + return "-".join(items) or "" + + def show_actions_menu(self, event=None): + if event and event.button() != QtCore.Qt.RightButton: + return + + if not self.allow_actions: + if event: + return self.mouseReleaseEvent(event) + return + + menu = QtWidgets.QMenu() + + actions_mapping = {} + if self.child_modified: + action = QtWidgets.QAction("Discard changes") + actions_mapping[action] = self._discard_changes + menu.addAction(action) + + if ( + self.is_overidable + and not self.is_overriden + and not self.any_parent_is_group + ): + action = QtWidgets.QAction("Set project override") + actions_mapping[action] = self._set_as_overriden + menu.addAction(action) + + if ( + not self.is_overidable + and ( + self.has_studio_override + ) + ): + action = QtWidgets.QAction("Reset to pype default") + actions_mapping[action] = self._reset_to_pype_default + menu.addAction(action) + + if ( + not self.is_overidable + and not self.is_overriden + and not self.any_parent_is_group + and not self._had_studio_override + ): + action = QtWidgets.QAction("Set studio default") + actions_mapping[action] = self._set_studio_default + menu.addAction(action) + + if ( + not self.any_parent_overriden() + and (self.is_overriden or self.child_overriden) + ): + # TODO better label + action = QtWidgets.QAction("Remove project override") + actions_mapping[action] = self._remove_overrides + menu.addAction(action) + + if not actions_mapping: + action = QtWidgets.QAction("< No action >") + actions_mapping[action] = None + menu.addAction(action) + + result = menu.exec_(QtGui.QCursor.pos()) + if result: + to_run = actions_mapping[result] + if to_run: + to_run() + + def mouseReleaseEvent(self, event): + if self.allow_actions and event.button() == QtCore.Qt.RightButton: + return self.show_actions_menu() + + mro = type(self).mro() + index = mro.index(self.__class__) + item = None + for idx in range(index + 1, len(mro)): + _item = mro[idx] + if hasattr(_item, "mouseReleaseEvent"): + item = _item + break + + if item: + return item.mouseReleaseEvent(self, event) + + def _discard_changes(self): + self.ignore_value_changes = True + self.discard_changes() + self.ignore_value_changes = False + + def discard_changes(self): + """Item's implementation to discard all changes made by user. + + Reset all values to same values as had when opened GUI + or when changed project. + + Must not affect `had_studio_override` value or `was_overriden` + value. It must be marked that there are keys/values which are not in + defaults or overrides. + """ + raise NotImplementedError( + "{} Method `discard_changes` not implemented!".format( + repr(self) + ) + ) + + def _set_studio_default(self): + self.ignore_value_changes = True + self.set_studio_default() + self.ignore_value_changes = False + + def set_studio_default(self): + """Item's implementation to set current values as studio's overrides. + + Mark item and it's children as they have studio overrides. + """ + raise NotImplementedError( + "{} Method `set_studio_default` not implemented!".format( + repr(self) + ) + ) + + def _reset_to_pype_default(self): + self.ignore_value_changes = True + self.reset_to_pype_default() + self.ignore_value_changes = False + + def reset_to_pype_default(self): + """Item's implementation to remove studio overrides. + + Mark item as it does not have studio overrides unset studio + override values. + """ + raise NotImplementedError( + "{} Method `reset_to_pype_default` not implemented!".format( + repr(self) + ) + ) + + def _remove_overrides(self): + self.ignore_value_changes = True + self.remove_overrides() + self.ignore_value_changes = False + + def remove_overrides(self): + """Item's implementation to remove project overrides. + + Mark item as does not have project overrides. Must not change + `was_overriden` attribute value. + """ + raise NotImplementedError( + "{} Method `remove_overrides` not implemented!".format( + repr(self) + ) + ) + + def _set_as_overriden(self): + self.ignore_value_changes = True + self.set_as_overriden() + self.ignore_value_changes = False + + def set_as_overriden(self): + """Item's implementation to set values as overriden for project. + + Mark item and all it's children as they're overriden. Must skip + items with children items that has attributes `is_group` + and `any_parent_is_group` set to False. In that case those items + are not meant to be overridable and should trigger the method on it's + children. + + """ + raise NotImplementedError( + "{} Method `set_as_overriden` not implemented!".format(repr(self)) + ) + + def hierarchical_style_update(self): + """Trigger update style method down the hierarchy.""" + raise NotImplementedError( + "{} Method `hierarchical_style_update` not implemented!".format( + repr(self) + ) + ) + + def update_default_values(self, parent_values): + """Fill default values on startup or on refresh. + + Default values stored in `pype` repository should update all items in + schema. Each item should take values for his key and set it's value or + pass values down to children items. + + Args: + parent_values (dict): Values of parent's item. But in case item is + used as widget, `parent_values` contain value for item. + """ + raise NotImplementedError( + "{} does not have implemented `update_default_values`".format(self) + ) + + def update_studio_values(self, parent_values): + """Fill studio override values on startup or on refresh. + + Set studio value if is not set to NOT_SET, in that case studio + overrides are not set yet. + + Args: + parent_values (dict): Values of parent's item. But in case item is + used as widget, `parent_values` contain value for item. + """ + raise NotImplementedError( + "{} does not have implemented `update_studio_values`".format(self) + ) + + def apply_overrides(self, parent_values): + """Fill project override values on startup, refresh or project change. + + Set project value if is not set to NOT_SET, in that case project + overrides are not set yet. + + Args: + parent_values (dict): Values of parent's item. But in case item is + used as widget, `parent_values` contain value for item. + """ + raise NotImplementedError( + "{} does not have implemented `apply_overrides`".format(self) + ) + + @property + def child_has_studio_override(self): + """Any children item has studio overrides.""" + raise NotImplementedError( + "{} does not have implemented `child_has_studio_override`".format( + self + ) + ) + + @property + def child_modified(self): + """Any children item is modified.""" + raise NotImplementedError( + "{} does not have implemented `child_modified`".format(self) + ) + + @property + def child_overriden(self): + """Any children item has project overrides.""" + raise NotImplementedError( + "{} does not have implemented `child_overriden`".format(self) + ) + + @property + def child_invalid(self): + """Any children item does not have valid value.""" + raise NotImplementedError( + "{} does not have implemented `child_invalid`".format(self) + ) + + def get_invalid(self): + """Return invalid item types all down the hierarchy.""" + raise NotImplementedError( + "{} does not have implemented `get_invalid`".format(self) + ) + + def item_value(self): + """Value of an item without key.""" + raise NotImplementedError( + "Method `item_value` not implemented!" + ) + + +class InputObject(SettingObject): + """Class for inputs with pre-implemented methods. + + Class is for item types not creating or using other item types, most + of methods has same code in that case. + """ + + def update_default_values(self, parent_values): + self._state = None + self._is_modified = False + + value = NOT_SET + if self._as_widget: + value = parent_values + elif parent_values is not NOT_SET: + value = parent_values.get(self.key, NOT_SET) + + if value is NOT_SET: + if self.develop_mode: + self.defaults_not_set = True + value = self.default_input_value + if value is NOT_SET: + raise NotImplementedError(( + "{} Does not have implemented" + " attribute `default_input_value`" + ).format(self)) + + else: + raise ValueError( + "Default value is not set. This is implementation BUG." + ) + else: + self.defaults_not_set = False + + self.default_value = value + self._has_studio_override = False + self._had_studio_override = False + self.set_value(value) + + def update_studio_values(self, parent_values): + self._state = None + self._is_modified = False + + value = NOT_SET + if self._as_widget: + value = parent_values + elif parent_values is not NOT_SET: + value = parent_values.get(self.key, NOT_SET) + + self.studio_value = value + if value is not NOT_SET: + self._has_studio_override = True + self._had_studio_override = True + self.set_value(value) + + else: + self._has_studio_override = False + self._had_studio_override = False + self.set_value(self.default_value) + + def apply_overrides(self, parent_values): + self._is_modified = False + self._state = None + self._had_studio_override = bool(self._has_studio_override) + if self._as_widget: + override_value = parent_values + elif parent_values is NOT_SET or self.key not in parent_values: + override_value = NOT_SET + else: + override_value = parent_values[self.key] + + self.override_value = override_value + + if override_value is NOT_SET: + self._is_overriden = False + self._was_overriden = False + if self.has_studio_override: + value = self.studio_value + else: + value = self.default_value + else: + self._is_overriden = True + self._was_overriden = True + value = override_value + + self.set_value(value) + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + if not self.any_parent_as_widget: + if self.is_overidable: + self._is_overriden = True + else: + self._has_studio_override = True + + if self._is_invalid: + self._is_modified = True + elif self._is_overriden: + self._is_modified = self.item_value() != self.override_value + elif self._has_studio_override: + self._is_modified = self.item_value() != self.studio_value + else: + self._is_modified = self.item_value() != self.default_value + + self.update_style() + + self.value_changed.emit(self) + + def studio_overrides(self): + if ( + not (self.as_widget or self.any_parent_as_widget) + and not self.has_studio_override + ): + return NOT_SET, False + return self.config_value(), self.is_group + + def overrides(self): + if ( + not (self.as_widget or self.any_parent_as_widget) + and not self.is_overriden + ): + return NOT_SET, False + return self.config_value(), self.is_group + + def hierarchical_style_update(self): + self.update_style() + + def remove_overrides(self): + self._is_overriden = False + self._is_modified = False + if self.has_studio_override: + self.set_value(self.studio_value) + else: + self.set_value(self.default_value) + self._is_overriden = False + self._is_modified = False + + def reset_to_pype_default(self): + self.set_value(self.default_value) + self._has_studio_override = False + + def set_studio_default(self): + self._has_studio_override = True + + def discard_changes(self): + self._is_overriden = self._was_overriden + self._has_studio_override = self._had_studio_override + if self.is_overidable: + if self._was_overriden and self.override_value is not NOT_SET: + self.set_value(self.override_value) + else: + if self._had_studio_override: + self.set_value(self.studio_value) + else: + self.set_value(self.default_value) + + if not self.is_overidable: + if self.has_studio_override: + self._is_modified = self.studio_value != self.item_value() + else: + self._is_modified = self.default_value != self.item_value() + self._is_overriden = False + return + + self._is_modified = False + self._is_overriden = self._was_overriden + + def set_as_overriden(self): + self._is_overriden = True + + @property + def child_has_studio_override(self): + return self._has_studio_override + + @property + def child_modified(self): + return self.is_modified + + @property + def child_overriden(self): + return self._is_overriden + + @property + def child_invalid(self): + return self.is_invalid + + def get_invalid(self): + output = [] + if self.is_invalid: + output.append(self) + return output + + def reset_children_attributes(self): + return + + +class BooleanWidget(QtWidgets.QWidget, InputObject): + default_input_value = True + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, parent, + as_widget=False, label_widget=None, parent_widget=None + ): + if parent_widget is None: + parent_widget = parent + super(BooleanWidget, self).__init__(parent_widget) + + self.initial_attributes(input_data, parent, as_widget) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + + if not self._as_widget: + self.key = input_data["key"] + if not label_widget: + label = input_data["label"] + label_widget = QtWidgets.QLabel(label) + label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + layout.addWidget(label_widget, 0) + self.label_widget = label_widget + + self.checkbox = QtWidgets.QCheckBox(self) + spacer = QtWidgets.QWidget(self) + layout.addWidget(self.checkbox, 0) + layout.addWidget(spacer, 1) + + spacer.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + self.setFocusProxy(self.checkbox) + + self.checkbox.stateChanged.connect(self._on_value_change) + + def set_value(self, value): + # Ignore value change because if `self.isChecked()` has same + # value as `value` the `_on_value_change` is not triggered + self.checkbox.setChecked(value) + + def update_style(self): + if self._as_widget: + if not self.isEnabled(): + state = self.style_state(False, False, False, False) + else: + state = self.style_state( + False, + self._is_invalid, + False, + self._is_modified + ) + else: + state = self.style_state( + self.has_studio_override, + self.is_invalid, + self.is_overriden, + self.is_modified + ) + if self._state == state: + return + + if self._as_widget: + property_name = "input-state" + else: + property_name = "state" + + self.label_widget.setProperty(property_name, state) + self.label_widget.style().polish(self.label_widget) + self._state = state + + def item_value(self): + return self.checkbox.isChecked() + + +class NumberWidget(QtWidgets.QWidget, InputObject): + default_input_value = 0 + value_changed = QtCore.Signal(object) + input_modifiers = ("minimum", "maximum", "decimal") + + def __init__( + self, input_data, parent, + as_widget=False, label_widget=None, parent_widget=None + ): + if parent_widget is None: + parent_widget = parent + super(NumberWidget, self).__init__(parent_widget) + + self.initial_attributes(input_data, parent, as_widget) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + + kwargs = { + modifier: input_data.get(modifier) + for modifier in self.input_modifiers + if input_data.get(modifier) + } + self.input_field = NumberSpinBox(self, **kwargs) + + self.setFocusProxy(self.input_field) + + if not self._as_widget: + self.key = input_data["key"] + if not label_widget: + label = input_data["label"] + label_widget = QtWidgets.QLabel(label) + layout.addWidget(label_widget, 0) + self.label_widget = label_widget + + layout.addWidget(self.input_field, 1) + + self.input_field.valueChanged.connect(self._on_value_change) + + def set_value(self, value): + self.input_field.setValue(value) + + def update_style(self): + if self._as_widget: + if not self.isEnabled(): + state = self.style_state(False, False, False, False) + else: + state = self.style_state( + False, + self._is_invalid, + False, + self._is_modified + ) + else: + state = self.style_state( + self.has_studio_override, + self.is_invalid, + self.is_overriden, + self.is_modified + ) + if self._state == state: + return + + if self._as_widget: + property_name = "input-state" + widget = self.input_field + else: + property_name = "state" + widget = self.label_widget + + widget.setProperty(property_name, state) + widget.style().polish(widget) + + def item_value(self): + return self.input_field.value() + + +class TextWidget(QtWidgets.QWidget, InputObject): + default_input_value = "" + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, parent, + as_widget=False, label_widget=None, parent_widget=None + ): + if parent_widget is None: + parent_widget = parent + super(TextWidget, self).__init__(parent_widget) + + self.initial_attributes(input_data, parent, as_widget) + + self.multiline = input_data.get("multiline", False) + placeholder = input_data.get("placeholder") + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + + if self.multiline: + self.text_input = QtWidgets.QPlainTextEdit(self) + else: + self.text_input = QtWidgets.QLineEdit(self) + + if placeholder: + self.text_input.setPlaceholderText(placeholder) + + self.setFocusProxy(self.text_input) + + layout_kwargs = {} + if self.multiline: + layout_kwargs["alignment"] = QtCore.Qt.AlignTop + + if not self._as_widget: + self.key = input_data["key"] + if not label_widget: + label = input_data["label"] + label_widget = QtWidgets.QLabel(label) + layout.addWidget(label_widget, 0, **layout_kwargs) + self.label_widget = label_widget + + layout.addWidget(self.text_input, 1, **layout_kwargs) + + self.text_input.textChanged.connect(self._on_value_change) + + def set_value(self, value): + if self.multiline: + self.text_input.setPlainText(value) + else: + self.text_input.setText(value) + + def update_style(self): + if self._as_widget: + if not self.isEnabled(): + state = self.style_state(False, False, False, False) + else: + state = self.style_state( + False, + self._is_invalid, + False, + self._is_modified + ) + else: + state = self.style_state( + self.has_studio_override, + self.is_invalid, + self.is_overriden, + self.is_modified + ) + + if self._state == state: + return + + if self._as_widget: + property_name = "input-state" + widget = self.text_input + else: + property_name = "state" + widget = self.label_widget + + widget.setProperty(property_name, state) + widget.style().polish(widget) + + def item_value(self): + if self.multiline: + return self.text_input.toPlainText() + else: + return self.text_input.text() + + +class PathInputWidget(QtWidgets.QWidget, InputObject): + default_input_value = "" + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, parent, + as_widget=False, label_widget=None, parent_widget=None + ): + if parent_widget is None: + parent_widget = parent + super(PathInputWidget, self).__init__(parent_widget) + + self.initial_attributes(input_data, parent, as_widget) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + + if not self._as_widget: + self.key = input_data["key"] + if not label_widget: + label = input_data["label"] + label_widget = QtWidgets.QLabel(label) + layout.addWidget(label_widget, 0) + self.label_widget = label_widget + + self.path_input = PathInput(self) + self.setFocusProxy(self.path_input) + layout.addWidget(self.path_input, 1) + + self.path_input.textChanged.connect(self._on_value_change) + + def set_value(self, value): + self.path_input.setText(value) + + def focusOutEvent(self, event): + self.path_input.clear_end_path() + super(PathInput, self).focusOutEvent(event) + + def update_style(self): + if self._as_widget: + if not self.isEnabled(): + state = self.style_state(False, False, False, False) + else: + state = self.style_state( + False, + self._is_invalid, + False, + self._is_modified + ) + else: + state = self.style_state( + self.has_studio_override, + self.is_invalid, + self.is_overriden, + self.is_modified + ) + + if self._state == state: + return + + if self._as_widget: + property_name = "input-state" + widget = self.path_input + else: + property_name = "state" + widget = self.label_widget + + widget.setProperty(property_name, state) + widget.style().polish(widget) + + def item_value(self): + return self.path_input.text() + + +class RawJsonInput(QtWidgets.QPlainTextEdit): + tab_length = 4 + + def __init__(self, *args, **kwargs): + super(RawJsonInput, self).__init__(*args, **kwargs) + self.setObjectName("RawJsonInput") + self.setTabStopDistance( + QtGui.QFontMetricsF( + self.font() + ).horizontalAdvance(" ") * self.tab_length + ) + + def sizeHint(self): + document = self.document() + layout = document.documentLayout() + + height = document.documentMargin() + 2 * self.frameWidth() + 1 + block = document.begin() + while block != document.end(): + height += layout.blockBoundingRect(block).height() + block = block.next() + + hint = super(RawJsonInput, self).sizeHint() + hint.setHeight(height) + + return hint + + def set_value(self, value): + if value is NOT_SET: + value = "" + elif not isinstance(value, str): + try: + value = json.dumps(value, indent=4) + except Exception: + value = "" + self.setPlainText(value) + + def json_value(self): + return json.loads(self.toPlainText()) + + def has_invalid_value(self): + try: + self.json_value() + return False + except Exception: + return True + + def resizeEvent(self, event): + self.updateGeometry() + super(RawJsonInput, self).resizeEvent(event) + + +class RawJsonWidget(QtWidgets.QWidget, InputObject): + default_input_value = "{}" + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, parent, + as_widget=False, label_widget=None, parent_widget=None + ): + if parent_widget is None: + parent_widget = parent + super(RawJsonWidget, self).__init__(parent_widget) + + self.initial_attributes(input_data, parent, as_widget) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + + self.text_input = RawJsonInput(self) + self.text_input.setSizePolicy( + QtWidgets.QSizePolicy.Minimum, + QtWidgets.QSizePolicy.MinimumExpanding + ) + + self.setFocusProxy(self.text_input) + + if not self._as_widget: + self.key = input_data["key"] + if not label_widget: + label = input_data["label"] + label_widget = QtWidgets.QLabel(label) + layout.addWidget(label_widget, 0, alignment=QtCore.Qt.AlignTop) + self.label_widget = label_widget + layout.addWidget(self.text_input, 1, alignment=QtCore.Qt.AlignTop) + + self.text_input.textChanged.connect(self._on_value_change) + + def update_studio_values(self, parent_values): + self._is_invalid = self.text_input.has_invalid_value() + return super(RawJsonWidget, self).update_studio_values(parent_values) + + def set_value(self, value): + self.text_input.set_value(value) + + def _on_value_change(self, *args, **kwargs): + self._is_invalid = self.text_input.has_invalid_value() + return super(RawJsonWidget, self)._on_value_change(*args, **kwargs) + + def update_style(self): + if self._as_widget: + if not self.isEnabled(): + state = self.style_state(False, False, False, False) + else: + state = self.style_state( + False, + self._is_invalid, + False, + self._is_modified + ) + else: + state = self.style_state( + self.has_studio_override, + self.is_invalid, + self.is_overriden, + self.is_modified + ) + + if self._state == state: + return + + if self._as_widget: + property_name = "input-state" + widget = self.text_input + else: + property_name = "state" + widget = self.label_widget + + widget.setProperty(property_name, state) + widget.style().polish(widget) + + def item_value(self): + if self.is_invalid: + return NOT_SET + return self.text_input.json_value() + + +class ListItem(QtWidgets.QWidget, SettingObject): + _btn_size = 20 + value_changed = QtCore.Signal(object) + + def __init__( + self, object_type, input_modifiers, config_parent, parent, + is_strict=False + ): + super(ListItem, self).__init__(parent) + + self._set_default_attributes() + + self._is_strict = is_strict + + self._parent = config_parent + self._any_parent_is_group = True + self._is_empty = False + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(3) + + char_up = qtawesome.charmap("fa.angle-up") + char_down = qtawesome.charmap("fa.angle-down") + + if not self._is_strict: + self.add_btn = QtWidgets.QPushButton("+") + self.remove_btn = QtWidgets.QPushButton("-") + self.up_btn = QtWidgets.QPushButton(char_up) + self.down_btn = QtWidgets.QPushButton(char_down) + + font_up_down = qtawesome.font("fa", 13) + self.up_btn.setFont(font_up_down) + self.down_btn.setFont(font_up_down) + + self.add_btn.setFocusPolicy(QtCore.Qt.ClickFocus) + self.remove_btn.setFocusPolicy(QtCore.Qt.ClickFocus) + self.up_btn.setFocusPolicy(QtCore.Qt.ClickFocus) + self.down_btn.setFocusPolicy(QtCore.Qt.ClickFocus) + + self.add_btn.setFixedSize(self._btn_size, self._btn_size) + self.remove_btn.setFixedSize(self._btn_size, self._btn_size) + self.up_btn.setFixedSize(self._btn_size, self._btn_size) + self.down_btn.setFixedSize(self._btn_size, self._btn_size) + + self.add_btn.setProperty("btn-type", "tool-item") + self.remove_btn.setProperty("btn-type", "tool-item") + self.up_btn.setProperty("btn-type", "tool-item") + self.down_btn.setProperty("btn-type", "tool-item") + + self.add_btn.clicked.connect(self._on_add_clicked) + self.remove_btn.clicked.connect(self._on_remove_clicked) + self.up_btn.clicked.connect(self._on_up_clicked) + self.down_btn.clicked.connect(self._on_down_clicked) + + layout.addWidget(self.add_btn, 0) + layout.addWidget(self.remove_btn, 0) + + ItemKlass = TypeToKlass.types[object_type] + self.value_input = ItemKlass( + input_modifiers, + self, + as_widget=True, + label_widget=None + ) + + layout.addWidget(self.value_input, 1) + + if not self._is_strict: + self.spacer_widget = QtWidgets.QWidget(self) + self.spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + self.spacer_widget.setVisible(False) + + layout.addWidget(self.spacer_widget, 1) + + layout.addWidget(self.up_btn, 0) + layout.addWidget(self.down_btn, 0) + + self.value_input.value_changed.connect(self._on_value_change) + + @property + def as_widget(self): + return self._parent.as_widget + + @property + def any_parent_as_widget(self): + return self.as_widget or self._parent.any_parent_as_widget + + def set_as_empty(self, is_empty=True): + self._is_empty = is_empty + + self.spacer_widget.setVisible(is_empty) + self.value_input.setVisible(not is_empty) + self.remove_btn.setEnabled(not is_empty) + self.up_btn.setVisible(not is_empty) + self.down_btn.setVisible(not is_empty) + self.order_changed() + self._on_value_change() + + def order_changed(self): + row = self.row() + parent_row_count = self.parent_rows_count() + if parent_row_count == 1: + self.up_btn.setVisible(False) + self.down_btn.setVisible(False) + return + + if not self.up_btn.isVisible(): + self.up_btn.setVisible(True) + self.down_btn.setVisible(True) + + if row == 0: + self.up_btn.setEnabled(False) + self.down_btn.setEnabled(True) + + elif row == parent_row_count - 1: + self.up_btn.setEnabled(True) + self.down_btn.setEnabled(False) + + else: + self.up_btn.setEnabled(True) + self.down_btn.setEnabled(True) + + def _on_value_change(self, item=None): + self.value_changed.emit(self) + + def row(self): + return self._parent.input_fields.index(self) + + def parent_rows_count(self): + return len(self._parent.input_fields) + + def _on_add_clicked(self): + if self._is_empty: + self.set_as_empty(False) + else: + self._parent.add_row(row=self.row() + 1) + + def _on_remove_clicked(self): + self._parent.remove_row(self) + + def _on_up_clicked(self): + row = self.row() + self._parent.swap_rows(row - 1, row) + + def _on_down_clicked(self): + row = self.row() + self._parent.swap_rows(row, row + 1) + + def config_value(self): + if not self._is_empty: + return self.value_input.item_value() + return NOT_SET + + @property + def child_has_studio_override(self): + return self.value_input.child_has_studio_override + + @property + def child_modified(self): + return self.value_input.child_modified + + @property + def child_overriden(self): + return self.value_input.child_overriden + + def hierarchical_style_update(self): + self.value_input.hierarchical_style_update() + + def mouseReleaseEvent(self, event): + return QtWidgets.QWidget.mouseReleaseEvent(self, event) + + def update_default_values(self, value): + self.value_input.update_default_values(value) + + def update_studio_values(self, value): + self.value_input.update_studio_values(value) + + def apply_overrides(self, value): + self.value_input.apply_overrides(value) + + +class ListWidget(QtWidgets.QWidget, InputObject): + default_input_value = [] + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, parent, + as_widget=False, label_widget=None, parent_widget=None + ): + if parent_widget is None: + parent_widget = parent + super(ListWidget, self).__init__(parent_widget) + self.setObjectName("ListWidget") + + self.initial_attributes(input_data, parent, as_widget) + + self.object_type = input_data["object_type"] + self.input_modifiers = input_data.get("input_modifiers") or {} + + self.input_fields = [] + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 5) + layout.setSpacing(5) + + if not self.as_widget: + self.key = input_data["key"] + if not label_widget: + label_widget = QtWidgets.QLabel(input_data["label"], self) + layout.addWidget(label_widget, alignment=QtCore.Qt.AlignTop) + + self.label_widget = label_widget + + inputs_widget = QtWidgets.QWidget(self) + inputs_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + layout.addWidget(inputs_widget) + + inputs_layout = QtWidgets.QVBoxLayout(inputs_widget) + inputs_layout.setContentsMargins(0, 0, 0, 0) + inputs_layout.setSpacing(3) + + self.inputs_widget = inputs_widget + self.inputs_layout = inputs_layout + + self.add_row(is_empty=True) + + def count(self): + return len(self.input_fields) + + def update_studio_values(self, parent_values): + super(ListWidget, self).update_studio_values(parent_values) + + self.hierarchical_style_update() + + def set_value(self, value): + previous_inputs = tuple(self.input_fields) + for item_value in value: + self.add_row(value=item_value) + + for input_field in previous_inputs: + self.remove_row(input_field) + + if self.count() == 0: + self.add_row(is_empty=True) + + def swap_rows(self, row_1, row_2): + if row_1 == row_2: + return + + if row_1 > row_2: + row_1, row_2 = row_2, row_1 + + field_1 = self.input_fields[row_1] + field_2 = self.input_fields[row_2] + + self.input_fields[row_1] = field_2 + self.input_fields[row_2] = field_1 + + layout_index = self.inputs_layout.indexOf(field_1) + self.inputs_layout.insertWidget(layout_index + 1, field_1) + + field_1.order_changed() + field_2.order_changed() + + def add_row(self, row=None, value=None, is_empty=False): + # Create new item + item_widget = ListItem( + self.object_type, self.input_modifiers, self, self.inputs_widget + ) + + previous_field = None + next_field = None + + if row is None: + if self.input_fields: + previous_field = self.input_fields[-1] + self.inputs_layout.addWidget(item_widget) + self.input_fields.append(item_widget) + else: + if row > 0: + previous_field = self.input_fields[row - 1] + + max_index = self.count() + if row < max_index: + next_field = self.input_fields[row] + + self.inputs_layout.insertWidget(row, item_widget) + self.input_fields.insert(row, item_widget) + + if previous_field: + previous_field.order_changed() + + if next_field: + next_field.order_changed() + + if is_empty: + item_widget.set_as_empty() + item_widget.value_changed.connect(self._on_value_change) + + item_widget.order_changed() + + previous_input = None + for input_field in self.input_fields: + if previous_input is not None: + self.setTabOrder( + previous_input, input_field.value_input.focusProxy() + ) + previous_input = input_field.value_input.focusProxy() + + # Set text if entered text is not None + # else (when add button clicked) trigger `_on_value_change` + if value is not None: + if self._is_overriden: + item_widget.apply_overrides(value) + elif not self._has_studio_override: + item_widget.update_default_values(value) + else: + item_widget.update_studio_values(value) + self.hierarchical_style_update() + else: + self._on_value_change() + self.updateGeometry() + + def remove_row(self, item_widget): + item_widget.value_changed.disconnect() + + row = self.input_fields.index(item_widget) + previous_field = None + next_field = None + if row > 0: + previous_field = self.input_fields[row - 1] + + if row != len(self.input_fields) - 1: + next_field = self.input_fields[row + 1] + + self.inputs_layout.removeWidget(item_widget) + self.input_fields.pop(row) + item_widget.setParent(None) + item_widget.deleteLater() + + if previous_field: + previous_field.order_changed() + + if next_field: + next_field.order_changed() + + if self.count() == 0: + self.add_row(is_empty=True) + + self._on_value_change() + self.updateGeometry() + + def apply_overrides(self, parent_values): + self._is_modified = False + if parent_values is NOT_SET or self.key not in parent_values: + override_value = NOT_SET + else: + override_value = parent_values[self.key] + + self.override_value = override_value + + if override_value is NOT_SET: + self._is_overriden = False + self._was_overriden = False + if self.has_studio_override: + value = self.studio_value + else: + value = self.default_value + else: + self._is_overriden = True + self._was_overriden = True + value = override_value + + self._is_modified = False + self._state = None + + self.set_value(value) + + def hierarchical_style_update(self): + for input_field in self.input_fields: + input_field.hierarchical_style_update() + self.update_style() + + def update_style(self): + if self._as_widget: + if not self.isEnabled(): + state = self.style_state(False, False, False, False) + else: + state = self.style_state( + False, + self._is_invalid, + False, + self._is_modified + ) + else: + state = self.style_state( + self.has_studio_override, + self.is_invalid, + self.is_overriden, + self.is_modified + ) + if self._state == state: + return + + if self.label_widget: + self.label_widget.setProperty("state", state) + self.label_widget.style().polish(self.label_widget) + + def item_value(self): + output = [] + for item in self.input_fields: + value = item.config_value() + if value is not NOT_SET: + output.append(value) + return output + + +class ListStrictWidget(QtWidgets.QWidget, InputObject): + value_changed = QtCore.Signal(object) + _default_input_value = None + + def __init__( + self, input_data, parent, + as_widget=False, label_widget=None, parent_widget=None + ): + if parent_widget is None: + parent_widget = parent + super(ListStrictWidget, self).__init__(parent_widget) + self.setObjectName("ListStrictWidget") + + self.initial_attributes(input_data, parent, as_widget) + + self.input_fields = [] + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 5) + layout.setSpacing(5) + + if not self.as_widget: + self.key = input_data["key"] + if not label_widget: + label_widget = QtWidgets.QLabel(input_data["label"], self) + layout.addWidget(label_widget, alignment=QtCore.Qt.AlignTop) + + self.label_widget = label_widget + + self._add_children(layout, input_data) + + def _add_children(self, layout, input_data): + inputs_widget = QtWidgets.QWidget(self) + inputs_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + layout.addWidget(inputs_widget) + + horizontal = input_data.get("horizontal", True) + if horizontal: + inputs_layout = QtWidgets.QHBoxLayout(inputs_widget) + else: + inputs_layout = QtWidgets.QGridLayout(inputs_widget) + + inputs_layout.setContentsMargins(0, 0, 0, 0) + inputs_layout.setSpacing(3) + + self.inputs_widget = inputs_widget + self.inputs_layout = inputs_layout + + children_item_mapping = [] + for child_configuration in input_data["object_types"]: + object_type = child_configuration["type"] + + item_widget = ListItem( + object_type, child_configuration, self, self.inputs_widget, + is_strict=True + ) + + self.input_fields.append(item_widget) + item_widget.value_changed.connect(self._on_value_change) + + label = child_configuration.get("label") + label_widget = None + if label: + label_widget = QtWidgets.QLabel(label, self) + + children_item_mapping.append((label_widget, item_widget)) + + if horizontal: + self._add_children_horizontally(children_item_mapping) + else: + self._add_children_vertically(children_item_mapping) + + self.updateGeometry() + + def _add_children_vertically(self, children_item_mapping): + any_has_label = False + for item_mapping in children_item_mapping: + if item_mapping[0]: + any_has_label = True + break + + row = self.inputs_layout.count() + if not any_has_label: + self.inputs_layout.setColumnStretch(1, 1) + for item_mapping in children_item_mapping: + item_widget = item_mapping[1] + self.inputs_layout.addWidget(item_widget, row, 0, 1, 1) + + spacer_widget = QtWidgets.QWidget(self.inputs_widget) + self.inputs_layout.addWidget(spacer_widget, row, 1, 1, 1) + row += 1 + + else: + self.inputs_layout.setColumnStretch(2, 1) + for label_widget, item_widget in children_item_mapping: + self.inputs_layout.addWidget( + label_widget, row, 0, 1, 1, + alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignTop + ) + self.inputs_layout.addWidget(item_widget, row, 1, 1, 1) + + spacer_widget = QtWidgets.QWidget(self.inputs_widget) + self.inputs_layout.addWidget(spacer_widget, row, 2, 1, 1) + row += 1 + + def _add_children_horizontally(self, children_item_mapping): + for label_widget, item_widget in children_item_mapping: + if label_widget: + self.inputs_layout.addWidget(label_widget, 0) + self.inputs_layout.addWidget(item_widget, 0) + + spacer_widget = QtWidgets.QWidget(self.inputs_widget) + self.inputs_layout.addWidget(spacer_widget, 1) + + @property + def default_input_value(self): + if self._default_input_value is None: + self.set_value(NOT_SET) + self._default_input_value = self.item_value() + return self._default_input_value + + def set_value(self, value): + if self._is_overriden: + method_name = "apply_overrides" + elif not self._has_studio_override: + method_name = "update_default_values" + else: + method_name = "update_studio_values" + + for idx, input_field in enumerate(self.input_fields): + if value is NOT_SET: + _value = value + else: + if idx > len(value) - 1: + _value = NOT_SET + else: + _value = value[idx] + _method = getattr(input_field, method_name) + _method(_value) + + def hierarchical_style_update(self): + for input_field in self.input_fields: + input_field.hierarchical_style_update() + self.update_style() + + def update_style(self): + if self._as_widget: + if not self.isEnabled(): + state = self.style_state(False, False, False, False) + else: + state = self.style_state( + False, + self._is_invalid, + False, + self._is_modified + ) + else: + state = self.style_state( + self.has_studio_override, + self.is_invalid, + self.is_overriden, + self.is_modified + ) + + if self._state == state: + return + + if self.label_widget: + self.label_widget.setProperty("state", state) + self.label_widget.style().polish(self.label_widget) + + def item_value(self): + output = [] + for item in self.input_fields: + output.append(item.config_value()) + return output + + +class ModifiableDictItem(QtWidgets.QWidget, SettingObject): + _btn_size = 20 + value_changed = QtCore.Signal(object) + + def __init__(self, object_type, input_modifiers, config_parent, parent): + super(ModifiableDictItem, self).__init__(parent) + + self._set_default_attributes() + self._parent = config_parent + + any_parent_as_widget = config_parent.as_widget + if not any_parent_as_widget: + any_parent_as_widget = config_parent.any_parent_as_widget + + self._any_parent_as_widget = any_parent_as_widget + self._any_parent_is_group = True + + self._is_empty = False + self.is_key_duplicated = False + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(3) + + ItemKlass = TypeToKlass.types[object_type] + + self.key_input = QtWidgets.QLineEdit(self) + self.key_input.setObjectName("DictKey") + + self.value_input = ItemKlass( + input_modifiers, + self, + as_widget=True, + label_widget=None + ) + self.add_btn = QtWidgets.QPushButton("+") + self.remove_btn = QtWidgets.QPushButton("-") + + self.add_btn.setFocusPolicy(QtCore.Qt.ClickFocus) + self.remove_btn.setFocusPolicy(QtCore.Qt.ClickFocus) + + self.add_btn.setProperty("btn-type", "tool-item") + self.remove_btn.setProperty("btn-type", "tool-item") + + self.spacer_widget = QtWidgets.QWidget(self) + self.spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + self.spacer_widget.setVisible(False) + + layout.addWidget(self.add_btn, 0) + layout.addWidget(self.remove_btn, 0) + layout.addWidget(self.key_input, 0) + layout.addWidget(self.spacer_widget, 1) + layout.addWidget(self.value_input, 1) + + self.setFocusProxy(self.value_input) + + self.add_btn.setFixedSize(self._btn_size, self._btn_size) + self.remove_btn.setFixedSize(self._btn_size, self._btn_size) + self.add_btn.clicked.connect(self.on_add_clicked) + self.remove_btn.clicked.connect(self.on_remove_clicked) + + self.key_input.textChanged.connect(self._on_value_change) + self.value_input.value_changed.connect(self._on_value_change) + + self.origin_key = NOT_SET + + def key_value(self): + return self.key_input.text() + + def is_key_invalid(self): + if self._is_empty: + return False + + if self.key_value() == "": + return True + + if self.is_key_duplicated: + return True + return False + + def _on_value_change(self, item=None): + self.update_style() + self.value_changed.emit(self) + + def update_default_values(self, key, value): + self.origin_key = key + self.key_input.setText(key) + self.value_input.update_default_values(value) + + def update_studio_values(self, key, value): + self.origin_key = key + self.key_input.setText(key) + self.value_input.update_studio_values(value) + + def apply_overrides(self, key, value): + self.origin_key = key + self.key_input.setText(key) + self.value_input.apply_overrides(value) + + @property + def is_group(self): + return self._parent.is_group + + def on_add_clicked(self): + if self._is_empty: + self.set_as_empty(False) + else: + self._parent.add_row(row=self.row() + 1) + + def on_remove_clicked(self): + self._parent.remove_row(self) + + def set_as_empty(self, is_empty=True): + self._is_empty = is_empty + + self.key_input.setVisible(not is_empty) + self.value_input.setVisible(not is_empty) + self.remove_btn.setEnabled(not is_empty) + self.spacer_widget.setVisible(is_empty) + self._on_value_change() + + @property + def any_parent_is_group(self): + return self._parent.any_parent_is_group + + def is_key_modified(self): + return self.key_value() != self.origin_key + + def is_value_modified(self): + return self.value_input.is_modified + + @property + def is_modified(self): + return self.is_value_modified() or self.is_key_modified() + + def hierarchical_style_update(self): + self.value_input.hierarchical_style_update() + self.update_style() + + @property + def is_invalid(self): + if self._is_empty: + return False + return self.is_key_invalid() or self.value_input.is_invalid + + def update_style(self): + state = "" + if not self._is_empty: + if self.is_key_invalid(): + state = "invalid" + elif self.is_key_modified(): + state = "modified" + + self.key_input.setProperty("state", state) + self.key_input.style().polish(self.key_input) + + def row(self): + return self._parent.input_fields.index(self) + + def item_value(self): + key = self.key_input.text() + value = self.value_input.item_value() + return {key: value} + + def config_value(self): + if self._is_empty: + return {} + return self.item_value() + + def mouseReleaseEvent(self, event): + return QtWidgets.QWidget.mouseReleaseEvent(self, event) + + +class ModifiableDict(QtWidgets.QWidget, InputObject): + default_input_value = {} + # Should be used only for dictionary with one datatype as value + # TODO this is actually input field (do not care if is group or not) + value_changed = QtCore.Signal(object) + expand_in_grid = True + + def __init__( + self, input_data, parent, + as_widget=False, label_widget=None, parent_widget=None + ): + if parent_widget is None: + parent_widget = parent + super(ModifiableDict, self).__init__(parent_widget) + self.setObjectName("ModifiableDict") + + self.initial_attributes(input_data, parent, as_widget) + + self.input_fields = [] + + self.key = input_data["key"] + + if input_data.get("highlight_content", False): + content_state = "hightlighted" + bottom_margin = 5 + else: + content_state = "" + bottom_margin = 0 + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + + if as_widget: + body_widget = None + else: + body_widget = ExpandingWidget(input_data["label"], self) + main_layout.addWidget(body_widget) + + self.body_widget = body_widget + self.label_widget = body_widget.label_widget + + collapsable = input_data.get("collapsable", True) + if collapsable: + collapsed = input_data.get("collapsed", True) + if not collapsed: + body_widget.toggle_content() + + else: + body_widget.hide_toolbox(hide_content=False) + + if body_widget is None: + content_parent_widget = self + else: + content_parent_widget = body_widget + + content_widget = QtWidgets.QWidget(content_parent_widget) + content_widget.setObjectName("ContentWidget") + content_widget.setProperty("content_state", content_state) + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setContentsMargins(CHILD_OFFSET, 3, 0, bottom_margin) + + if body_widget is None: + main_layout.addWidget(content_widget) + else: + body_widget.set_content_widget(content_widget) + + self.body_widget = body_widget + self.content_widget = content_widget + self.content_layout = content_layout + + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + self.object_type = input_data["object_type"] + self.input_modifiers = input_data.get("input_modifiers") or {} + + self.add_row(is_empty=True) + + def count(self): + return len(self.input_fields) + + def set_value(self, value): + previous_inputs = tuple(self.input_fields) + for item_key, item_value in value.items(): + self.add_row(key=item_key, value=item_value) + + for input_field in previous_inputs: + self.remove_row(input_field) + + if self.count() == 0: + self.add_row(is_empty=True) + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + fields_by_keys = collections.defaultdict(list) + for input_field in self.input_fields: + key = input_field.key_value() + fields_by_keys[key].append(input_field) + + for fields in fields_by_keys.values(): + if len(fields) == 1: + field = fields[0] + if field.is_key_duplicated: + field.is_key_duplicated = False + field.update_style() + else: + for field in fields: + field.is_key_duplicated = True + field.update_style() + + if self.is_overidable: + self._is_overriden = True + else: + self._has_studio_override = True + + if self._is_invalid: + self._is_modified = True + elif self._is_overriden: + self._is_modified = self.item_value() != self.override_value + elif self._has_studio_override: + self._is_modified = self.item_value() != self.studio_value + else: + self._is_modified = self.item_value() != self.default_value + + self.update_style() + + self.value_changed.emit(self) + + def hierarchical_style_update(self): + for input_field in self.input_fields: + input_field.hierarchical_style_update() + self.update_style() + + def update_style(self): + if self._as_widget: + if not self.isEnabled(): + state = self.style_state(False, False, False, False) + else: + state = self.style_state( + False, + self.is_invalid, + False, + self._is_modified + ) + else: + state = self.style_state( + self.has_studio_override, + self.is_invalid, + self.is_overriden, + self.is_modified + ) + if self._state == state: + return + + if state: + child_state = "child-{}".format(state) + else: + child_state = "" + + if self.body_widget: + self.body_widget.side_line_widget.setProperty("state", child_state) + self.body_widget.side_line_widget.style().polish( + self.body_widget.side_line_widget + ) + + if not self._as_widget: + self.label_widget.setProperty("state", state) + self.label_widget.style().polish(self.label_widget) + + self._state = state + + def all_item_values(self): + output = {} + for item in self.input_fields: + output.update(item.item_value()) + return output + + def item_value(self): + output = {} + for item in self.input_fields: + output.update(item.config_value()) + return output + + def add_row(self, row=None, key=None, value=None, is_empty=False): + # Create new item + item_widget = ModifiableDictItem( + self.object_type, self.input_modifiers, self, self.content_widget + ) + if is_empty: + item_widget.set_as_empty() + + item_widget.value_changed.connect(self._on_value_change) + + if row is None: + self.content_layout.addWidget(item_widget) + self.input_fields.append(item_widget) + else: + self.content_layout.insertWidget(row, item_widget) + self.input_fields.insert(row, item_widget) + + previous_input = None + for input_field in self.input_fields: + if previous_input is not None: + self.setTabOrder( + previous_input, input_field.key_input + ) + previous_input = input_field.value_input.focusProxy() + self.setTabOrder( + input_field.key_input, previous_input + ) + + # Set value if entered value is not None + # else (when add button clicked) trigger `_on_value_change` + if value is not None and key is not None: + if not self._has_studio_override: + item_widget.update_default_values(key, value) + elif self._is_overriden: + item_widget.apply_overrides(key, value) + else: + item_widget.update_studio_values(key, value) + self.hierarchical_style_update() + else: + self._on_value_change() + self.parent().updateGeometry() + + def remove_row(self, item_widget): + item_widget.value_changed.disconnect() + + self.content_layout.removeWidget(item_widget) + self.input_fields.remove(item_widget) + item_widget.setParent(None) + item_widget.deleteLater() + + if self.count() == 0: + self.add_row(is_empty=True) + + self._on_value_change() + self.parent().updateGeometry() + + @property + def is_invalid(self): + return self._is_invalid or self.child_invalid + + @property + def child_invalid(self): + for input_field in self.input_fields: + if input_field.is_invalid: + return True + return False + + +# Dictionaries +class DictWidget(QtWidgets.QWidget, SettingObject): + value_changed = QtCore.Signal(object) + expand_in_grid = True + + def __init__( + self, input_data, parent, + as_widget=False, label_widget=None, parent_widget=None + ): + if parent_widget is None: + parent_widget = parent + super(DictWidget, self).__init__(parent_widget) + + self.initial_attributes(input_data, parent, as_widget) + + self.input_fields = [] + + self.checkbox_widget = None + self.checkbox_key = input_data.get("checkbox_key") + + self.label_widget = label_widget + + if self.as_widget: + self._ui_as_widget(input_data) + else: + self._ui_as_item(input_data) + + def _ui_as_item(self, input_data): + self.key = input_data["key"] + if input_data.get("highlight_content", False): + content_state = "hightlighted" + bottom_margin = 5 + else: + content_state = "" + bottom_margin = 0 + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + + body_widget = ExpandingWidget(input_data["label"], self) + + main_layout.addWidget(body_widget) + + content_widget = QtWidgets.QWidget(body_widget) + content_widget.setObjectName("ContentWidget") + content_widget.setProperty("content_state", content_state) + content_layout = QtWidgets.QGridLayout(content_widget) + content_layout.setContentsMargins(CHILD_OFFSET, 5, 0, bottom_margin) + + body_widget.set_content_widget(content_widget) + + self.body_widget = body_widget + self.content_widget = content_widget + self.content_layout = content_layout + + self.label_widget = body_widget.label_widget + + for child_data in input_data.get("children", []): + self.add_children_gui(child_data) + + collapsable = input_data.get("collapsable", True) + if len(self.input_fields) == 1 and self.checkbox_widget: + body_widget.hide_toolbox(hide_content=True) + + elif collapsable: + collapsed = input_data.get("collapsed", True) + if not collapsed: + body_widget.toggle_content() + else: + body_widget.hide_toolbox(hide_content=False) + + def _ui_as_widget(self, input_data): + body = QtWidgets.QWidget(self) + body.setObjectName("DictAsWidgetBody") + + content_layout = QtWidgets.QGridLayout(body) + content_layout.setContentsMargins(5, 5, 5, 5) + self.content_layout = content_layout + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + layout.addWidget(body) + + for child_configuration in input_data["children"]: + self.add_children_gui(child_configuration) + + def add_children_gui(self, child_configuration): + item_type = child_configuration["type"] + klass = TypeToKlass.types.get(item_type) + + row = self.content_layout.rowCount() + if not getattr(klass, "is_input_type", False): + item = klass(child_configuration, self) + self.content_layout.addWidget(item, row, 0, 1, 2) + return item + + if self.checkbox_key and not self.checkbox_widget: + key = child_configuration.get("key") + if key == self.checkbox_key: + return self._add_checkbox_child(child_configuration) + + label_widget = None + if not klass.expand_in_grid: + label = child_configuration.get("label") + if label is not None: + label_widget = GridLabelWidget(label, self) + self.content_layout.addWidget(label_widget, row, 0, 1, 1) + + item = klass(child_configuration, self, label_widget=label_widget) + item.value_changed.connect(self._on_value_change) + + if label_widget: + label_widget.input_field = item + self.content_layout.addWidget(item, row, 1, 1, 1) + else: + self.content_layout.addWidget(item, row, 0, 1, 2) + + self.input_fields.append(item) + return item + + def _add_checkbox_child(self, child_configuration): + item = BooleanWidget( + child_configuration, self, label_widget=self.label_widget + ) + item.value_changed.connect(self._on_value_change) + + self.body_widget.add_widget_after_label(item) + self.checkbox_widget = item + self.input_fields.append(item) + return item + + def remove_overrides(self): + self._is_overriden = False + self._is_modified = False + for input_field in self.input_fields: + input_field.remove_overrides() + + def reset_to_pype_default(self): + for input_field in self.input_fields: + input_field.reset_to_pype_default() + self._has_studio_override = False + + def set_studio_default(self): + for input_field in self.input_fields: + input_field.set_studio_default() + + if self.is_group: + self._has_studio_override = True + + def discard_changes(self): + self._is_overriden = self._was_overriden + self._is_modified = False + + for input_field in self.input_fields: + input_field.discard_changes() + + self._is_modified = self.child_modified + + def set_as_overriden(self): + if self.is_overriden: + return + + if self.is_group: + self._is_overriden = True + return + + for item in self.input_fields: + item.set_as_overriden() + + def update_default_values(self, parent_values): + # Make sure this is set to False + self._state = None + self._child_state = None + + value = NOT_SET + if self.as_widget: + value = parent_values + elif parent_values is not NOT_SET: + value = parent_values.get(self.key, NOT_SET) + + for item in self.input_fields: + item.update_default_values(value) + + def update_studio_values(self, parent_values): + # Make sure this is set to False + self._state = None + self._child_state = None + value = NOT_SET + if self.as_widget: + value = parent_values + else: + if parent_values is not NOT_SET: + value = parent_values.get(self.key, NOT_SET) + + self._has_studio_override = False + if self.is_group and value is not NOT_SET: + self._has_studio_override = True + + self._had_studio_override = bool(self._has_studio_override) + + for item in self.input_fields: + item.update_studio_values(value) + + def apply_overrides(self, parent_values): + # Make sure this is set to False + self._state = None + self._child_state = None + + if not self.as_widget: + metadata = {} + groups = tuple() + override_values = NOT_SET + if parent_values is not NOT_SET: + metadata = parent_values.get(METADATA_KEY) or metadata + groups = metadata.get("groups") or groups + override_values = parent_values.get(self.key, override_values) + + self._is_overriden = self.key in groups + + for item in self.input_fields: + item.apply_overrides(override_values) + + if not self.as_widget: + if not self._is_overriden: + self._is_overriden = ( + self.is_group + and self.is_overidable + and self.child_overriden + ) + self._was_overriden = bool(self._is_overriden) + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + if self.is_group and not (self.as_widget or self.any_parent_as_widget): + if self.is_overidable: + self._is_overriden = True + else: + self._has_studio_override = True + + # TODO check if this is required + self.hierarchical_style_update() + + self.value_changed.emit(self) + + self.update_style() + + def hierarchical_style_update(self): + for input_field in self.input_fields: + input_field.hierarchical_style_update() + self.update_style() + + def update_style(self, is_overriden=None): + # TODO add style update when used as widget + if self.as_widget: + return + + child_has_studio_override = self.child_has_studio_override + child_modified = self.child_modified + child_invalid = self.child_invalid + child_state = self.style_state( + child_has_studio_override, + child_invalid, + self.child_overriden, + child_modified + ) + if child_state: + child_state = "child-{}".format(child_state) + + if child_state != self._child_state: + self.body_widget.side_line_widget.setProperty("state", child_state) + self.body_widget.side_line_widget.style().polish( + self.body_widget.side_line_widget + ) + self._child_state = child_state + + state = self.style_state( + self.had_studio_override, + child_invalid, + self.is_overriden, + self.is_modified + ) + if self._state == state: + return + + self.label_widget.setProperty("state", state) + self.label_widget.style().polish(self.label_widget) + + self._state = state + + @property + def is_modified(self): + if self.is_group: + return self._is_modified or self.child_modified + return False + + @property + def child_has_studio_override(self): + for input_field in self.input_fields: + if ( + input_field.has_studio_override + or input_field.child_has_studio_override + ): + return True + return False + + @property + def child_modified(self): + for input_field in self.input_fields: + if input_field.child_modified: + return True + return False + + @property + def child_overriden(self): + for input_field in self.input_fields: + if input_field.is_overriden or input_field.child_overriden: + return True + return False + + @property + def child_invalid(self): + for input_field in self.input_fields: + if input_field.child_invalid: + return True + return False + + def get_invalid(self): + output = [] + for input_field in self.input_fields: + output.extend(input_field.get_invalid()) + return output + + def item_value(self): + output = {} + for input_field in self.input_fields: + # TODO maybe merge instead of update should be used + # NOTE merge is custom function which merges 2 dicts + output.update(input_field.config_value()) + return output + + def studio_overrides(self): + if ( + not (self.as_widget or self.any_parent_as_widget) + and not self.has_studio_override + and not self.child_has_studio_override + ): + return NOT_SET, False + + values = {} + groups = [] + for input_field in self.input_fields: + value, is_group = input_field.studio_overrides() + if value is not NOT_SET: + values.update(value) + if is_group: + groups.extend(value.keys()) + if groups: + values[METADATA_KEY] = {"groups": groups} + return {self.key: values}, self.is_group + + def overrides(self): + if not self.is_overriden and not self.child_overriden: + return NOT_SET, False + + values = {} + groups = [] + for input_field in self.input_fields: + value, is_group = input_field.overrides() + if value is not NOT_SET: + values.update(value) + if is_group: + groups.extend(value.keys()) + if groups: + values[METADATA_KEY] = {"groups": groups} + return {self.key: values}, self.is_group + + +class DictInvisible(QtWidgets.QWidget, SettingObject): + # TODO is not overridable by itself + value_changed = QtCore.Signal(object) + allow_actions = False + expand_in_grid = True + + def __init__( + self, input_data, parent, + as_widget=False, label_widget=None, parent_widget=None + ): + if parent_widget is None: + parent_widget = parent + super(DictInvisible, self).__init__(parent_widget) + self.setObjectName("DictInvisible") + + self.initial_attributes(input_data, parent, as_widget) + + if self._is_group: + raise TypeError("DictInvisible can't be marked as group input.") + + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + layout = QtWidgets.QGridLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + + self.content_layout = layout + + self.input_fields = [] + + self.key = input_data["key"] + + for child_data in input_data.get("children", []): + self.add_children_gui(child_data) + + def add_children_gui(self, child_configuration): + item_type = child_configuration["type"] + klass = TypeToKlass.types.get(item_type) + + row = self.content_layout.rowCount() + if not getattr(klass, "is_input_type", False): + item = klass(child_configuration, self) + self.content_layout.addWidget(item, row, 0, 1, 2) + return item + + label_widget = None + if not klass.expand_in_grid: + label = child_configuration.get("label") + if label is not None: + label_widget = GridLabelWidget(label, self) + self.content_layout.addWidget(label_widget, row, 0, 1, 1) + + item = klass(child_configuration, self, label_widget=label_widget) + item.value_changed.connect(self._on_value_change) + + if label_widget: + label_widget.input_field = item + self.content_layout.addWidget(item, row, 1, 1, 1) + else: + self.content_layout.addWidget(item, row, 0, 1, 2) + + self.input_fields.append(item) + return item + + def update_style(self, *args, **kwargs): + return + + @property + def child_has_studio_override(self): + for input_field in self.input_fields: + if ( + input_field.has_studio_override + or input_field.child_has_studio_override + ): + return True + return False + + @property + def child_modified(self): + for input_field in self.input_fields: + if input_field.child_modified: + return True + return False + + @property + def child_overriden(self): + for input_field in self.input_fields: + if input_field.is_overriden or input_field.child_overriden: + return True + return False + + @property + def child_invalid(self): + for input_field in self.input_fields: + if input_field.child_invalid: + return True + return False + + def get_invalid(self): + output = [] + for input_field in self.input_fields: + output.extend(input_field.get_invalid()) + return output + + def item_value(self): + output = {} + for input_field in self.input_fields: + # TODO maybe merge instead of update should be used + # NOTE merge is custom function which merges 2 dicts + output.update(input_field.config_value()) + return output + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + if self.is_group and not self.any_parent_as_widget: + if self.is_overidable: + self._is_overriden = True + else: + self._has_studio_override = True + self.hierarchical_style_update() + + self.value_changed.emit(self) + + def hierarchical_style_update(self): + for input_field in self.input_fields: + input_field.hierarchical_style_update() + self.update_style() + + def remove_overrides(self): + self._is_overriden = False + self._is_modified = False + for input_field in self.input_fields: + input_field.remove_overrides() + + def reset_to_pype_default(self): + for input_field in self.input_fields: + input_field.reset_to_pype_default() + self._has_studio_override = False + + def set_studio_default(self): + for input_field in self.input_fields: + input_field.set_studio_default() + + if self.is_group: + self._has_studio_override = True + + def discard_changes(self): + self._is_modified = False + self._is_overriden = self._was_overriden + + for input_field in self.input_fields: + input_field.discard_changes() + + self._is_modified = self.child_modified + + def set_as_overriden(self): + if self.is_overriden: + return + + if self.is_group: + self._is_overriden = True + return + + for item in self.input_fields: + item.set_as_overriden() + + def update_default_values(self, parent_values): + value = NOT_SET + if self._as_widget: + value = parent_values + elif parent_values is not NOT_SET: + value = parent_values.get(self.key, NOT_SET) + + for item in self.input_fields: + item.update_default_values(value) + + def update_studio_values(self, parent_values): + value = NOT_SET + if parent_values is not NOT_SET: + value = parent_values.get(self.key, NOT_SET) + + for item in self.input_fields: + item.update_studio_values(value) + + def apply_overrides(self, parent_values): + # Make sure this is set to False + self._state = None + self._child_state = None + + metadata = {} + groups = tuple() + override_values = NOT_SET + if parent_values is not NOT_SET: + metadata = parent_values.get(METADATA_KEY) or metadata + groups = metadata.get("groups") or groups + override_values = parent_values.get(self.key, override_values) + + self._is_overriden = self.key in groups + + for item in self.input_fields: + item.apply_overrides(override_values) + + if not self._is_overriden: + self._is_overriden = ( + self.is_group + and self.is_overidable + and self.child_overriden + ) + self._was_overriden = bool(self._is_overriden) + + def studio_overrides(self): + if ( + not (self.as_widget or self.any_parent_as_widget) + and not self.has_studio_override + and not self.child_has_studio_override + ): + return NOT_SET, False + + values = {} + groups = [] + for input_field in self.input_fields: + value, is_group = input_field.studio_overrides() + if value is not NOT_SET: + values.update(value) + if is_group: + groups.extend(value.keys()) + if groups: + values[METADATA_KEY] = {"groups": groups} + return {self.key: values}, self.is_group + + def overrides(self): + if not self.is_overriden and not self.child_overriden: + return NOT_SET, False + + values = {} + groups = [] + for input_field in self.input_fields: + value, is_group = input_field.overrides() + if value is not NOT_SET: + values.update(value) + if is_group: + groups.extend(value.keys()) + if groups: + values[METADATA_KEY] = {"groups": groups} + return {self.key: values}, self.is_group + + +class PathWidget(QtWidgets.QWidget, SettingObject): + value_changed = QtCore.Signal(object) + platforms = ("windows", "darwin", "linux") + platform_labels_mapping = { + "windows": "Windows", + "darwin": "MacOS", + "linux": "Linux" + } + + def __init__( + self, input_data, parent, + as_widget=False, label_widget=None, parent_widget=None + ): + if parent_widget is None: + parent_widget = parent + super(PathWidget, self).__init__(parent_widget) + + self.initial_attributes(input_data, parent, as_widget) + + # This is partial input and dictionary input + if not self.any_parent_is_group and not self._as_widget: + self._is_group = True + else: + self._is_group = False + + self.multiplatform = input_data.get("multiplatform", False) + self.multipath = input_data.get("multipath", False) + + self.input_fields = [] + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + + if not self._as_widget: + self.key = input_data["key"] + if not label_widget: + label = input_data["label"] + label_widget = QtWidgets.QLabel(label) + label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + layout.addWidget(label_widget, 0, alignment=QtCore.Qt.AlignTop) + self.label_widget = label_widget + + self.content_widget = QtWidgets.QWidget(self) + self.content_layout = QtWidgets.QVBoxLayout(self.content_widget) + self.content_layout.setSpacing(0) + self.content_layout.setContentsMargins(0, 0, 0, 0) + + layout.addWidget(self.content_widget) + + self.create_gui() + + @property + def default_input_value(self): + if self.multipath: + value_type = list + else: + value_type = str + + if self.multiplatform: + return { + platform: value_type() + for platform in self.platforms + } + else: + return value_type() + + def create_gui(self): + if not self.multiplatform and not self.multipath: + input_data = {"key": self.key} + path_input = PathInputWidget( + input_data, self, label_widget=self.label_widget + ) + self.setFocusProxy(path_input) + self.content_layout.addWidget(path_input) + self.input_fields.append(path_input) + path_input.value_changed.connect(self._on_value_change) + return + + input_data_for_list = { + "object_type": "path-input" + } + if not self.multiplatform: + input_data_for_list["key"] = self.key + input_widget = ListWidget( + input_data_for_list, self, label_widget=self.label_widget + ) + self.setFocusProxy(input_widget) + self.content_layout.addWidget(input_widget) + self.input_fields.append(input_widget) + input_widget.value_changed.connect(self._on_value_change) + return + + proxy_widget = QtWidgets.QWidget(self.content_widget) + proxy_layout = QtWidgets.QFormLayout(proxy_widget) + for platform_key in self.platforms: + platform_label = self.platform_labels_mapping[platform_key] + label_widget = QtWidgets.QLabel(platform_label, proxy_widget) + if self.multipath: + input_data_for_list["key"] = platform_key + input_widget = ListWidget( + input_data_for_list, self, label_widget=label_widget + ) + else: + input_data = {"key": platform_key} + input_widget = PathInputWidget( + input_data, self, label_widget=label_widget + ) + proxy_layout.addRow(label_widget, input_widget) + self.input_fields.append(input_widget) + input_widget.value_changed.connect(self._on_value_change) + + self.setFocusProxy(self.input_fields[0]) + self.content_layout.addWidget(proxy_widget) + + def update_default_values(self, parent_values): + self._state = None + self._child_state = None + self._is_modified = False + + value = NOT_SET + if self._as_widget: + value = parent_values + elif parent_values is not NOT_SET: + if not self.multiplatform: + value = parent_values + else: + value = parent_values.get(self.key, NOT_SET) + + if value is NOT_SET: + if self.develop_mode: + if self._as_widget or not self.multiplatform: + value = {self.key: self.default_input_value} + else: + value = self.default_input_value + self.defaults_not_set = True + if value is NOT_SET: + raise NotImplementedError(( + "{} Does not have implemented" + " attribute `default_input_value`" + ).format(self)) + + else: + raise ValueError( + "Default value is not set. This is implementation BUG." + ) + else: + self.defaults_not_set = False + + self.default_value = value + self._has_studio_override = False + self._had_studio_override = False + + if not self.multiplatform: + self.input_fields[0].update_default_values(value) + else: + for input_field in self.input_fields: + input_field.update_default_values(value) + + def update_studio_values(self, parent_values): + self._state = None + self._child_state = None + self._is_modified = False + + value = NOT_SET + if self._as_widget: + value = parent_values + elif parent_values is not NOT_SET: + if not self.multiplatform: + value = parent_values + else: + value = parent_values.get(self.key, NOT_SET) + + self.studio_value = value + if value is not NOT_SET: + self._has_studio_override = True + self._had_studio_override = True + else: + self._has_studio_override = False + self._had_studio_override = False + value = self.default_value + + if not self.multiplatform: + self.input_fields[0].update_studio_values(value) + else: + for input_field in self.input_fields: + input_field.update_studio_values(value) + + def apply_overrides(self, parent_values): + self._is_modified = False + self._state = None + self._child_state = None + + override_values = NOT_SET + if self._as_widget: + override_values = parent_values + elif parent_values is not NOT_SET: + if not self.multiplatform: + override_values = parent_values + else: + override_values = parent_values.get(self.key, NOT_SET) + + self._is_overriden = override_values is not NOT_SET + self._was_overriden = bool(self._is_overriden) + + if not self.multiplatform: + self.input_fields[0].apply_overrides(parent_values) + else: + for input_field in self.input_fields: + input_field.apply_overrides(override_values) + + if not self._is_overriden: + self._is_overriden = ( + self.is_group + and self.is_overidable + and self.child_overriden + ) + self._is_modified = False + self._was_overriden = bool(self._is_overriden) + + def set_value(self, value): + if not self.multiplatform: + self.input_fields[0].set_value(value) + + else: + for input_field in self.input_fields: + _value = value[input_field.key] + input_field.set_value(_value) + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + if not self.any_parent_as_widget: + if self.is_overidable: + self._is_overriden = True + else: + self._has_studio_override = True + + if self._is_invalid: + self._is_modified = True + elif self._is_overriden: + self._is_modified = self.item_value() != self.override_value + elif self._has_studio_override: + self._is_modified = self.item_value() != self.studio_value + else: + self._is_modified = self.item_value() != self.default_value + + self.hierarchical_style_update() + + self.value_changed.emit(self) + + def update_style(self, is_overriden=None): + child_has_studio_override = self.child_has_studio_override + child_modified = self.child_modified + child_invalid = self.child_invalid + child_state = self.style_state( + child_has_studio_override, + child_invalid, + self.child_overriden, + child_modified + ) + if child_state: + child_state = "child-{}".format(child_state) + + if child_state != self._child_state: + self.setProperty("state", child_state) + self.style().polish(self) + self._child_state = child_state + + if not self._as_widget: + state = self.style_state( + child_has_studio_override, + child_invalid, + self.is_overriden, + self.is_modified + ) + if self._state == state: + return + + self.label_widget.setProperty("state", state) + self.label_widget.style().polish(self.label_widget) + + self._state = state + + def remove_overrides(self): + self._is_overriden = False + self._is_modified = False + for input_field in self.input_fields: + input_field.remove_overrides() + + def reset_to_pype_default(self): + for input_field in self.input_fields: + input_field.reset_to_pype_default() + self._has_studio_override = False + + def set_studio_default(self): + for input_field in self.input_fields: + input_field.set_studio_default() + + if self.is_group: + self._has_studio_override = True + + def discard_changes(self): + self._is_modified = False + self._is_overriden = self._was_overriden + + for input_field in self.input_fields: + input_field.discard_changes() + + self._is_modified = self.child_modified + + def set_as_overriden(self): + self._is_overriden = True + + @property + def child_has_studio_override(self): + for input_field in self.input_fields: + if ( + input_field.has_studio_override + or input_field.child_has_studio_override + ): + return True + return False + + @property + def child_modified(self): + for input_field in self.input_fields: + if input_field.child_modified: + return True + return False + + @property + def child_overriden(self): + for input_field in self.input_fields: + if input_field.child_overriden: + return True + return False + + @property + def child_invalid(self): + for input_field in self.input_fields: + if input_field.child_invalid: + return True + return False + + def hierarchical_style_update(self): + for input_field in self.input_fields: + input_field.hierarchical_style_update() + self.update_style() + + def item_value(self): + if not self.multiplatform and not self.multipath: + return self.input_fields[0].item_value() + + if not self.multiplatform: + return self.input_fields[0].item_value() + + output = {} + for input_field in self.input_fields: + output.update(input_field.config_value()) + return output + + def studio_overrides(self): + if ( + not (self.as_widget or self.any_parent_as_widget) + and not self.has_studio_override + and not self.child_has_studio_override + ): + return NOT_SET, False + + value = self.item_value() + if not self.multiplatform: + value = {self.key: value} + return value, self.is_group + + def overrides(self): + if not self.is_overriden and not self.child_overriden: + return NOT_SET, False + + value = self.item_value() + if not self.multiplatform: + value = {self.key: value} + return value, self.is_group + + +# Proxy for form layout +class FormLabel(QtWidgets.QLabel): + def __init__(self, *args, **kwargs): + super(FormLabel, self).__init__(*args, **kwargs) + self.item = None + + +class DictFormWidget(QtWidgets.QWidget, SettingObject): + value_changed = QtCore.Signal(object) + allow_actions = False + expand_in_grid = True + + def __init__( + self, input_data, parent, + as_widget=False, label_widget=None, parent_widget=None + ): + if parent_widget is None: + parent_widget = parent + super(DictFormWidget, self).__init__(parent_widget) + + self.initial_attributes(input_data, parent, as_widget) + + self._as_widget = False + self._is_group = False + + self.input_fields = [] + self.content_layout = QtWidgets.QFormLayout(self) + self.content_layout.setContentsMargins(0, 0, 0, 0) + + for child_data in input_data.get("children", []): + self.add_children_gui(child_data) + + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + def add_children_gui(self, child_configuration): + item_type = child_configuration["type"] + # Pop label to not be set in child + label = child_configuration["label"] + + klass = TypeToKlass.types.get(item_type) + + label_widget = FormLabel(label, self) + + item = klass(child_configuration, self, label_widget=label_widget) + label_widget.item = item + + item.value_changed.connect(self._on_value_change) + self.content_layout.addRow(label_widget, item) + self.input_fields.append(item) + return item + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.RightButton: + position = self.mapFromGlobal(QtGui.QCursor().pos()) + widget = self.childAt(position) + if widget and isinstance(widget, FormLabel): + widget.item.mouseReleaseEvent(event) + event.accept() + return + super(DictFormWidget, self).mouseReleaseEvent(event) + + def apply_overrides(self, parent_values): + for item in self.input_fields: + item.apply_overrides(parent_values) + + def discard_changes(self): + self._is_modified = False + self._is_overriden = self._was_overriden + + for item in self.input_fields: + item.discard_changes() + + self._is_modified = self.child_modified + + def remove_overrides(self): + self._is_overriden = False + self._is_modified = False + for input_field in self.input_fields: + input_field.remove_overrides() + + def reset_to_pype_default(self): + for input_field in self.input_fields: + input_field.reset_to_pype_default() + self._has_studio_override = False + + def set_studio_default(self): + for input_field in self.input_fields: + input_field.set_studio_default() + + if self.is_group: + self._has_studio_override = True + + def set_as_overriden(self): + if self.is_overriden: + return + + if self.is_group: + self._is_overriden = True + return + + for item in self.input_fields: + item.set_as_overriden() + + def update_default_values(self, value): + for item in self.input_fields: + item.update_default_values(value) + + def update_studio_values(self, value): + for item in self.input_fields: + item.update_studio_values(value) + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + self.value_changed.emit(self) + if self.any_parent_is_group: + self.hierarchical_style_update() + + @property + def child_has_studio_override(self): + for input_field in self.input_fields: + if ( + input_field.has_studio_override + or input_field.child_has_studio_override + ): + return True + return False + + @property + def child_modified(self): + for input_field in self.input_fields: + if input_field.child_modified: + return True + return False + + @property + def child_overriden(self): + for input_field in self.input_fields: + if input_field.is_overriden or input_field.child_overriden: + return True + return False + + @property + def child_invalid(self): + for input_field in self.input_fields: + if input_field.child_invalid: + return True + return False + + def get_invalid(self): + output = [] + for input_field in self.input_fields: + output.extend(input_field.get_invalid()) + return output + + def hierarchical_style_update(self): + for input_field in self.input_fields: + input_field.hierarchical_style_update() + + def item_value(self): + output = {} + for input_field in self.input_fields: + # TODO maybe merge instead of update should be used + # NOTE merge is custom function which merges 2 dicts + output.update(input_field.config_value()) + return output + + def config_value(self): + return self.item_value() + + def studio_overrides(self): + if ( + not (self.as_widget or self.any_parent_as_widget) + and not self.has_studio_override + and not self.child_has_studio_override + ): + return NOT_SET, False + + values = {} + groups = [] + for input_field in self.input_fields: + value, is_group = input_field.studio_overrides() + if value is not NOT_SET: + values.update(value) + if is_group: + groups.extend(value.keys()) + if groups: + values[METADATA_KEY] = {"groups": groups} + return values, self.is_group + + def overrides(self): + if not self.is_overriden and not self.child_overriden: + return NOT_SET, False + + values = {} + groups = [] + for input_field in self.input_fields: + value, is_group = input_field.overrides() + if value is not NOT_SET: + values.update(value) + if is_group: + groups.extend(value.keys()) + if groups: + values[METADATA_KEY] = {"groups": groups} + return values, self.is_group + + +class LabelWidget(QtWidgets.QWidget): + is_input_type = False + + def __init__(self, configuration, parent=None): + super(LabelWidget, self).__init__(parent) + self.setObjectName("LabelWidget") + + label = configuration["label"] + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(5, 5, 5, 5) + label_widget = QtWidgets.QLabel(label, self) + layout.addWidget(label_widget) + + +class SplitterWidget(QtWidgets.QWidget): + is_input_type = False + _height = 2 + + def __init__(self, configuration, parent=None): + super(SplitterWidget, self).__init__(parent) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(5, 5, 5, 5) + splitter_item = QtWidgets.QWidget(self) + splitter_item.setObjectName("SplitterItem") + splitter_item.setMinimumHeight(self._height) + splitter_item.setMaximumHeight(self._height) + layout.addWidget(splitter_item) + + +TypeToKlass.types["boolean"] = BooleanWidget +TypeToKlass.types["number"] = NumberWidget +TypeToKlass.types["text"] = TextWidget +TypeToKlass.types["path-input"] = PathInputWidget +TypeToKlass.types["raw-json"] = RawJsonWidget +TypeToKlass.types["list"] = ListWidget +TypeToKlass.types["list-strict"] = ListStrictWidget +TypeToKlass.types["dict-modifiable"] = ModifiableDict +# DEPRECATED - remove when removed from schemas +TypeToKlass.types["dict-item"] = DictWidget +TypeToKlass.types["dict"] = DictWidget +TypeToKlass.types["dict-invisible"] = DictInvisible +TypeToKlass.types["path-widget"] = PathWidget +TypeToKlass.types["form"] = DictFormWidget + +TypeToKlass.types["label"] = LabelWidget +TypeToKlass.types["splitter"] = SplitterWidget diff --git a/pype/tools/settings/settings/widgets/lib.py b/pype/tools/settings/settings/widgets/lib.py new file mode 100644 index 0000000000..f54989cfd7 --- /dev/null +++ b/pype/tools/settings/settings/widgets/lib.py @@ -0,0 +1,310 @@ +import os +import json +import copy +from pype.settings.lib import OVERRIDEN_KEY +from queue import Queue + + +# Singleton database of available inputs +class TypeToKlass: + types = {} + + +NOT_SET = type("NOT_SET", (), {"__bool__": lambda obj: False})() +METADATA_KEY = type("METADATA_KEY", (), {}) +OVERRIDE_VERSION = 1 +CHILD_OFFSET = 15 + + +def convert_gui_data_to_overrides(data, first=True): + if not data or not isinstance(data, dict): + return data + + output = {} + if first: + output["__override_version__"] = OVERRIDE_VERSION + + if METADATA_KEY in data: + metadata = data.pop(METADATA_KEY) + for key, value in metadata.items(): + if key == "groups": + output[OVERRIDEN_KEY] = value + else: + KeyError("Unknown metadata key \"{}\"".format(key)) + + for key, value in data.items(): + output[key] = convert_gui_data_to_overrides(value, False) + return output + + +def convert_overrides_to_gui_data(data, first=True): + if not data or not isinstance(data, dict): + return data + + output = {} + if OVERRIDEN_KEY in data: + groups = data.pop(OVERRIDEN_KEY) + if METADATA_KEY not in output: + output[METADATA_KEY] = {} + output[METADATA_KEY]["groups"] = groups + + for key, value in data.items(): + output[key] = convert_overrides_to_gui_data(value, False) + + return output + + +def _fill_inner_schemas(schema_data, schema_collection): + if schema_data["type"] == "schema": + raise ValueError("First item in schema data can't be schema.") + + children = schema_data.get("children") + if not children: + return schema_data + + new_children = [] + for child in children: + if child["type"] != "schema": + new_child = _fill_inner_schemas(child, schema_collection) + new_children.append(new_child) + continue + + new_child = _fill_inner_schemas( + schema_collection[child["name"]], + schema_collection + ) + new_children.append(new_child) + + schema_data["children"] = new_children + return schema_data + + +class SchemaMissingFileInfo(Exception): + def __init__(self, invalid): + full_path_keys = [] + for item in invalid: + full_path_keys.append("\"{}\"".format("/".join(item))) + + msg = ( + "Schema has missing definition of output file (\"is_file\" key)" + " for keys. [{}]" + ).format(", ".join(full_path_keys)) + super(SchemaMissingFileInfo, self).__init__(msg) + + +class SchemeGroupHierarchyBug(Exception): + def __init__(self, invalid): + full_path_keys = [] + for item in invalid: + full_path_keys.append("\"{}\"".format("/".join(item))) + + msg = ( + "Items with attribute \"is_group\" can't have another item with" + " \"is_group\" attribute as child. Error happened for keys: [{}]" + ).format(", ".join(full_path_keys)) + super(SchemeGroupHierarchyBug, self).__init__(msg) + + +class SchemaDuplicatedKeys(Exception): + def __init__(self, invalid): + items = [] + for key_path, keys in invalid.items(): + joined_keys = ", ".join([ + "\"{}\"".format(key) for key in keys + ]) + items.append("\"{}\" ({})".format(key_path, joined_keys)) + + msg = ( + "Schema items contain duplicated keys in one hierarchy level. {}" + ).format(" || ".join(items)) + super(SchemaDuplicatedKeys, self).__init__(msg) + + +def file_keys_from_schema(schema_data): + output = [] + item_type = schema_data["type"] + klass = TypeToKlass.types[item_type] + if not klass.is_input_type: + return output + + keys = [] + key = schema_data.get("key") + if key: + keys.append(key) + + for child in schema_data["children"]: + if child.get("is_file"): + _keys = copy.deepcopy(keys) + _keys.append(child["key"]) + output.append(_keys) + continue + + for result in file_keys_from_schema(child): + _keys = copy.deepcopy(keys) + _keys.extend(result) + output.append(_keys) + return output + + +def validate_all_has_ending_file(schema_data, is_top=True): + item_type = schema_data["type"] + klass = TypeToKlass.types[item_type] + if not klass.is_input_type: + return None + + if schema_data.get("is_file"): + return None + + children = schema_data.get("children") + if not children: + return [[schema_data["key"]]] + + invalid = [] + keyless = "key" not in schema_data + for child in children: + result = validate_all_has_ending_file(child, False) + if result is None: + continue + + if keyless: + invalid.extend(result) + else: + for item in result: + new_invalid = [schema_data["key"]] + new_invalid.extend(item) + invalid.append(new_invalid) + + if not invalid: + return None + + if not is_top: + return invalid + + raise SchemaMissingFileInfo(invalid) + + +def validate_is_group_is_unique_in_hierarchy( + schema_data, any_parent_is_group=False, keys=None +): + is_top = keys is None + if keys is None: + keys = [] + + keyless = "key" not in schema_data + + if not keyless: + keys.append(schema_data["key"]) + + invalid = [] + is_group = schema_data.get("is_group") + if is_group and any_parent_is_group: + invalid.append(copy.deepcopy(keys)) + + if is_group: + any_parent_is_group = is_group + + children = schema_data.get("children") + if not children: + return invalid + + for child in children: + result = validate_is_group_is_unique_in_hierarchy( + child, any_parent_is_group, copy.deepcopy(keys) + ) + if not result: + continue + + invalid.extend(result) + + if invalid and is_group and keys not in invalid: + invalid.append(copy.deepcopy(keys)) + + if not is_top: + return invalid + + if invalid: + raise SchemeGroupHierarchyBug(invalid) + + +def validate_keys_are_unique(schema_data, keys=None): + children = schema_data.get("children") + if not children: + return + + is_top = keys is None + if keys is None: + keys = [schema_data["key"]] + else: + keys.append(schema_data["key"]) + + child_queue = Queue() + for child in children: + child_queue.put(child) + + child_inputs = [] + while not child_queue.empty(): + child = child_queue.get() + if "key" not in child: + _children = child.get("children") or [] + for _child in _children: + child_queue.put(_child) + else: + child_inputs.append(child) + + duplicated_keys = set() + child_keys = set() + for child in child_inputs: + key = child["key"] + if key in child_keys: + duplicated_keys.add(key) + else: + child_keys.add(key) + + invalid = {} + if duplicated_keys: + joined_keys = "/".join(keys) + invalid[joined_keys] = duplicated_keys + + for child in child_inputs: + result = validate_keys_are_unique(child, copy.deepcopy(keys)) + if result: + invalid.update(result) + + if not is_top: + return invalid + + if invalid: + raise SchemaDuplicatedKeys(invalid) + + +def validate_schema(schema_data): + validate_all_has_ending_file(schema_data) + validate_is_group_is_unique_in_hierarchy(schema_data) + validate_keys_are_unique(schema_data) + + +def gui_schema(subfolder, main_schema_name): + subfolder, main_schema_name + dirpath = os.path.join( + os.path.dirname(os.path.dirname(__file__)), + "gui_schemas", + subfolder + ) + + loaded_schemas = {} + for filename in os.listdir(dirpath): + basename, ext = os.path.splitext(filename) + if ext != ".json": + continue + + filepath = os.path.join(dirpath, filename) + with open(filepath, "r") as json_stream: + schema_data = json.load(json_stream) + loaded_schemas[basename] = schema_data + + main_schema = _fill_inner_schemas( + loaded_schemas[main_schema_name], + loaded_schemas + ) + validate_schema(main_schema) + return main_schema diff --git a/pype/tools/settings/settings/widgets/tests.py b/pype/tools/settings/settings/widgets/tests.py new file mode 100644 index 0000000000..fc53e38ad5 --- /dev/null +++ b/pype/tools/settings/settings/widgets/tests.py @@ -0,0 +1,136 @@ +from Qt import QtWidgets, QtCore + + +def indented_print(data, indent=0): + spaces = " " * (indent * 4) + if not isinstance(data, dict): + print("{}{}".format(spaces, data)) + return + + for key, value in data.items(): + print("{}{}".format(spaces, key)) + indented_print(value, indent + 1) + + +class SelectableMenu(QtWidgets.QMenu): + + selection_changed = QtCore.Signal() + + def mouseReleaseEvent(self, event): + action = self.activeAction() + if action and action.isEnabled(): + action.trigger() + self.selection_changed.emit() + else: + super(SelectableMenu, self).mouseReleaseEvent(event) + + def event(self, event): + result = super(SelectableMenu, self).event(event) + if event.type() == QtCore.QEvent.Show: + parent = self.parent() + + move_point = parent.mapToGlobal(QtCore.QPoint(0, parent.height())) + check_point = ( + move_point + + QtCore.QPoint(self.width(), self.height()) + ) + visibility_check = ( + QtWidgets.QApplication.desktop().rect().contains(check_point) + ) + if not visibility_check: + move_point -= QtCore.QPoint(0, parent.height() + self.height()) + self.move(move_point) + + self.updateGeometry() + self.repaint() + + return result + + +class AddibleComboBox(QtWidgets.QComboBox): + """Searchable ComboBox with empty placeholder value as first value""" + + def __init__(self, placeholder="", parent=None): + super(AddibleComboBox, self).__init__(parent) + + self.setEditable(True) + # self.setInsertPolicy(self.NoInsert) + + self.lineEdit().setPlaceholderText(placeholder) + # self.lineEdit().returnPressed.connect(self.on_return_pressed) + + # Apply completer settings + completer = self.completer() + completer.setCompletionMode(completer.PopupCompletion) + completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) + + # def on_return_pressed(self): + # text = self.lineEdit().text().strip() + # if not text: + # return + # + # index = self.findText(text) + # if index < 0: + # self.addItems([text]) + # index = self.findText(text) + + 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 + + +class MultiselectEnum(QtWidgets.QWidget): + + selection_changed = QtCore.Signal() + + def __init__(self, title, parent=None): + super(MultiselectEnum, self).__init__(parent) + toolbutton = QtWidgets.QToolButton(self) + toolbutton.setText(title) + + toolmenu = SelectableMenu(toolbutton) + + toolbutton.setMenu(toolmenu) + toolbutton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) + + layout = QtWidgets.QHBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(toolbutton) + + self.setLayout(layout) + + toolmenu.selection_changed.connect(self.selection_changed) + + self.toolbutton = toolbutton + self.toolmenu = toolmenu + self.main_layout = layout + + def populate(self, items): + self.toolmenu.clear() + self.addItems(items) + + def addItems(self, items): + for item in items: + action = self.toolmenu.addAction(item) + action.setCheckable(True) + action.setChecked(True) + self.toolmenu.addAction(action) + + def items(self): + for action in self.toolmenu.actions(): + yield action diff --git a/pype/tools/settings/settings/widgets/widgets.py b/pype/tools/settings/settings/widgets/widgets.py new file mode 100644 index 0000000000..2a1f5fe804 --- /dev/null +++ b/pype/tools/settings/settings/widgets/widgets.py @@ -0,0 +1,281 @@ +from Qt import QtWidgets, QtCore, QtGui + + +class NumberSpinBox(QtWidgets.QDoubleSpinBox): + def __init__(self, *args, **kwargs): + min_value = kwargs.pop("minimum", -99999) + max_value = kwargs.pop("maximum", 99999) + decimals = kwargs.pop("decimal", 0) + super(NumberSpinBox, self).__init__(*args, **kwargs) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + self.setDecimals(decimals) + self.setMinimum(min_value) + self.setMaximum(max_value) + + def wheelEvent(self, event): + if self.hasFocus(): + super(NumberSpinBox, self).wheelEvent(event) + else: + event.ignore() + + def value(self): + output = super(NumberSpinBox, self).value() + if self.decimals() == 0: + output = int(output) + return output + + +class PathInput(QtWidgets.QLineEdit): + def clear_end_path(self): + value = self.text().strip() + if value.endswith("/"): + while value and value[-1] == "/": + value = value[:-1] + self.setText(value) + + def keyPressEvent(self, event): + # Always change backslash `\` for forwardslash `/` + if event.key() == QtCore.Qt.Key_Backslash: + event.accept() + new_event = QtGui.QKeyEvent( + event.type(), + QtCore.Qt.Key_Slash, + event.modifiers(), + "/", + event.isAutoRepeat(), + event.count() + ) + QtWidgets.QApplication.sendEvent(self, new_event) + return + super(PathInput, self).keyPressEvent(event) + + def focusOutEvent(self, event): + super(PathInput, self).focusOutEvent(event) + self.clear_end_path() + + +class ClickableWidget(QtWidgets.QWidget): + clicked = QtCore.Signal() + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self.clicked.emit() + super(ClickableWidget, self).mouseReleaseEvent(event) + + +class ExpandingWidget(QtWidgets.QWidget): + def __init__(self, label, parent): + super(ExpandingWidget, self).__init__(parent) + + self.toolbox_hidden = False + + top_part = ClickableWidget(parent=self) + + button_size = QtCore.QSize(5, 5) + button_toggle = QtWidgets.QToolButton(parent=top_part) + button_toggle.setProperty("btn-type", "expand-toggle") + button_toggle.setIconSize(button_size) + button_toggle.setArrowType(QtCore.Qt.RightArrow) + button_toggle.setCheckable(True) + button_toggle.setChecked(False) + + label_widget = QtWidgets.QLabel(label, parent=top_part) + label_widget.setObjectName("DictLabel") + + side_line_widget = QtWidgets.QWidget(top_part) + side_line_widget.setObjectName("SideLineWidget") + side_line_layout = QtWidgets.QHBoxLayout(side_line_widget) + side_line_layout.setContentsMargins(5, 10, 0, 10) + side_line_layout.addWidget(button_toggle) + side_line_layout.addWidget(label_widget) + + top_part_layout = QtWidgets.QHBoxLayout(top_part) + top_part_layout.setContentsMargins(0, 0, 0, 0) + top_part_layout.addWidget(side_line_widget) + + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + self.top_part_ending = None + self.after_label_layout = None + self.end_of_layout = None + + self.side_line_widget = side_line_widget + self.side_line_layout = side_line_layout + self.button_toggle = button_toggle + self.label_widget = label_widget + + top_part.clicked.connect(self._top_part_clicked) + self.button_toggle.clicked.connect(self._btn_clicked) + + self.main_layout = QtWidgets.QVBoxLayout(self) + self.main_layout.setContentsMargins(0, 0, 0, 0) + self.main_layout.setSpacing(0) + self.main_layout.addWidget(top_part) + + def hide_toolbox(self, hide_content=False): + self.button_toggle.setArrowType(QtCore.Qt.NoArrow) + self.toolbox_hidden = True + self.content_widget.setVisible(not hide_content) + self.parent().updateGeometry() + + def set_content_widget(self, content_widget): + content_widget.setVisible(False) + self.main_layout.addWidget(content_widget) + self.content_widget = content_widget + + def _btn_clicked(self): + self.toggle_content(self.button_toggle.isChecked()) + + def _top_part_clicked(self): + self.toggle_content() + + def toggle_content(self, *args): + if self.toolbox_hidden: + return + + if len(args) > 0: + checked = args[0] + else: + checked = not self.button_toggle.isChecked() + arrow_type = QtCore.Qt.RightArrow + if checked: + arrow_type = QtCore.Qt.DownArrow + self.button_toggle.setChecked(checked) + self.button_toggle.setArrowType(arrow_type) + self.content_widget.setVisible(checked) + self.parent().updateGeometry() + + def add_widget_after_label(self, widget): + self._add_side_widget_subwidgets() + self.after_label_layout.addWidget(widget) + + def _add_side_widget_subwidgets(self): + if self.top_part_ending is not None: + return + + top_part_ending = QtWidgets.QWidget(self.side_line_widget) + top_part_ending.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + top_part_ending_layout = QtWidgets.QHBoxLayout(top_part_ending) + top_part_ending_layout.setContentsMargins(0, 0, 0, 0) + top_part_ending_layout.setSpacing(0) + top_part_ending_layout.setAlignment(QtCore.Qt.AlignVCenter) + + after_label_widget = QtWidgets.QWidget(top_part_ending) + spacer_item = QtWidgets.QWidget(top_part_ending) + end_of_widget = QtWidgets.QWidget(top_part_ending) + + self.after_label_layout = QtWidgets.QVBoxLayout(after_label_widget) + self.after_label_layout.setContentsMargins(0, 0, 0, 0) + + self.end_of_layout = QtWidgets.QVBoxLayout(end_of_widget) + self.end_of_layout.setContentsMargins(0, 0, 0, 0) + + spacer_layout = QtWidgets.QVBoxLayout(spacer_item) + spacer_layout.setContentsMargins(0, 0, 0, 0) + + top_part_ending_layout.addWidget(after_label_widget, 0) + top_part_ending_layout.addWidget(spacer_item, 1) + top_part_ending_layout.addWidget(end_of_widget, 0) + + top_part_ending.setAttribute(QtCore.Qt.WA_TranslucentBackground) + after_label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + spacer_item.setAttribute(QtCore.Qt.WA_TranslucentBackground) + end_of_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + self.top_part_ending = top_part_ending + self.side_line_layout.addWidget(top_part_ending) + + def resizeEvent(self, event): + super(ExpandingWidget, self).resizeEvent(event) + self.content_widget.updateGeometry() + + +class UnsavedChangesDialog(QtWidgets.QDialog): + message = "You have unsaved changes. What do you want to do with them?" + + def __init__(self, parent=None): + super().__init__(parent) + message_label = QtWidgets.QLabel(self.message) + + btns_widget = QtWidgets.QWidget(self) + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + + btn_ok = QtWidgets.QPushButton("Save") + btn_ok.clicked.connect(self.on_ok_pressed) + btn_discard = QtWidgets.QPushButton("Discard") + btn_discard.clicked.connect(self.on_discard_pressed) + btn_cancel = QtWidgets.QPushButton("Cancel") + btn_cancel.clicked.connect(self.on_cancel_pressed) + + btns_layout.addWidget(btn_ok) + btns_layout.addWidget(btn_discard) + btns_layout.addWidget(btn_cancel) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(message_label) + layout.addWidget(btns_widget) + + self.state = None + + def on_cancel_pressed(self): + self.done(0) + + def on_ok_pressed(self): + self.done(1) + + def on_discard_pressed(self): + self.done(2) + + +class SpacerWidget(QtWidgets.QWidget): + def __init__(self, parent=None): + super(SpacerWidget, self).__init__(parent) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + +class GridLabelWidget(QtWidgets.QWidget): + def __init__(self, label, parent=None): + super(GridLabelWidget, self).__init__(parent) + + self.input_field = None + + self.properties = {} + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + label_proxy = QtWidgets.QWidget(self) + label_proxy_layout = QtWidgets.QHBoxLayout(label_proxy) + label_proxy_layout.setContentsMargins(0, 0, 0, 0) + label_proxy_layout.setSpacing(0) + + label_widget = QtWidgets.QLabel(label, label_proxy) + spacer_widget_h = SpacerWidget(label_proxy) + label_proxy_layout.addWidget( + spacer_widget_h, 0, alignment=QtCore.Qt.AlignRight + ) + label_proxy_layout.addWidget( + label_widget, 0, alignment=QtCore.Qt.AlignRight + ) + + spacer_widget_v = SpacerWidget(self) + + layout.addWidget(label_proxy, 0) + layout.addWidget(spacer_widget_v, 1) + + self.label_widget = label_widget + + def setProperty(self, name, value): + cur_value = self.properties.get(name) + if cur_value == value: + return + + self.label_widget.setProperty(name, value) + self.label_widget.style().polish(self.label_widget) + + def mouseReleaseEvent(self, event): + if self.input_field: + return self.input_field.show_actions_menu(event) + return super(GridLabelWidget, self).mouseReleaseEvent(event) diff --git a/pype/tools/settings/settings/widgets/window.py b/pype/tools/settings/settings/widgets/window.py new file mode 100644 index 0000000000..f83da8efe0 --- /dev/null +++ b/pype/tools/settings/settings/widgets/window.py @@ -0,0 +1,28 @@ +from Qt import QtWidgets +from .base import SystemWidget, ProjectWidget + + +class MainWidget(QtWidgets.QWidget): + widget_width = 1000 + widget_height = 600 + + def __init__(self, develop, parent=None): + super(MainWidget, self).__init__(parent) + self.setObjectName("MainWidget") + self.setWindowTitle("Pype Settings") + + self.resize(self.widget_width, self.widget_height) + + header_tab_widget = QtWidgets.QTabWidget(parent=self) + + studio_widget = SystemWidget(develop, header_tab_widget) + project_widget = ProjectWidget(develop, header_tab_widget) + header_tab_widget.addTab(studio_widget, "System") + header_tab_widget.addTab(project_widget, "Project") + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(5, 5, 5, 5) + layout.setSpacing(0) + layout.addWidget(header_tab_widget) + + self.setLayout(layout) diff --git a/pype/tools/standalonepublish/app.py b/pype/tools/standalonepublish/app.py index d139366a1c..feba46987f 100644 --- a/pype/tools/standalonepublish/app.py +++ b/pype/tools/standalonepublish/app.py @@ -1,7 +1,7 @@ from bson.objectid import ObjectId from Qt import QtWidgets, QtCore from widgets import AssetWidget, FamilyWidget, ComponentsWidget, ShadowWidget -from avalon.tools.libraryloader.io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB class Window(QtWidgets.QDialog): @@ -10,7 +10,7 @@ class Window(QtWidgets.QDialog): :param parent: Main widget that cares about all GUIs :type parent: QtWidgets.QMainWindow """ - _db = DbConnector() + _db = AvalonMongoDB() _jobs = {} valid_family = False valid_components = False diff --git a/pype/tools/standalonepublish/widgets/widget_asset.py b/pype/tools/standalonepublish/widgets/widget_asset.py index c468c9627b..6f041a535f 100644 --- a/pype/tools/standalonepublish/widgets/widget_asset.py +++ b/pype/tools/standalonepublish/widgets/widget_asset.py @@ -240,7 +240,7 @@ class AssetWidget(QtWidgets.QWidget): self.combo_projects.clear() if len(projects) > 0: self.combo_projects.addItems(projects) - self.dbcon.activate_project(projects[0]) + self.dbcon.Session["AVALON_PROJECT"] = projects[0] def on_project_change(self): projects = list() @@ -248,7 +248,7 @@ class AssetWidget(QtWidgets.QWidget): projects.append(project['name']) project_name = self.combo_projects.currentText() if project_name in projects: - self.dbcon.activate_project(project_name) + self.dbcon.Session["AVALON_PROJECT"] = project_name self.refresh() def _refresh_model(self): diff --git a/pype/tools/tray/modules_imports.json b/pype/tools/tray/modules_imports.json index e9526dcddb..6a278840ea 100644 --- a/pype/tools/tray/modules_imports.json +++ b/pype/tools/tray/modules_imports.json @@ -54,5 +54,10 @@ "type": "module", "import_path": "pype.modules.adobe_communicator", "fromlist": ["pype", "modules"] + }, { + "title": "Websocket Server", + "type": "module", + "import_path": "pype.modules.websocket_server", + "fromlist": ["pype", "modules"] } ] diff --git a/pype/version.py b/pype/version.py index 0d4f03098b..96fc614cb2 100644 --- a/pype/version.py +++ b/pype/version.py @@ -1 +1 @@ -__version__ = "2.11.5" +__version__ = "2.12.1"