├── .gitignore ├── 2d-list-view ├── 2d-list-view.png ├── README.md ├── environment.yaml ├── main.py └── main.qml ├── LICENSE ├── README.md ├── launcher-app ├── ContextDropDown.qml ├── launcher-app.png ├── main.py ├── main.qml └── mystudiologo.png ├── list-view ├── environment.yaml ├── list-view.png ├── main.py └── main.qml └── publisher-app ├── .gitignore ├── .style.yapf ├── AssetModel.qml ├── ComponentModel.qml ├── MyCompanyButton.qml ├── SmallCheckBox.qml ├── example-data ├── WideFloatRange.exr ├── lighting_default_v003 │ ├── beauty.1001.exr │ ├── beauty.1002.exr │ ├── beauty.1003.exr │ ├── beauty.1004.exr │ ├── beauty.1005.exr │ ├── beauty.1006.exr │ ├── beauty.1007.exr │ ├── beauty.1008.exr │ ├── beauty.1009.exr │ ├── beauty.1010.exr │ ├── beauty.1011.exr │ ├── beauty.1012.exr │ ├── beauty.1013.exr │ ├── beauty.1014.exr │ ├── beauty.1015.exr │ ├── beauty.1016.exr │ ├── beauty.1017.exr │ ├── beauty.1018.exr │ ├── beauty.1019.exr │ ├── beauty.1020.exr │ └── beauty.1021.exr └── lighting_default_v004 │ ├── rgba.1001.exr │ ├── rgba.1002.exr │ ├── rgba.1003.exr │ ├── rgba.1004.exr │ ├── rgba.1005.exr │ ├── rgba.1006.exr │ ├── rgba.1007.exr │ ├── rgba.1008.exr │ ├── rgba.1009.exr │ ├── rgba.1010.exr │ ├── rgba.1011.exr │ ├── rgba.1012.exr │ ├── rgba.1013.exr │ ├── rgba.1014.exr │ ├── rgba.1015.exr │ ├── rgba.1016.exr │ ├── rgba.1017.exr │ ├── rgba.1018.exr │ ├── rgba.1019.exr │ ├── rgba.1020.exr │ └── rgba.1021.exr ├── main.py ├── main.qml └── publisher-app.png /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used to ignore files which are generated 2 | # ---------------------------------------------------------------------------- 3 | *~ 4 | *.vscode 5 | *.qmlc 6 | *.autosave 7 | *.a 8 | *.core 9 | *.moc 10 | *.o 11 | *.obj 12 | *.orig 13 | *.rej 14 | *.so 15 | *.so.* 16 | *_pch.h.cpp 17 | *_resource.rc 18 | *.qm 19 | .#* 20 | *.*# 21 | core 22 | !core/ 23 | tags 24 | .DS_Store 25 | .directory 26 | *.debug 27 | Makefile* 28 | *.prl 29 | *.app 30 | moc_*.cpp 31 | ui_*.h 32 | qrc_*.cpp 33 | Thumbs.db 34 | *.res 35 | *.rc 36 | /.qmake.cache 37 | /.qmake.stash 38 | 39 | # qtcreator generated files 40 | *.pro.user* 41 | 42 | # xemacs temporary files 43 | *.flc 44 | 45 | # Vim temporary files 46 | .*.swp 47 | 48 | # Visual Studio generated files 49 | *.ib_pdb_index 50 | *.idb 51 | *.ilk 52 | *.pdb 53 | *.sln 54 | *.suo 55 | *.vcproj 56 | *vcproj.*.*.user 57 | *.ncb 58 | *.sdf 59 | *.opensdf 60 | *.vcxproj 61 | *vcxproj.* 62 | 63 | # MinGW generated files 64 | *.Debug 65 | *.Release 66 | 67 | # Python byte code 68 | *.pyc 69 | 70 | # Binaries 71 | # -------- 72 | *.dll 73 | *.exe 74 | 75 | -------------------------------------------------------------------------------- /2d-list-view/2d-list-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/2d-list-view/2d-list-view.png -------------------------------------------------------------------------------- /2d-list-view/README.md: -------------------------------------------------------------------------------- 1 | # Nested List View 2 | This example shows how to send a nested data structure (list of lists) from Python to QML. 3 | 4 | When the button is pressed, the QML frontend will request data to the backend, which will then retrieve it using a thread. 5 | This guarantees that the QML UI will not freeze (since you're not blocking its UI thread). 6 | 7 | After the thread on the backend has completed successfully, it will emit a signal and then the backend object will emit a signal himself containing all of the retrieved data. 8 | 9 | I think this example is a good one since you don't need to subclass any QAbstractListModel or other models at all. 10 | 11 | You're basically just sending JSON stuff between QML and python, which in my opinion is just super cool and very effective. 12 | In this way you can easily construct a nested list without writing too much code. 13 | 14 | For more simple scenarios, I would still refer to the list-view example, which exposes the model as a property of the backend and listens to its changes. 15 | -------------------------------------------------------------------------------- /2d-list-view/environment.yaml: -------------------------------------------------------------------------------- 1 | name: qml-env 2 | channels: 3 | - conda-forge 4 | - defaults 5 | dependencies: 6 | - ca-certificates=2019.9.11=hecc5488_0 7 | - certifi=2019.9.11=py27_0 8 | - clang=8.0.1=h770b8ee_1 9 | - clang-tools=8.0.1=h770b8ee_1 10 | - clangdev=8.0.1=h770b8ee_1 11 | - clangxx=8.0.1=1 12 | - icu=64.2=h6de7cb9_1 13 | - jpeg=9c=h1de35cc_1001 14 | - libclang=8.0.1=h770b8ee_1 15 | - libcxx=4.0.1=hcfea43d_1 16 | - libcxxabi=4.0.1=hcfea43d_1 17 | - libedit=3.1.20181209=hb402a30_0 18 | - libffi=3.2.1=h475c297_4 19 | - libiconv=1.15=h01d97ff_1005 20 | - libllvm8=8.0.1=h770b8ee_0 21 | - libpng=1.6.37=h2573ce8_0 22 | - libxml2=2.9.9=h12c6b28_5 23 | - libxslt=1.1.33=h320ff13_0 24 | - llvm-tools=8.0.1=0 25 | - llvmdev=8.0.1=h770b8ee_0 26 | - ncurses=6.1=h0a44026_1 27 | - nspr=4.20=h0a44026_1000 28 | - nss=3.46=h39b4b1f_0 29 | - openssl=1.1.1c=h01d97ff_0 30 | - pip=19.2.2=py27_0 31 | - pyside2=5.12.4=py27h9cca949_0 32 | - python=2.7.15=h8f8e585_6 33 | - qt=5.12.5=h1b46049_0 34 | - readline=7.0=h1de35cc_5 35 | - setuptools=41.0.1=py27_0 36 | - sqlite=3.29.0=ha441bb4_0 37 | - tk=8.6.8=ha441bb4_0 38 | - wheel=0.33.4=py27_0 39 | - xz=5.2.4=h1de35cc_1001 40 | - zlib=1.2.11=h1de35cc_3 41 | prefix: /Users/vvzen/miniconda3/envs/qml-env 42 | 43 | -------------------------------------------------------------------------------- /2d-list-view/main.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import os 3 | import sys 4 | import time 5 | import random 6 | 7 | from PySide2 import QtCore as qtc 8 | from PySide2 import QtGui as qtg 9 | from PySide2 import QtQml as qml 10 | 11 | CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) 12 | 13 | 14 | class RunnableSignals(qtc.QObject): 15 | completed = qtc.Signal(list) 16 | 17 | 18 | class RunnableExample(qtc.QRunnable): 19 | 20 | def __init__(self): 21 | super(RunnableExample, self).__init__() 22 | self.signals = RunnableSignals() 23 | 24 | def run(self): 25 | 26 | print('started long thread calculation..') 27 | time.sleep(1) 28 | 29 | # This is the moment where we might interrogate our backend 30 | item_name = "sc010_{r}".format(r=str(random.randint(0, 100)).zfill(4)) 31 | 32 | data_retrieved = [{ 33 | 'itemName': item_name, 34 | 'itemIsChecked': True, 35 | 'itemModel': [ 36 | { 37 | 'passName': 'beauty', 38 | 'path': '/mnt/projects/{name}/rgba.%04d.exr'.format(name=item_name) 39 | }, 40 | { 41 | 'passName': 'crypto', 42 | 'path': '/mnt/projects/{name}/crypto.%04d.exr'.format(name=item_name) 43 | }, 44 | { 45 | 'passName': 'position', 46 | 'path': '/mnt/projects/{name}/position.%04d.exr'.format(name=item_name) 47 | }] 48 | }] 49 | 50 | self.signals.completed.emit(data_retrieved) 51 | 52 | 53 | class Backend(qtc.QObject): 54 | 55 | dataRetrieved = qtc.Signal('QVariantList') 56 | 57 | def __init__(self): 58 | super(Backend, self).__init__() 59 | 60 | # This slot will be called from QML 61 | @qtc.Slot() 62 | def retrieve_data(self): 63 | 64 | print('This is a long function that will spawn a separate thread') 65 | 66 | separate_thread = RunnableExample() 67 | 68 | qtc.QThreadPool.globalInstance().start(separate_thread) 69 | # Connect the completed signal of the thread 70 | separate_thread.signals.completed.connect(self.on_thread_completed) 71 | 72 | def on_thread_completed(self, data): 73 | print('thread has finished') 74 | print(data) 75 | self.dataRetrieved.emit(data) 76 | 77 | 78 | def main(): 79 | app = qtg.QGuiApplication(sys.argv) 80 | 81 | engine = qml.QQmlApplicationEngine() 82 | 83 | # Bind the backend object in qml 84 | backend = Backend() 85 | engine.rootContext().setContextProperty('backend', backend) 86 | 87 | # Load the target .qml file 88 | engine.load(qtc.QUrl.fromLocalFile(os.path.join(CURRENT_DIR, 'main.qml'))) 89 | 90 | if not engine.rootObjects(): 91 | return -1 92 | 93 | return app.exec_() 94 | 95 | 96 | if __name__ == '__main__': 97 | sys.exit(main()) -------------------------------------------------------------------------------- /2d-list-view/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.11 2 | import QtQuick.Window 2.2 3 | import QtQuick.Controls 2.2 4 | import QtQuick.Layouts 1.4 5 | 6 | ApplicationWindow { 7 | id: root 8 | visible: true 9 | width: 640 10 | height: 480 11 | minimumWidth: 400 12 | minimumHeight: 200 13 | title: '2D List View' 14 | 15 | signal reDataRetrieved(var data) 16 | 17 | // Signal argument names are not propagated from Python to QML, 18 | // so we need to re-emit the signal 19 | Component.onCompleted: { 20 | backend.dataRetrieved.connect(reDataRetrieved) 21 | } 22 | 23 | onReDataRetrieved: { 24 | console.log('onReDataRetrieved on the QML Side') 25 | console.log(data) 26 | console.log('data items:') 27 | // Lets append each received item to the listView 28 | for (var i = 0; i < data.length; i++){ 29 | var item = data[i]; 30 | console.log(item) 31 | listViewModel.append(item) 32 | } 33 | } 34 | 35 | Label { 36 | id: textLabel 37 | anchors.top: parent.top 38 | anchors.topMargin: 16 39 | anchors.left: parent.left 40 | anchors.leftMargin: 16 41 | text: '2D ListView example' 42 | font.bold: true 43 | } 44 | 45 | ListView { 46 | id: listView 47 | clip: true 48 | anchors.top: textLabel.bottom 49 | anchors.topMargin: 8 50 | anchors.left: parent.left 51 | anchors.right: parent.right 52 | anchors.rightMargin: 8 53 | anchors.bottom: clickmeButton.top 54 | anchors.bottomMargin: 8 55 | width: 100 56 | height: 100 57 | 58 | model: ListModel { 59 | id: listViewModel 60 | } 61 | delegate: ColumnLayout { 62 | 63 | anchors.left: parent.left 64 | anchors.right: parent.right 65 | 66 | CheckBox { 67 | checked: itemIsChecked 68 | text: itemName 69 | font.bold: true 70 | } 71 | 72 | Column { 73 | 74 | Repeater { 75 | id: repeaterId 76 | model: itemModel 77 | delegate: RowLayout { 78 | 79 | width: 160 80 | 81 | Item { 82 | width: 8 83 | } 84 | 85 | Label { 86 | text: passName 87 | font.italic: true 88 | } 89 | 90 | Label { 91 | text: path 92 | } 93 | } 94 | } 95 | } 96 | 97 | Item { 98 | width: 10 99 | } 100 | } 101 | 102 | ScrollBar.vertical: ScrollBar { 103 | interactive: true 104 | policy: ScrollBar.AsNeeded 105 | } 106 | } 107 | 108 | Button { 109 | id: clickmeButton 110 | anchors.right: parent.right 111 | anchors.rightMargin: 16 112 | anchors.bottom: parent.bottom 113 | anchors.bottomMargin: 16 114 | 115 | text: 'Add Item' 116 | background: Rectangle { 117 | 118 | implicitWidth: 100 119 | implicitHeight: 40 120 | 121 | color: clickmeButton.down ? "#9ED624" : "#ABE827" 122 | radius: 4 123 | } 124 | 125 | onClicked: { 126 | console.log('clicked') 127 | console.log('calling get_data() slot from QML') 128 | 129 | // Here we call a slot on the python side 130 | // that will spawn a thread and take a loooong time 131 | backend.retrieve_data() 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Valerio Viperino 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyside2-qml-examples 2 | 3 | [![license](https://img.shields.io/github/license/vvzen/pyside2-qml-examples)](https://github.com/vvzen/pyside2-qml-examples/blob/main/LICENSE) 4 | ![Generic badge](https://img.shields.io/badge/status-wip-yellow.svg) 5 | 6 | There are very few examples online of **QML** using **PySide2** as a backend, so I decided to start collecting some simple ones while I learn this technology. 7 | I think QML is a great idea that clearly separates logic from presentation, so hopefully people in VFX and Animation will start using it (instead of handling all of the .ui to .py autogenerated code madness). 8 | 9 | I've read online about QML from people in VFX and there is a certain sentiment that it's only for creating fancy "mobile-like" experiences. 10 | In my humble opinion, they really aren't getting the big picture... 11 | 12 | #### Why I think QML can be good idea.. 13 | 14 | - I don't like looking at your 1000 lines of autogerated UI code just to get a feeling of the underneath data model 15 | 16 | - No need to use QtDesigner/QtCreator. Iterating a UI mockup is simple enough via `qmlscene` and vim, neovim, vscode, etc. 17 | 18 | - It makes it easier to make edits on the presentation layer without touching the python code 19 | 20 | - You can create components that define a certain look/behaviour and reuse them (buttons but even lists views, menu bars, etc..) 21 | 22 | - Yes, It's still a little bit of a PITA to use QML inside Nuke/Maya/Houdini, but things are slowly changing. 23 | In the meantime, nothing prevents you from doing standalone apps in QML, as long as you've got ways to properly package/source PySide2 (rez, conda, pip, tcl-modules, etc..) 24 | 25 | - If you're crazy enough and you realize in the mid of a project that you need more performance for the backend, you could potentially rewrite it in C++ and leave the frontend almost untouched 26 | 27 | - Great UI performance thanks to the new QtQuick Scene Graph which greatly reduces draw calls (https://doc.qt.io/qt-5/qtquick-visualcanvas-scenegraph.html) 28 | 29 | - It finally offers TreeViews and TreeModels! 30 | 31 | #### ..and why it's still challenging 32 | 33 | - You need to learn something new (pros for some, con for others) 34 | 35 | - There's not really a lot of huge amount of documentation for Python as a backend (but if you're here we can change that!) 36 | 37 | - You might find it harder to tweak some of the UI elements exactly as you'd like to 38 | 39 | - As a framework, it's not as old and mature as QtWidgets, so expect things to change or be deprecated between versions 40 | 41 | ## FAQs 42 | 43 | ### How to quickly preview some qml 44 | 45 | This is really useful for prototyping a UI without wanting to write a `main.py` at all. 46 | ```bash 47 | $ qmlscene main.qml 48 | ``` 49 | 50 | ### Ok, you convinced me. Where can I learn QtQml? 51 | 52 | - The official Qt website is a great reference: https://doc.qt.io/qt-5/qmlapplications.html 53 | - Youtube series by KDAB: https://www.youtube.com/watch?v=JxyTkXLbcV4 54 | - Udemy course for beginners: https://www.udemy.com/course/qt_quick_qml_tutorial_for_beginners/ 55 | 56 | # Examples 57 | 58 | Note: The following examples have been tested under `python3.7` and `pyside2=5.13.2`. 59 | They should also work under `python2.7`, but I haven't tested that 60 | 61 | ## list-view 62 | Very simple example showing how to populate a 1d list-view using a model. 63 | 64 | ![list-view/list-view.png](list-view/list-view.png) 65 | 66 | ## 2d-list-view 67 | Slightly more complex example showing how to implement a basic app that uses threads and signals to perform a long blocking calculation without blocking the UI. 68 | Also shows an example of a 2d list view where each row item contains sub items. 69 | 70 | ![2d-list-view/2d-list-view.png](2d-list-view/2d-list-view.png) 71 | 72 | ## publisher-app 73 | Boiler-plate code that shows how you could integrate drag & drop and some parsing logic in order to fill a list view. 74 | This could be for example used as the start of an app for publishing frames 75 | 76 | ![publisher-app/publisher-app.png](publisher-app/publisher-app.png) 77 | 78 | ## launcher-app 79 | An example of an application launcher where updating a dropdown menu causes the update of the others. Uses a reusable `ContextDropDown` qml component that works with a standard `ListModel` with a custom delegate. 80 | 81 | ![launcher-app/launcher-app.png](launcher-app/launcher-app.png) 82 | 83 | 84 | # How to contribute 85 | 86 | As always, PRs are welcome! 87 | There are different types of contributions that you could make. 88 | For now, I think that those would be nice: 89 | 90 | ## Readme & docs 91 | - Improve the wiki! (which is empty, at the moment) 92 | - Improve the readme 93 | - Add relevant links where people can learn more about PySide2 + QML 94 | - Start an issue to discuss about something relevant to everybody (IE: how the heck can I use this workflow in Nuke?) 95 | 96 | ## Code 97 | - Improving the existing examples to make it easier to follow through 98 | - Add more generic examples to showcase different usage scenarios (I'd love to see a Text autocompleter implemented in Qml!) 99 | - Check on all OS (I can only test on macOS and Centos 7.7!) 100 | - Write tests 101 | -------------------------------------------------------------------------------- /launcher-app/ContextDropDown.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Window 2.2 3 | import QtQuick.Controls 2.2 4 | import QtQuick.Layouts 1.4 5 | 6 | RowLayout { 7 | id: contextDropdown 8 | visible: true 9 | 10 | // Custom properties 11 | property string name 12 | property string currentElementName 13 | 14 | // See https://doc.qt.io/qt-5/qtqml-syntax-signals.html#adding-signals-to-custom-qml-types 15 | signal elementChosen(var elementName) 16 | 17 | function clear(){ 18 | dropdownModel.clear() 19 | } 20 | 21 | function addElements(elements){ 22 | for (let elementName of elements){ 23 | dropdownModel.append({'name': elementName}) 24 | } 25 | } 26 | 27 | Layout.alignment: Qt.AlignTop | Qt.AlignRight 28 | Layout.topMargin: customMargins() * 0.5 29 | 30 | Label { 31 | id: logoLabel 32 | text: contextDropdown.name 33 | font.bold: true 34 | color: "white" 35 | Layout.rightMargin: customMargins() 36 | } 37 | 38 | // Data model of the ComboBox 39 | // See https://doc.qt.io/qt-5/qml-qtqml-models-listmodel.html 40 | ListModel { 41 | id: dropdownModel 42 | } 43 | 44 | // Dictates how each element in the ComboBox will render 45 | Component { 46 | id: dropdownDelegate 47 | // https://doc.qt.io/qt-5/qml-qtquick-controls2-itemdelegate.html 48 | ItemDelegate { 49 | text: name 50 | } 51 | } 52 | 53 | // The dropdown menu 54 | // https://doc.qt.io/qt-5/qml-qtquick-controls2-combobox.html 55 | ComboBox { 56 | id: dropdown 57 | model: dropdownModel 58 | background: Rectangle { 59 | implicitWidth: root.width - logoLabel.width - (customMargins() * 4) 60 | implicitHeight: 40 61 | color: primaryColor(launchButton.down) 62 | radius: 4 63 | } 64 | onActivated: { 65 | console.log("User clicked on", contextDropdown.name) 66 | let current = this.model.get(index); 67 | currentElementName = current.name; 68 | elementChosen(current) 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /launcher-app/launcher-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/launcher-app/launcher-app.png -------------------------------------------------------------------------------- /launcher-app/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function 3 | import os 4 | import sys 5 | 6 | from PySide2 import QtCore as qtc 7 | from PySide2 import QtGui as qtg 8 | from PySide2 import QtQml as qml 9 | 10 | CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) 11 | 12 | # This just mimics whatever database you might have 13 | class MyDB(object): 14 | 15 | shows = { 16 | 'the_witcher_season_2': { 17 | 'sc010': ['sc010_0010', 'sc010_0010'], 18 | 'sc020': ['sc020_0040', 'sc020_0050'], 19 | }, 20 | 'lotr_season_1': { 21 | 'sc030': ['sc030_0020', 'sc030_0030'], 22 | 'sc040': ['sc040_0040', 'sc040_0050'], 23 | }, 24 | 'the_crown_season_5': { 25 | 'sc050': ['sc050_0010', 'sc050_0020'], 26 | 'sc060': ['sc060_0040', 'sc060_0050'], 27 | }, 28 | 'an_awfully_long_show_name_that_you_should_avoid': { 29 | 'sc090': ['sc090_0010', 'sc090_0020'], 30 | }, 31 | } 32 | 33 | dccs = { 34 | "maya": ["2021", "2022"], 35 | "houdini": ["18.5", "19.0", "19.5"], 36 | "nuke": ["11.2v3", "11.3v4", "12.3v2"], 37 | } 38 | 39 | 40 | # This class will be accessible within the QML context 41 | class Backend(qtc.QObject): 42 | 43 | db = MyDB() 44 | 45 | def __init__(self): 46 | super(Backend, self).__init__() 47 | 48 | @qtc.Slot(result='QVariantList') 49 | def get_shows(self): 50 | shows = sorted(list(self.db.shows.keys())) 51 | return shows 52 | 53 | @qtc.Slot(str, result='QVariantList') 54 | def get_sequences_for_show(self, show_name): 55 | sequences = sorted(list(self.db.shows[show_name].keys())) 56 | return sequences 57 | 58 | @qtc.Slot(str, str, result='QVariantList') 59 | def get_shots_for_sequence(self, show_name, sequence_name): 60 | shots = self.db.shows[show_name][sequence_name] 61 | return sorted(list(shots)) 62 | 63 | @qtc.Slot(result='QVariantList') 64 | def get_dccs(self): 65 | return sorted(list(self.db.dccs.keys())) 66 | 67 | @qtc.Slot(str, result='QVariantList') 68 | def get_dcc_versions(self, dcc_name): 69 | return sorted(list(self.db.dccs[dcc_name])) 70 | 71 | # A QVariantMap can be used to pass a POJO(1) to Python and interpret them 72 | # as native python dictionaries 73 | # (1) -> Plain Old Javascript Object 74 | @qtc.Slot('QVariantMap') 75 | def launch(self, context): 76 | print(context) 77 | context = dict(context) 78 | print(("Running {show}/{sequence}/{shot} " 79 | "in {dcc} {dcc_version}").format(**context)) 80 | 81 | 82 | def main(): 83 | 84 | app = qtg.QGuiApplication(sys.argv) 85 | 86 | engine = qml.QQmlApplicationEngine() 87 | 88 | # Make the backend object accessible in QML via the 'backend' keyword 89 | # See https://doc.qt.io/qt-5/qqmlcontext.html 90 | backend = Backend() 91 | engine.rootContext().setContextProperty('backend', backend) 92 | 93 | # Load the qml file 94 | engine.load(qtc.QUrl(os.path.join(CURRENT_DIR, 'main.qml'))) 95 | 96 | if not engine.rootObjects: 97 | sys.exit(-1) 98 | 99 | sys.exit(app.exec_()) 100 | 101 | 102 | if __name__ == '__main__': 103 | main() 104 | -------------------------------------------------------------------------------- /launcher-app/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Window 2.2 3 | import QtQuick.Controls 2.2 4 | import QtQuick.Layouts 1.4 5 | 6 | 7 | ApplicationWindow { 8 | id: root 9 | visible: true 10 | width: 440 11 | height: 460 12 | color: "#222" 13 | minimumWidth: 400 14 | minimumHeight: 460 15 | maximumHeight: 460 16 | title: 'Launcher App' 17 | 18 | // Custom properties that we want to keep track of at the application level 19 | property string currentShow 20 | property string currentSequence 21 | property string currentShot 22 | property string currentDCC 23 | property string currentDCCVersion 24 | 25 | function customMargins(){ 26 | return 16 27 | } 28 | 29 | function primaryColor(){ 30 | return "#ABE827" 31 | } 32 | 33 | function primaryColorPressed(){ 34 | return "#ddd" 35 | } 36 | 37 | ColumnLayout { 38 | 39 | anchors.top: parent.top 40 | anchors.bottom: parent.bottom 41 | anchors.bottomMargin: customMargins() 42 | anchors.left: parent.left 43 | anchors.right: parent.right 44 | 45 | // Branding of your studio 46 | Image { 47 | Layout.alignment: Qt.AlignCenter 48 | Layout.topMargin: customMargins() 49 | Layout.leftMargin: customMargins() 50 | source: "mystudiologo.png" 51 | } 52 | 53 | // Show 54 | ContextDropDown { 55 | Layout.alignment: Qt.AlignCenter 56 | id: showContext 57 | name: 'Show' 58 | onElementChosen: { 59 | currentShow = currentElementName 60 | console.log("Current Show:", currentShow) 61 | let sequences = backend.get_sequences_for_show(currentShow) 62 | sequenceContext.clear() 63 | shotContext.clear() 64 | sequenceContext.addElements(sequences) 65 | } 66 | Component.onCompleted: { 67 | let shows = backend.get_shows() 68 | this.addElements(shows) 69 | } 70 | } 71 | 72 | // Sequence 73 | ContextDropDown { 74 | Layout.alignment: Qt.AlignCenter 75 | id: sequenceContext 76 | name: 'Sequence' 77 | onElementChosen: { 78 | currentSequence = currentElementName 79 | console.log("Current Sequence:", currentSequence) 80 | let shots = backend.get_shots_for_sequence(currentShow, 81 | currentSequence) 82 | shotContext.clear() 83 | shotContext.addElements(shots) 84 | } 85 | } 86 | 87 | // Shot 88 | ContextDropDown { 89 | Layout.alignment: Qt.AlignCenter 90 | id: shotContext 91 | name: 'Shot' 92 | onElementChosen: { 93 | currentShot = currentElementName 94 | console.log("Current Shot:", currentShot) 95 | } 96 | } 97 | 98 | // DCC 99 | ContextDropDown { 100 | Layout.alignment: Qt.AlignCenter 101 | id: dccContext 102 | name: 'DCC' 103 | onElementChosen: { 104 | currentDCC = currentElementName 105 | console.log("Current DCC", currentDCC) 106 | let versions = backend.get_dcc_versions(currentDCC) 107 | dccVersion.clear() 108 | dccVersion.addElements(versions) 109 | } 110 | Component.onCompleted: { 111 | let dccs = backend.get_dccs() 112 | this.addElements(dccs) 113 | } 114 | } 115 | 116 | // DCC version 117 | ContextDropDown { 118 | Layout.alignment: Qt.AlignCenter 119 | id: dccVersion 120 | name: 'DCC Version' 121 | onElementChosen: { 122 | currentDCCVersion = currentElementName 123 | } 124 | } 125 | 126 | // Filler 127 | Item { 128 | height: customMargins() 129 | } 130 | 131 | Button { 132 | id: launchButton 133 | text: 'Launch' 134 | Layout.alignment: Qt.AlignCenter 135 | background: Rectangle { 136 | implicitWidth: 100 137 | implicitHeight: 40 138 | color: launchButton.down ? primaryColorPressed() : primaryColor() 139 | radius: 4 140 | } 141 | 142 | onClicked: { 143 | let context = { 144 | show: currentShow, 145 | sequence: currentSequence, 146 | shot: currentShot, 147 | dcc: currentDCC, 148 | dcc_version: currentDCCVersion 149 | } 150 | backend.launch(context) 151 | } 152 | } 153 | } 154 | } -------------------------------------------------------------------------------- /launcher-app/mystudiologo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/launcher-app/mystudiologo.png -------------------------------------------------------------------------------- /list-view/environment.yaml: -------------------------------------------------------------------------------- 1 | name: pipeline-gui 2 | channels: 3 | - defaults 4 | dependencies: 5 | - ca-certificates=2019.1.23=0 6 | - certifi=2018.11.29=py27_0 7 | - libcxx=4.0.1=hcfea43d_1 8 | - libcxxabi=4.0.1=hcfea43d_1 9 | - libedit=3.1.20170329=hb402a30_2 10 | - libffi=3.2.1=h475c297_4 11 | - ncurses=6.1=h0a44026_1 12 | - openssl=1.1.1a=h1de35cc_0 13 | - pip=18.1=py27_0 14 | - python=2.7.15=h8f8e585_6 15 | - readline=7.0=h1de35cc_5 16 | - rope=0.11.0=py27_0 17 | - setuptools=40.6.3=py27_0 18 | - sqlite=3.26.0=ha441bb4_0 19 | - tk=8.6.8=ha441bb4_0 20 | - wheel=0.32.3=py27_0 21 | - zlib=1.2.11=h1de35cc_3 22 | - pip: 23 | - altgraph==0.16.1 24 | - arrow==0.12.1 25 | - atomicwrites==1.3.0 26 | - attrs==18.2.0 27 | - backports-functools-lru-cache==1.5 28 | - chardet==3.0.4 29 | - clique==1.5.0 30 | - dis3==0.1.3 31 | - ftrack-python-api==1.7.1 32 | - funcsigs==1.0.2 33 | - future==0.17.1 34 | - idna==2.8 35 | - macholib==1.11 36 | - more-itertools==5.0.0 37 | - pathlib2==2.3.3 38 | - pefile==2019.4.18 39 | - pluggy==0.8.1 40 | - py==1.7.0 41 | - pyinstaller==3.4 42 | - pyparsing==2.3.0 43 | - pyside2==5.12.0 44 | - pytest==4.2.0 45 | - python-dateutil==2.7.5 46 | - requests==2.21.0 47 | - scandir==1.9.0 48 | - shiboken2==5.12.0 49 | - six==1.12.0 50 | - termcolor==1.1.0 51 | - urllib3==1.24.1 52 | - websocket-client==0.54.0 53 | prefix: /Users/vvzen/miniconda3/envs/pipeline-gui 54 | 55 | -------------------------------------------------------------------------------- /list-view/list-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/list-view/list-view.png -------------------------------------------------------------------------------- /list-view/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from PySide2 import QtCore as qtc 5 | from PySide2 import QtGui as qtg 6 | from PySide2 import QtQml as qml 7 | 8 | CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | 11 | class MyListModel(qtc.QAbstractListModel): 12 | # Our custom roles 13 | NameRole = qtc.Qt.UserRole + 1000 14 | CheckedRole = qtc.Qt.UserRole + 1002 15 | 16 | def __init__(self, parent=None): 17 | super(MyListModel, self).__init__(parent) 18 | self._assets = [] 19 | 20 | def rowCount(self, parent=qtc.QModelIndex()): 21 | if parent.isValid(): 22 | return 0 23 | return len(self._assets) 24 | 25 | def data(self, index, role=qtc.Qt.DisplayRole): 26 | 27 | if 0 <= index.row() < self.rowCount() and index.isValid(): 28 | item = self._assets[index.row()] 29 | 30 | if role == MyListModel.NameRole: 31 | return item['assetName'] 32 | 33 | elif role == MyListModel.CheckedRole: 34 | return item['assetIsChecked'] 35 | 36 | def roleNames(self): 37 | roles = dict() 38 | roles[MyListModel.NameRole] = b'assetName' 39 | roles[MyListModel.CheckedRole] = b'assetIsChecked' 40 | return roles 41 | 42 | # This can be called from the QML side 43 | @qtc.Slot(str, bool) 44 | def appendRow(self, name, ischecked): 45 | self.beginInsertRows(qtc.QModelIndex(), self.rowCount(), 46 | self.rowCount()) 47 | self._assets.append({'assetName': name, 'assetIsChecked': ischecked}) 48 | self.endInsertRows() 49 | 50 | 51 | class Backend(qtc.QObject): 52 | 53 | modelChanged = qtc.Signal() 54 | 55 | def __init__(self, parent=None): 56 | super(Backend, self).__init__(parent) 57 | self._model = MyListModel() 58 | 59 | # Expose model as a property of our backend 60 | @qtc.Property(qtc.QObject, constant=False, notify=modelChanged) 61 | def model(self): 62 | return self._model 63 | 64 | 65 | def test_add_item(model): 66 | model.appendRow('test_item', True) 67 | 68 | 69 | def main(): 70 | app = qtg.QGuiApplication(sys.argv) 71 | 72 | engine = qml.QQmlApplicationEngine() 73 | 74 | # Bind the backend object in qml 75 | backend = Backend() 76 | engine.rootContext().setContextProperty('backend', backend) 77 | 78 | engine.load(qtc.QUrl.fromLocalFile(os.path.join(CURRENT_DIR, 'main.qml'))) 79 | 80 | test_add_item(backend.model) 81 | 82 | if not engine.rootObjects(): 83 | return -1 84 | 85 | return app.exec_() 86 | 87 | 88 | if __name__ == '__main__': 89 | sys.exit(main()) -------------------------------------------------------------------------------- /list-view/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.11 2 | import QtQuick.Window 2.2 3 | import QtQuick.Controls 2.2 4 | import QtQuick.Layouts 1.4 5 | 6 | ApplicationWindow { 7 | id: root 8 | visible: true 9 | width: 640 10 | height: 480 11 | 12 | DropArea { 13 | id: dropArea 14 | width: root.width 15 | height: root.height 16 | 17 | onDropped: { 18 | console.log('dropped files') 19 | } 20 | } 21 | 22 | Label { 23 | id: textLabel 24 | anchors.top: parent.top 25 | anchors.topMargin: 16 26 | anchors.left: parent.left 27 | anchors.leftMargin: 16 28 | text: 'ListView example' 29 | font.bold: true 30 | } 31 | 32 | ListView { 33 | id: listView 34 | clip: true 35 | anchors.top: textLabel.bottom 36 | anchors.topMargin: 8 37 | anchors.left: parent.left 38 | anchors.right: parent.right 39 | anchors.bottom: clickmeButton.top 40 | anchors.bottomMargin: 8 41 | width: 100 42 | height: 100 43 | 44 | model: backend.model 45 | delegate: RowLayout { 46 | 47 | CheckBox { 48 | checked: assetIsChecked 49 | text: assetName 50 | } 51 | 52 | Item { width: 10 } 53 | } 54 | 55 | ScrollBar.vertical: ScrollBar {} 56 | } 57 | 58 | Button { 59 | id: clickmeButton 60 | anchors.right: parent.right 61 | anchors.rightMargin: 16 62 | anchors.bottom: parent.bottom 63 | anchors.bottomMargin: 16 64 | 65 | text: 'Add Item' 66 | background: Rectangle { 67 | 68 | implicitWidth: 100 69 | implicitHeight: 40 70 | 71 | color: clickmeButton.down ? "#9ED624" : "#ABE827" 72 | radius: 4 73 | } 74 | 75 | onClicked: { 76 | console.log('clicked') 77 | var itemNum = listView.model.rowCount(); 78 | console.log('num of items: ' + itemNum); 79 | var isChecked = Math.round(Math.random()); 80 | 81 | listView.model.appendRow(`item_${itemNum}`, isChecked) 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /publisher-app/.gitignore: -------------------------------------------------------------------------------- 1 | qml.qrc 2 | *.pro 3 | *.pro.user 4 | *.qmlc 5 | *.vscode 6 | *.pyc 7 | -------------------------------------------------------------------------------- /publisher-app/.style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | # Align closing bracket with visual indentation. 3 | align_closing_bracket_with_visual_indent=False 4 | 5 | # Allow dictionary keys to exist on multiple lines. For example: 6 | # 7 | # x = { 8 | # ('this is the first element of a tuple', 9 | # 'this is the second element of a tuple'): 10 | # value, 11 | # } 12 | allow_multiline_dictionary_keys=False 13 | 14 | # Allow lambdas to be formatted on more than one line. 15 | allow_multiline_lambdas=False 16 | 17 | # Allow splitting before a default / named assignment in an argument list. 18 | allow_split_before_default_or_named_assigns=True 19 | 20 | # Allow splits before the dictionary value. 21 | allow_split_before_dict_value=False 22 | 23 | # Let spacing indicate operator precedence. For example: 24 | # 25 | # a = 1 * 2 + 3 / 4 26 | # b = 1 / 2 - 3 * 4 27 | # c = (1 + 2) * (3 - 4) 28 | # d = (1 - 2) / (3 + 4) 29 | # e = 1 * 2 - 3 30 | # f = 1 + 2 + 3 + 4 31 | # 32 | # will be formatted as follows to indicate precedence: 33 | # 34 | # a = 1*2 + 3/4 35 | # b = 1/2 - 3*4 36 | # c = (1+2) * (3-4) 37 | # d = (1-2) / (3+4) 38 | # e = 1*2 - 3 39 | # f = 1 + 2 + 3 + 4 40 | # 41 | arithmetic_precedence_indication=True 42 | 43 | # Number of blank lines surrounding top-level function and class 44 | # definitions. 45 | blank_lines_around_top_level_definition=2 46 | 47 | # Insert a blank line before a class-level docstring. 48 | blank_line_before_class_docstring=False 49 | 50 | # Insert a blank line before a module docstring. 51 | blank_line_before_module_docstring=False 52 | 53 | # Insert a blank line before a 'def' or 'class' immediately nested 54 | # within another 'def' or 'class'. For example: 55 | # 56 | # class Foo: 57 | # # <------ this blank line 58 | # def method(): 59 | # ... 60 | blank_line_before_nested_class_or_def=True 61 | 62 | # Do not split consecutive brackets. Only relevant when 63 | # dedent_closing_brackets is set. For example: 64 | # 65 | # call_func_that_takes_a_dict( 66 | # { 67 | # 'key1': 'value1', 68 | # 'key2': 'value2', 69 | # } 70 | # ) 71 | # 72 | # would reformat to: 73 | # 74 | # call_func_that_takes_a_dict({ 75 | # 'key1': 'value1', 76 | # 'key2': 'value2', 77 | # }) 78 | coalesce_brackets=False 79 | 80 | # The column limit. 81 | column_limit=80 82 | 83 | # The style for continuation alignment. Possible values are: 84 | # 85 | # - SPACE: Use spaces for continuation alignment. This is default behavior. 86 | # - FIXED: Use fixed number (CONTINUATION_INDENT_WIDTH) of columns 87 | # (ie: CONTINUATION_INDENT_WIDTH/INDENT_WIDTH tabs) for continuation 88 | # alignment. 89 | # - VALIGN-RIGHT: Vertically align continuation lines with indent 90 | # characters. Slightly right (one more indent character) if cannot 91 | # vertically align continuation lines with indent characters. 92 | # 93 | # For options FIXED, and VALIGN-RIGHT are only available when USE_TABS is 94 | # enabled. 95 | continuation_align_style=SPACE 96 | 97 | # Indent width used for line continuations. 98 | continuation_indent_width=4 99 | 100 | # Put closing brackets on a separate line, dedented, if the bracketed 101 | # expression can't fit in a single line. Applies to all kinds of brackets, 102 | # including function definitions and calls. For example: 103 | # 104 | # config = { 105 | # 'key1': 'value1', 106 | # 'key2': 'value2', 107 | # } # <--- this bracket is dedented and on a separate line 108 | # 109 | # time_series = self.remote_client.query_entity_counters( 110 | # entity='dev3246.region1', 111 | # key='dns.query_latency_tcp', 112 | # transform=Transformation.AVERAGE(window=timedelta(seconds=60)), 113 | # start_ts=now()-timedelta(days=3), 114 | # end_ts=now(), 115 | # ) # <--- this bracket is dedented and on a separate line 116 | dedent_closing_brackets=False 117 | 118 | # Disable the heuristic which places each list element on a separate line 119 | # if the list is comma-terminated. 120 | disable_ending_comma_heuristic=False 121 | 122 | # Place each dictionary entry onto its own line. 123 | each_dict_entry_on_separate_line=True 124 | 125 | # The regex for an i18n comment. The presence of this comment stops 126 | # reformatting of that line, because the comments are required to be 127 | # next to the string they translate. 128 | i18n_comment=#\..* 129 | 130 | # The i18n function call names. The presence of this function stops 131 | # reformattting on that line, because the string it has cannot be moved 132 | # away from the i18n comment. 133 | i18n_function_call=N_, _ 134 | 135 | # Indent blank lines. 136 | indent_blank_lines=False 137 | 138 | # Indent the dictionary value if it cannot fit on the same line as the 139 | # dictionary key. For example: 140 | # 141 | # config = { 142 | # 'key1': 143 | # 'value1', 144 | # 'key2': value1 + 145 | # value2, 146 | # } 147 | indent_dictionary_value=False 148 | 149 | # The number of columns to use for indentation. 150 | indent_width=4 151 | 152 | # Join short lines into one line. E.g., single line 'if' statements. 153 | join_multiple_lines=True 154 | 155 | # Do not include spaces around selected binary operators. For example: 156 | # 157 | # 1 + 2 * 3 - 4 / 5 158 | # 159 | # will be formatted as follows when configured with "*,/": 160 | # 161 | # 1 + 2*3 - 4/5 162 | no_spaces_around_selected_binary_operators= 163 | 164 | # Use spaces around default or named assigns. 165 | spaces_around_default_or_named_assign=False 166 | 167 | # Use spaces around the power operator. 168 | spaces_around_power_operator=False 169 | 170 | # The number of spaces required before a trailing comment. 171 | # This can be a single value (representing the number of spaces 172 | # before each trailing comment) or list of values (representing 173 | # alignment column values; trailing comments within a block will 174 | # be aligned to the first column value that is greater than the maximum 175 | # line length within the block). For example: 176 | # 177 | # With spaces_before_comment=5: 178 | # 179 | # 1 + 1 # Adding values 180 | # 181 | # will be formatted as: 182 | # 183 | # 1 + 1 # Adding values <-- 5 spaces between the end of the statement and comment 184 | # 185 | # With spaces_before_comment=15, 20: 186 | # 187 | # 1 + 1 # Adding values 188 | # two + two # More adding 189 | # 190 | # longer_statement # This is a longer statement 191 | # short # This is a shorter statement 192 | # 193 | # a_very_long_statement_that_extends_beyond_the_final_column # Comment 194 | # short # This is a shorter statement 195 | # 196 | # will be formatted as: 197 | # 198 | # 1 + 1 # Adding values <-- end of line comments in block aligned to col 15 199 | # two + two # More adding 200 | # 201 | # longer_statement # This is a longer statement <-- end of line comments in block aligned to col 20 202 | # short # This is a shorter statement 203 | # 204 | # a_very_long_statement_that_extends_beyond_the_final_column # Comment <-- the end of line comments are aligned based on the line length 205 | # short # This is a shorter statement 206 | # 207 | spaces_before_comment=2 208 | 209 | # Insert a space between the ending comma and closing bracket of a list, 210 | # etc. 211 | space_between_ending_comma_and_closing_bracket=False 212 | 213 | # Split before arguments 214 | split_all_comma_separated_values=False 215 | 216 | # Split before arguments, but do not split all subexpressions recursively 217 | # (unless needed). 218 | split_all_top_level_comma_separated_values=False 219 | 220 | # Split before arguments if the argument list is terminated by a 221 | # comma. 222 | split_arguments_when_comma_terminated=False 223 | 224 | # Set to True to prefer splitting before '+', '-', '*', '/', '//', or '@' 225 | # rather than after. 226 | split_before_arithmetic_operator=False 227 | 228 | # Set to True to prefer splitting before '&', '|' or '^' rather than 229 | # after. 230 | split_before_bitwise_operator=False 231 | 232 | # Split before the closing bracket if a list or dict literal doesn't fit on 233 | # a single line. 234 | split_before_closing_bracket=True 235 | 236 | # Split before a dictionary or set generator (comp_for). For example, note 237 | # the split before the 'for': 238 | # 239 | # foo = { 240 | # variable: 'Hello world, have a nice day!' 241 | # for variable in bar if variable != 42 242 | # } 243 | split_before_dict_set_generator=False 244 | 245 | # Split before the '.' if we need to split a longer expression: 246 | # 247 | # foo = ('This is a really long string: {}, {}, {}, {}'.format(a, b, c, d)) 248 | # 249 | # would reformat to something like: 250 | # 251 | # foo = ('This is a really long string: {}, {}, {}, {}' 252 | # .format(a, b, c, d)) 253 | split_before_dot=False 254 | 255 | # Split after the opening paren which surrounds an expression if it doesn't 256 | # fit on a single line. 257 | split_before_expression_after_opening_paren=False 258 | 259 | # If an argument / parameter list is going to be split, then split before 260 | # the first argument. 261 | split_before_first_argument=False 262 | 263 | # Set to True to prefer splitting before 'and' or 'or' rather than 264 | # after. 265 | split_before_logical_operator=False 266 | 267 | # Split named assignments onto individual lines. 268 | split_before_named_assigns=True 269 | 270 | # Set to True to split list comprehensions and generators that have 271 | # non-trivial expressions and multiple clauses before each of these 272 | # clauses. For example: 273 | # 274 | # result = [ 275 | # a_long_var + 100 for a_long_var in xrange(1000) 276 | # if a_long_var % 10] 277 | # 278 | # would reformat to something like: 279 | # 280 | # result = [ 281 | # a_long_var + 100 282 | # for a_long_var in xrange(1000) 283 | # if a_long_var % 10] 284 | split_complex_comprehension=True 285 | 286 | # The penalty for splitting right after the opening bracket. 287 | split_penalty_after_opening_bracket=300 288 | 289 | # The penalty for splitting the line after a unary operator. 290 | split_penalty_after_unary_operator=10000 291 | 292 | # The penalty of splitting the line around the '+', '-', '*', '/', '//', 293 | # ``%``, and '@' operators. 294 | split_penalty_arithmetic_operator=300 295 | 296 | # The penalty for splitting right before an if expression. 297 | split_penalty_before_if_expr=0 298 | 299 | # The penalty of splitting the line around the '&', '|', and '^' 300 | # operators. 301 | split_penalty_bitwise_operator=300 302 | 303 | # The penalty for splitting a list comprehension or generator 304 | # expression. 305 | split_penalty_comprehension=2100 306 | 307 | # The penalty for characters over the column limit. 308 | split_penalty_excess_character=7000 309 | 310 | # The penalty incurred by adding a line split to the unwrapped line. The 311 | # more line splits added the higher the penalty. 312 | split_penalty_for_added_line_split=30 313 | 314 | # The penalty of splitting a list of "import as" names. For example: 315 | # 316 | # from a_very_long_or_indented_module_name_yada_yad import (long_argument_1, 317 | # long_argument_2, 318 | # long_argument_3) 319 | # 320 | # would reformat to something like: 321 | # 322 | # from a_very_long_or_indented_module_name_yada_yad import ( 323 | # long_argument_1, long_argument_2, long_argument_3) 324 | split_penalty_import_names=0 325 | 326 | # The penalty of splitting the line around the 'and' and 'or' 327 | # operators. 328 | split_penalty_logical_operator=100 329 | 330 | # Use the Tab character for indentation. 331 | use_tabs=False -------------------------------------------------------------------------------- /publisher-app/AssetModel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | // This is no more used 4 | // but served as a mockup before actaully writing any backend code 5 | 6 | 7 | ListModel { 8 | 9 | id: assetModel 10 | 11 | ListElement { 12 | name: 'env_building_010' 13 | cbPublishAsset: true 14 | assetComponents:[ 15 | 16 | ListElement { 17 | passName: 'position' 18 | path: '/mnt/vindaloo_projects/mbf2/02_seq/ep_02/mbf2_110_010/publish/mbf2_110_010_lighting/v002/p' 19 | cbPublishComponent: true 20 | startFrame: 1001 21 | endFrame: 1091 22 | }, 23 | 24 | ListElement { 25 | passName: 'z' 26 | path: '/mnt/vindaloo_projects/mbf2/02_seq/ep_02/mbf2_110_010/publish/mbf2_110_010_lighting/v002/z' 27 | cbPublishComponent: true 28 | startFrame: 1001 29 | endFrame: 1091 30 | }, 31 | 32 | ListElement { 33 | passName: 'crypto' 34 | path: '/mnt/vindaloo_projects/mbf2/02_seq/ep_02/mbf2_110_010/publish/mbf2_110_010_lighting/v002/crypto' 35 | cbPublishComponent: true 36 | startFrame: 1001 37 | endFrame: 1091 38 | }, 39 | 40 | ListElement { 41 | passName: 'normals' 42 | path: '/mnt/vindaloo_projects/mbf2/02_seq/ep_02/mbf2_110_010/publish/mbf2_110_010_lighting/v002/normals' 43 | cbPublishComponent: true 44 | startFrame: 1001 45 | endFrame: 1091 46 | }, 47 | 48 | ListElement { 49 | passName: 'id1' 50 | path: '/mnt/vindaloo_projects/mbf2/02_seq/ep_02/mbf2_110_010/publish/mbf2_110_010_lighting/v002/id1' 51 | cbPublishComponent: true 52 | startFrame: 1001 53 | endFrame: 1091 54 | } 55 | ] 56 | } 57 | 58 | ListElement { 59 | name: 'env_building_020' 60 | cbPublishAsset: true 61 | assetComponents: [ 62 | 63 | ListElement { 64 | passName: 'position' 65 | path: '/mnt/vindaloo_projects/mbf2/02_seq/ep_02/mbf2_110_010/publish/mbf2_110_010_lighting/v001/p' 66 | cbPublishComponent: true 67 | startFrame: 1001 68 | endFrame: 1022 69 | }, 70 | 71 | ListElement { 72 | passName: 'z' 73 | path: '/mnt/vindaloo_projects/mbf2/02_seq/ep_02/mbf2_110_010/publish/mbf2_110_010_lighting/v001/z' 74 | cbPublishComponent: true 75 | startFrame: 1001 76 | endFrame: 1015 77 | }, 78 | 79 | ListElement { 80 | passName: 'crypto' 81 | path: '/mnt/vindaloo_projects/mbf2/02_seq/ep_02/mbf2_110_010/publish/mbf2_110_010_lighting/v007/crypto' 82 | cbPublishComponent: true 83 | startFrame: 1001 84 | endFrame: 1077 85 | } 86 | ] 87 | } 88 | 89 | ListElement { 90 | name: 'env_building_030' 91 | cbPublishAsset: true 92 | } 93 | 94 | ListElement { 95 | name: 'env_building_040' 96 | cbPublishAsset: true 97 | } 98 | 99 | ListElement { 100 | name: 'env_building_050' 101 | cbPublishAsset: true 102 | } 103 | 104 | ListElement { 105 | name: 'env_building_060' 106 | cbPublishAsset: true 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /publisher-app/ComponentModel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | import QtQuick.Window 2.2 3 | import QtQuick.Layouts 1.3 4 | import QtQuick.Controls 2.4 5 | 6 | // This is not used 7 | // but served as a mockup before writing any backend stuff 8 | 9 | ListModel { 10 | 11 | ListElement { 12 | shouldPublish: true 13 | path: '/mnt/vindaloo_projects/mbf2' 14 | start: 1001 15 | end: 1030 16 | } 17 | 18 | ListElement { 19 | shouldPublish: true 20 | path: '/mnt/vindaloo_projects/mbf2' 21 | start: 1001 22 | end: 1030 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /publisher-app/MyCompanyButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 2.0 3 | import QtQuick.Layouts 1.3 4 | import QtGraphicalEffects 1.0 5 | 6 | Button { 7 | id: publishButton 8 | text: 'Publish' 9 | background: Rectangle { 10 | 11 | implicitWidth: 100 12 | implicitHeight: 40 13 | 14 | color: publishButton.down ? "#9ED624" : "#ABE827" 15 | radius: 4 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /publisher-app/SmallCheckBox.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.8 2 | import QtQuick.Controls 2.4 3 | 4 | CheckBox { 5 | 6 | id: passesCheckBox 7 | padding: 0 8 | 9 | indicator: Rectangle { 10 | 11 | implicitWidth: 14 12 | implicitHeight: 14 13 | x: passesCheckBox.leftPadding 14 | y: parent.height / 2 - height / 2 15 | radius: 3 16 | border.color: "#bbb" 17 | 18 | Rectangle { 19 | width: 6 20 | height: 6 21 | x: 4 22 | y: 4 23 | radius: 2 24 | color: passesCheckBox.down ? "#aaa" : "#000" 25 | visible: passesCheckBox.checked 26 | } 27 | } 28 | 29 | contentItem: Text { 30 | text: passesCheckBox.text 31 | font: passesCheckBox.font 32 | opacity: enabled ? 1.0 : 0.3 33 | color: passesCheckBox.checked ? "#000" : "#aaa" 34 | horizontalAlignment: Text.AlignHCenter 35 | verticalAlignment: Text.AlignVCenter 36 | leftPadding: passesCheckBox.indicator.width + passesCheckBox.spacing 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /publisher-app/example-data/WideFloatRange.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/WideFloatRange.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1001.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1001.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1002.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1002.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1003.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1003.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1004.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1004.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1005.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1005.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1006.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1006.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1007.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1007.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1008.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1008.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1009.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1009.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1010.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1010.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1011.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1011.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1012.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1012.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1013.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1013.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1014.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1014.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1015.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1015.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1016.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1016.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1017.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1017.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1018.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1018.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1019.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1019.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1020.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1020.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v003/beauty.1021.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v003/beauty.1021.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1001.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1001.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1002.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1002.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1003.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1003.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1004.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1004.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1005.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1005.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1006.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1006.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1007.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1007.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1008.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1008.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1009.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1009.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1010.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1010.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1011.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1011.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1012.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1012.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1013.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1013.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1014.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1014.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1015.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1015.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1016.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1016.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1017.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1017.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1018.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1018.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1019.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1019.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1020.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1020.exr -------------------------------------------------------------------------------- /publisher-app/example-data/lighting_default_v004/rgba.1021.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/example-data/lighting_default_v004/rgba.1021.exr -------------------------------------------------------------------------------- /publisher-app/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function 3 | import os 4 | import re 5 | import sys 6 | import math 7 | import time 8 | import pprint 9 | 10 | from PySide2 import QtCore as qtc 11 | from PySide2 import QtGui as qtg 12 | from PySide2 import QtQml as qml 13 | 14 | CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) 15 | 16 | # Some example regex used to filter the files 17 | ACCEPTED_EXTENSIONS_REGEX = re.compile(r'(\w+)\.(\w+)\.(exr|tiff|png|dpx)') 18 | RENDER_LAYER_REGEX = re.compile(r'\w+_\w+_v\d{3}') 19 | 20 | pp = pprint.PrettyPrinter(indent=4) 21 | 22 | 23 | def fit_range(x, inmin, inmax, outmin, outmax): 24 | """Maps a value from an interval to another 25 | 26 | Args: 27 | x (int or float): the input value 28 | inmin (int or float): The minimum of the input range 29 | inmax (int or float): The maximum of the input range 30 | outmin (int or float): The minimum of the desired range 31 | outmax (int or float): The maximum of the desired range 32 | Returns: 33 | int or float: the computed value 34 | """ 35 | # Avoid division by 0 36 | if math.floor((inmax-inmin) + outmin) == 0.0: 37 | return 0.0 38 | return (x-inmin) * (outmax-outmin) / (inmax-inmin) + outmin 39 | 40 | 41 | class PublishComponentsSignals(qtc.QObject): 42 | completed = qtc.Signal(list) 43 | error = qtc.Signal(str) 44 | progress = qtc.Signal(float) 45 | 46 | 47 | class PublishComponentsThread(qtc.QRunnable): 48 | 49 | def __init__(self, assetsdata): 50 | super(PublishComponentsThread, self).__init__() 51 | self.signals = PublishComponentsSignals() 52 | self.assets_data = assetsdata 53 | 54 | def run(self): 55 | published_assets = [] 56 | for i, asset in enumerate(self.assets_data): 57 | progress = fit_range(i, 0.0, float(len(self.assets_data) - 1), 0.15, 1.0) 58 | self.signals.progress.emit(progress) 59 | print('progress: ', progress) 60 | time.sleep(1) 61 | published_assets.append(asset['assetName']) 62 | 63 | self.signals.completed.emit(published_assets) 64 | 65 | 66 | class Backend(qtc.QObject): 67 | 68 | dataRetrieved = qtc.Signal('QVariantList') 69 | publishProgress = qtc.Signal(float) 70 | publishCompleted = qtc.Signal('QVariantList') 71 | 72 | def __init__(self): 73 | super(Backend, self).__init__() 74 | 75 | @qtc.Slot(str, 'QVariantList') 76 | def publish(self, dept, assetsdata): 77 | print('publish()') 78 | print('dept:') 79 | print(dept) 80 | print('data:') 81 | pp.pprint(assetsdata) 82 | 83 | print(fit_range(5.0, 0.0, 10.0, 0.0, 1.0)) 84 | print(fit_range(1.0, 0.0, 10.0, 0.0, 1.0)) 85 | print(fit_range(9.0, 0.0, 10.0, 0.0, 1.0)) 86 | print(fit_range(100.0, 0.0, 100.0, 0.0, 1.0)) 87 | print(fit_range(33.0, 0.0, 50.0, 0.0, 1.0)) 88 | 89 | publish_thread = PublishComponentsThread(assetsdata) 90 | publish_thread.signals.completed.connect(self.on_publish_completed) 91 | publish_thread.signals.progress.connect(self.on_publish_progress) 92 | qtc.QThreadPool.globalInstance().start(publish_thread) 93 | 94 | def on_publish_progress(self, value): 95 | self.publishProgress.emit(value) 96 | 97 | def on_publish_completed(self, assets): 98 | self.publishCompleted.emit(assets) 99 | 100 | # Use whatever business logic you want to process the list of paths 101 | @qtc.Slot('QVariant') 102 | def parseDraggedFiles(self, urllist): 103 | print('parseDraggedFile()') 104 | print(urllist) 105 | 106 | render_layers = {} 107 | 108 | for url in urllist: 109 | print(url.path()) 110 | 111 | current_asset = None 112 | 113 | for root, dirnames, filenames in os.walk(url.path()): 114 | 115 | # Workaround for a MacOS bug of os.path.basename 116 | current_root = root[:-1] if root.endswith('/') else root 117 | dir_name = os.path.basename(current_root) 118 | 119 | if RENDER_LAYER_REGEX.match(dir_name): 120 | current_asset = dir_name 121 | 122 | print('current_asset: ', current_asset) 123 | if current_asset not in render_layers and current_asset is not None: 124 | render_layers[current_asset] = {} 125 | render_layers[current_asset]['assetName'] = current_asset 126 | render_layers[current_asset]['assetIsChecked'] = True 127 | render_layers[current_asset]['assetComponents'] = [] 128 | 129 | frames = sorted( 130 | filter(lambda f: ACCEPTED_EXTENSIONS_REGEX.match(f), 131 | filenames)) 132 | 133 | if frames: 134 | first_frame_num = re.findall(r'\d+', 135 | os.path.basename(frames[0]))[0] 136 | end_frame_num = re.findall(r'\d+', 137 | os.path.basename(frames[-1]))[0] 138 | 139 | pass_name = os.path.basename(root) 140 | 141 | frame_name = frames[0].split('.')[0] 142 | frame_ext = frames[0].split('.')[-1] 143 | print('frame name: ', frame_name) 144 | padding = '#' * len(first_frame_num.split('.')[-1]) 145 | 146 | render_layers[current_asset]['assetComponents'].append({ 147 | 'passName': pass_name, 148 | 'startFrame': first_frame_num, 149 | 'endFrame': end_frame_num, 150 | 'path': '%s.%s.%s' % (frame_name, padding, frame_ext), 151 | 'passIsChecked': True 152 | }) 153 | 154 | returned_layers = [] 155 | for asset_name, asset_dict in render_layers.items(): 156 | returned_layers.append(asset_dict) 157 | 158 | # Emit a signal to communicate with the frontend 159 | self.dataRetrieved.emit( 160 | sorted(returned_layers, key=lambda e: e['assetName'])) 161 | 162 | 163 | def main(): 164 | 165 | # sys.argv += ['--style', 'material'] 166 | app = qtg.QGuiApplication(sys.argv) 167 | 168 | engine = qml.QQmlApplicationEngine() 169 | backend = Backend() 170 | engine.rootContext().setContextProperty('backend', backend) 171 | 172 | engine.load(qtc.QUrl(os.path.join(CURRENT_DIR, 'main.qml'))) 173 | 174 | if not engine.rootObjects: 175 | sys.exit(-1) 176 | 177 | sys.exit(app.exec_()) 178 | 179 | 180 | if __name__ == '__main__': 181 | main() 182 | -------------------------------------------------------------------------------- /publisher-app/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Window 2.0 3 | import QtQuick.Controls 2.0 4 | import QtQuick.Layouts 1.3 5 | import QtQuick.Dialogs 1.2 6 | 7 | ApplicationWindow { 8 | 9 | id: root 10 | visible: true 11 | 12 | width: 900 13 | minimumWidth: 700 14 | minimumHeight: 430 15 | maximumHeight: 800 16 | 17 | title: 'Publish Application' 18 | property string appVersion: 'v1.0.0' 19 | property int marginSize: 16 20 | 21 | signal reDataRetrieved(var assetsData) 22 | signal rePublishProgress(var progress) 23 | signal publishCompleted(var assetsList) 24 | 25 | Component.onCompleted: { 26 | backend.dataRetrieved.connect(reDataRetrieved) 27 | backend.publishProgress.connect(rePublishProgress) 28 | backend.publishCompleted.connect(publishCompleted) 29 | } 30 | 31 | onRePublishProgress: { 32 | console.log(`progress: ${progress}`) 33 | assetModel.remove(0) 34 | publishProgressBar.value = progress; 35 | } 36 | 37 | onPublishCompleted: { 38 | console.log('publish completed!') 39 | publishProgressBar.value = 0.0; 40 | publishProgressBar.visible = false; 41 | publishDialog.text = `Published assets: ${assetsList.join(', ')}` 42 | publishDialog.open() 43 | } 44 | 45 | onReDataRetrieved: { 46 | console.log('onReDataRetrieved') 47 | console.log(assetsData) 48 | 49 | // Populate the model with the retrieved data 50 | assetModel.clear() 51 | for (let i = 0; i < assetsData.length; i++){ 52 | let asset = assetsData[i]; 53 | if (asset.assetComponents.length > 0){ 54 | assetModel.append(asset) 55 | } 56 | } 57 | } 58 | 59 | MessageDialog { 60 | id: publishDialog 61 | visible: false 62 | icon: StandardIcon.Information 63 | title: 'Publishing complete!' 64 | modality: Qt.WindowMaximized 65 | onAccepted: {} 66 | } 67 | 68 | // Area where files can be drag 'n' dropped 69 | DropArea { 70 | width: root.width 71 | height: root.height 72 | 73 | onDropped: { 74 | console.log('user has dropped something!') 75 | if (drop.hasUrls){ 76 | // Call a slot on the backend 77 | backend.parseDraggedFiles(drop.urls) 78 | } 79 | publishProgressBar.visible = false; 80 | publishProgressBar.value = 0.0; 81 | startupText.text = '' 82 | } 83 | } 84 | 85 | Rectangle { 86 | 87 | id: mainLayout 88 | visible: true 89 | anchors.fill: parent 90 | 91 | Rectangle { 92 | 93 | id: header 94 | width: mainLayout.width 95 | height: 80 96 | 97 | Text { 98 | id: textAppPublishText 99 | anchors.left: header.left 100 | anchors.top: header.top 101 | anchors.leftMargin: 16 102 | anchors.topMargin: 16 103 | text: '' + 'MyCompany Publisher' + '' 104 | font.pointSize: 18 105 | } 106 | 107 | Text { 108 | id: versionText 109 | anchors.left: header.left 110 | anchors.top: textAppPublishText.bottom 111 | anchors.leftMargin: 16 112 | text: appVersion 113 | font.pointSize: 14 114 | } 115 | 116 | ComboBox { 117 | id: deptComboBox 118 | anchors.top: header.top 119 | anchors.right: header.right 120 | anchors.rightMargin: 16 121 | anchors.topMargin: 16 122 | model: ['Lighting', 'Compositing'] 123 | } 124 | 125 | Text { 126 | id: startupText 127 | width: root.width 128 | height: root.height 129 | anchors.top: textAppPublishText.bottom 130 | anchors.topMargin: 32 131 | anchors.left: textAppPublishText.left 132 | anchors.leftMargin: 16 133 | text: "To start, drag a frame sequence (eg: the one in 'example-data')..." 134 | } 135 | } 136 | 137 | ListView { 138 | 139 | id: assetsListView 140 | visible: true 141 | focus: true 142 | anchors.top: header.bottom 143 | anchors.bottom: finalRowLayout.top 144 | anchors.bottomMargin: marginSize / 2 145 | clip: true 146 | width: parent.width 147 | height: 300 148 | 149 | 150 | model: ListModel { 151 | id: assetModel 152 | } 153 | 154 | // How do we want to render our listview 155 | delegate: ColumnLayout { 156 | 157 | width: root.width 158 | 159 | RowLayout { 160 | 161 | ButtonGroup { 162 | id: passesButtonGroup 163 | exclusive: false 164 | checkState: assetCheckBox.checkState 165 | } 166 | 167 | CheckBox { 168 | id: assetCheckBox 169 | checked: assetIsChecked 170 | checkState: passesButtonGroup.checkState 171 | onClicked: { 172 | console.log('checked group: ' + checked) 173 | assetIsChecked = checked 174 | } 175 | } 176 | 177 | Label { 178 | text: assetName 179 | font.pointSize: 14 180 | font.bold: true 181 | } 182 | } 183 | 184 | Repeater { 185 | model: assetComponents 186 | delegate: RowLayout { 187 | 188 | Item { 189 | width: marginSize 190 | } 191 | 192 | SmallCheckBox { 193 | checked: passIsChecked 194 | ButtonGroup.group: passesButtonGroup 195 | onClicked: { 196 | passIsChecked = checked 197 | } 198 | } 199 | 200 | Label { 201 | text: passName 202 | font.italic: true 203 | } 204 | 205 | TextEdit { 206 | Layout.fillWidth: true 207 | text: path 208 | readOnly: true 209 | selectByMouse: true 210 | selectionColor: 'green' 211 | 212 | } 213 | 214 | Label { 215 | text: startFrame 216 | } 217 | 218 | Label { 219 | text: '-' 220 | } 221 | 222 | Label { 223 | text: endFrame 224 | } 225 | 226 | Item { 227 | width: marginSize 228 | } 229 | } 230 | } 231 | 232 | Item { 233 | height: marginSize 234 | } 235 | } 236 | 237 | ScrollBar.vertical: ScrollBar {} 238 | } 239 | 240 | RowLayout { 241 | 242 | id: finalRowLayout 243 | anchors.bottom: mainLayout.bottom 244 | anchors.bottomMargin: marginSize / 2 245 | anchors.right: mainLayout.right 246 | anchors.rightMargin: 8 247 | anchors.left: mainLayout.left 248 | 249 | Item { 250 | width: marginSize 251 | } 252 | 253 | ProgressBar { 254 | id: publishProgressBar 255 | visible: false 256 | Layout.alignment: Qt.AlignLeft 257 | Layout.fillWidth: true 258 | value: 0.0 259 | 260 | background: Rectangle { 261 | implicitWidth: 200 262 | implicitHeight: 6 263 | color: "#e6e6e6" 264 | radius: 3 265 | } 266 | 267 | contentItem: Item { 268 | implicitWidth: 200 269 | implicitHeight: 4 270 | 271 | Rectangle { 272 | width: publishProgressBar.value * parent.width 273 | height: parent.height 274 | radius: 2 275 | color: "#17a81a" 276 | } 277 | } 278 | } 279 | 280 | Item { 281 | id: fillerItem 282 | visible: true 283 | Layout.fillWidth: true 284 | Layout.alignment: Qt.AlignLeft 285 | } 286 | 287 | Item { 288 | width: marginSize 289 | } 290 | 291 | MyCompanyButton { 292 | id: publishButton 293 | text: 'Publish' 294 | onClicked: { 295 | 296 | // We create a JSON list of only the items 297 | // that have been checkend and we pass back to python 298 | // by calling a slot on the backend 299 | let publishData = []; 300 | 301 | if (assetModel.count === 0){ 302 | console.log('Nothing to publish..') 303 | return 304 | } 305 | console.log('Publishing..') 306 | 307 | for (let i = 0; i < assetModel.count; i++){ 308 | let assetName = assetModel.get(i).assetName; 309 | let assetIsChecked = assetModel.get(i).assetIsChecked; 310 | 311 | console.log(`current asset: ${assetName}`); 312 | if (!assetIsChecked){ 313 | console.log('asset is not checked, skipping..'); 314 | continue; 315 | } 316 | 317 | let assetData = { 318 | assetName: assetName 319 | }; 320 | let assetComponents = assetModel.get(i).assetComponents; 321 | 322 | for (let j = 0; j < assetComponents.count; j++){ 323 | let passComponent = assetComponents.get(j); 324 | console.log(`\t ${passComponent.passName} ${passComponent.passIsChecked} ${passComponent.path}`); 325 | 326 | if (!passComponent.passIsChecked){ 327 | console.log('pass component is not checked, skipping..'); 328 | continue; 329 | } 330 | 331 | assetData[passComponent.passName] = { 332 | path: passComponent.path 333 | } 334 | } 335 | 336 | publishData.push(assetData) 337 | } 338 | let currentDept = deptComboBox.currentText; 339 | backend.publish(currentDept, publishData) 340 | 341 | publishProgressBar.visible = true; 342 | // fillerItem.visible = false 343 | } 344 | } 345 | } 346 | } 347 | } 348 | 349 | -------------------------------------------------------------------------------- /publisher-app/publisher-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvzen/pyside2-qml-examples/8ce8a737dc531b2cd61c3456a9c0f27ecacbd014/publisher-app/publisher-app.png --------------------------------------------------------------------------------