├── .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 | [](https://github.com/vvzen/pyside2-qml-examples/blob/main/LICENSE)
4 | 
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 | 
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 | 
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 | 
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 | 
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
--------------------------------------------------------------------------------