├── .coveragerc ├── .gitignore ├── .travis.yml ├── CHANGES.rst ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── appveyor.yml ├── ci ├── appveyor-install-deps.cmd ├── appveyor-run-tests.cmd └── travis_ci_bootstrap.sh ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ ├── et.ico │ └── logo.png │ ├── conf.py │ ├── index.rst │ └── intro.rst ├── examples ├── __init__.py ├── ex10_list_of_primitives.py ├── ex11_method_call.py ├── ex12_method_call_slow.py ├── ex13_success_error_callbacks.html ├── ex13_success_error_callbacks.py ├── ex14_event_trait.py ├── ex15_embedding_in_qt.py ├── ex16_template_person.html ├── ex16_templating.js ├── ex16_templating.py ├── ex17_custom_angular_application.html ├── ex17_custom_angular_application.py ├── ex18_embedding_chaco.py ├── ex19_embedding_mayavi.py ├── ex1_simple_view.py ├── ex20_mayavi_webgl_demo.html ├── ex20_mayavi_webgl_demo.py ├── ex21_matplotlib_web.py ├── ex22_vuejs_demo.html ├── ex22_vuejs_demo.py ├── ex23_vuejs_web_demo.py ├── ex2_simple_view_full.html ├── ex2_simple_view_full_html.py ├── ex3_simple_view_web.py ├── ex4_simple_view_sans_traits.py ├── ex5_data │ ├── Lena.png │ ├── color_change.js │ └── colors.css ├── ex5_user_resources.py ├── ex6_model_updates.py ├── ex7_model_updates_web.py ├── ex8_instance_trait.py ├── ex9_list_of_instances.py ├── examples_notebook.ipynb └── xtk.js ├── jigna ├── __init__.py ├── api.py ├── core │ ├── __init__.py │ ├── concurrent.py │ ├── interoperation.py │ ├── network_access.py │ ├── proxy_qwebview.py │ └── wsgi.py ├── html_widget.py ├── js │ ├── build.js │ ├── dist │ │ ├── jigna-vue.js │ │ └── jigna.js │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── async_client.js │ │ │ ├── async_proxy_factory.js │ │ │ ├── client.js │ │ │ ├── event_target.js │ │ │ ├── jigna-angular.js │ │ │ ├── jigna-vue.js │ │ │ ├── jigna.js │ │ │ ├── list_proxy.js │ │ │ ├── proxy.js │ │ │ ├── proxy_factory.js │ │ │ ├── qt_bridge.js │ │ │ ├── subarray.js │ │ │ └── web_bridge.js │ │ └── external │ │ │ ├── angular.js │ │ │ ├── jquery.min.js │ │ │ └── vue.js │ └── watcher.js ├── qt.py ├── qt_server.py ├── server.py ├── template.py ├── tests │ ├── __init__.py │ ├── test_html_widget.py │ ├── test_jigna_qt.py │ ├── test_jigna_vue_qt.py │ ├── test_jigna_vue_web.py │ ├── test_jigna_web.py │ ├── test_jigna_web_async.py │ └── test_web_server.py ├── utils │ ├── __init__.py │ ├── gui.py │ ├── notebook.py │ └── web.py ├── vue_template.py ├── web_app.py └── web_server.py ├── requirements.txt └── setup.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = jigna 4 | omit = */tests/* 5 | 6 | [report] 7 | # Regexes for lines to exclude from consideration 8 | exclude_lines = 9 | # Have to re-enable the standard pragma 10 | pragma: no cover 11 | 12 | # Don't complain about missing debug-only code: 13 | def __repr__ 14 | 15 | # Don't complain if tests don't hit defensive assertion code: 16 | raise AssertionError 17 | raise NotImplementedError 18 | 19 | if __name__ == .__main__.: 20 | 21 | ignore_errors = True 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | jigna.egg-info/ 2 | 3 | *.py[cod] 4 | 5 | *.DS_Store 6 | 7 | .idea/ 8 | 9 | jigna/js/node_modules/ 10 | jigna/js/npm-debug.log 11 | 12 | docs/build/ 13 | build/ 14 | dist/ 15 | examples/.ipynb_checkpoints/ 16 | 17 | *.sublime-workspace 18 | *.sublime-project 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | sudo: false 3 | os: 4 | - linux 5 | - osx 6 | env: 7 | global: 8 | - PATH=$PATH:~/bin 9 | matrix: 10 | - RUNTIME=2.7 QT_APIS="pyside pyqt" 11 | - RUNTIME=3.5 QT_APIS=pyqt 12 | addons: 13 | firefox: "latest" 14 | cache: 15 | directories: 16 | - $HOME/.cache 17 | before_install: 18 | - export DISPLAY=:99.0 19 | - if [[ ${TRAVIS_OS_NAME} == "linux" ]] ; then sh -e /etc/init.d/xvfb start ; fi 20 | install: 21 | - ./ci/travis_ci_bootstrap.sh 22 | - edm install --version $RUNTIME -y pip traits tornado nose coverage mock 23 | - edm run -- python -V # Check the default Python version. 24 | - edm run -- pip install -U selenium # Need latest selenium for browser support 25 | - for QT_API in $QT_APIS; do edm install -y $QT_API; done 26 | - edm run -- pip install -e . 27 | script: 28 | - for QT_API in $QT_APIS; do QT_API=$QT_API edm run -- coverage run -m nose -v jigna; done 29 | after_success: 30 | - edm run -- coverage combine 31 | - edm run -- coverage report 32 | - edm run -- pip install codecov 33 | - edm run -- codecov 34 | notifications: 35 | email: false 36 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | 0.10.1 2 | ------ 3 | 4 | * Fix a bug with methods not being exposed in Python-3.x. 5 | * Fix and improve web-UI examples to run without Qt. 6 | * Improve test stability a little. 7 | 8 | 0.10.0 9 | ------- 10 | 11 | * Python3 support. 12 | * Jigna is now tested on Linux, OS X and Windows. 13 | * Fix problem with ``jigna.threaded`` calls. 14 | * Work with PyQt and PySide -- currently Qt4 only though. 15 | * Use a better version of guess_type to guess file mime type. 16 | * Add full async support for web UIs. The sync backend is the default. 17 | * Documentation and example cleanup. 18 | * Update documentation, README, setup.py. 19 | * Make available on PyPI. 20 | 21 | 22 | 0.9.9 23 | ------ 24 | 25 | These changes are for all previous releases. 26 | 27 | * Add support for Vue.js HTML templates. 28 | * Avoid Qt imports for pure web-based UI access. 29 | * Fix broken page reloads. 30 | * Fix issues with serving files having spaces. 31 | * Fix loading of jigna.js on Windows. 32 | 33 | 0.9.1 34 | ----- 35 | 36 | * First public release. 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This software is OSI Certified Open Source Software. 2 | OSI Certified is a certification mark of the Open Source Initiative. 3 | 4 | Copyright (c) 2013-2017, Enthought, Inc. 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | * Neither the name of Enthought, Inc. nor the names of its contributors may 16 | be used to endorse or promote products derived from this software without 17 | specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include examples *.py *.ipynb *.js *.css *.html *.png 2 | recursive-include docs * 3 | recursive-include jigna/js *.* 4 | recursive-exclude jigna/js/node_modules *.* 5 | recursive-exclude examples/.ipynb_checkpoints *.* 6 | include README.rst LICENSE.txt 7 | global-exclude *.pyc *.pyo .DS_Store 8 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Jigna 2 | ===== 3 | 4 | .. image:: https://travis-ci.org/enthought/jigna.svg?branch=master 5 | :target: https://travis-ci.org/enthought/jigna 6 | :alt: Travis-CI build status 7 | 8 | .. image:: https://ci.appveyor.com/api/projects/status/71v3yq4becmwj6fk?svg=true 9 | :target: https://ci.appveyor.com/project/Enthought/jigna 10 | :alt: Appveyor build status 11 | 12 | .. image:: https://codecov.io/gh/enthought/jigna/branch/master/graph/badge.svg 13 | :target: https://codecov.io/gh/enthought/jigna 14 | :alt: Codecov status 15 | 16 | .. image:: https://readthedocs.org/projects/jigna/badge/?version=latest 17 | :target: http://jigna.readthedocs.io/en/latest/?badge=latest 18 | :alt: Documentation Status 19 | 20 | 21 | Jigna is a bridge between Traits_ and the HTML world to provide a UI toolkit for 22 | traits models based on the powerful technologies of HTML, CSS and Javascript. 23 | 24 | It provides two-way data bindings between the traits model and the HTML 25 | interface i.e. traits notifications automatically update the HTML UI and user 26 | inputs on the UI seamlessly change model attributes in Python. Jigna uses 27 | AngularJS_ (or `Vue.js`_) for data bindings and one could think of it as an 28 | equivalent of AngularJS where the model lives in Python instead of Javascript. 29 | This gives us the power of Python and Traits for easily writing the application 30 | logic and the flexibility and expressibility of HTML for the user interface. 31 | 32 | The HTML UI can be displayed both in the same Python process as well as 33 | remotely, allowing us to view and manipulate a Traits-based-object from the web. 34 | 35 | Jigna can be used to create user interfaces in Qt using Qt's webkit support or 36 | on a browser. One needs to have Qt (PySide or PyQt) installed if one wants a Qt 37 | based UI and requires tornado_ for a web-based UI. 38 | 39 | With a web-based UI, the jigna dependencies are rather light requiring only 40 | traits, tornado, and a recent web browser. 41 | 42 | Please note that Jigna currently is only tested with Qt4 and Qt5 support will be 43 | added later. 44 | 45 | 46 | .. _Traits: http://code.enthought.com/projects/traits/ 47 | .. _tornado: http://tornadoweb.org 48 | .. _AngularJS: http://angularjs.org 49 | .. _`Vue.js`: http://vuejs.org 50 | 51 | 52 | Installation 53 | ============ 54 | 55 | Jigna can be installed with ``pip``:: 56 | 57 | $ pip install jigna 58 | 59 | This will not pull in any Qt requirements but will require tornado_ as it is 60 | easy to install. The test requirements can be installed via:: 61 | 62 | $ pip install jigna[test] 63 | 64 | This will install, PySide, nose, mock, coverage, and selenium_ if you do not 65 | already have them. 66 | 67 | To install Jigna from the sources you may do the following:: 68 | 69 | git clone git@github.com:enthought/jigna.git 70 | cd jigna 71 | pip install -r requirements.txt 72 | python setup.py install # or python setup.py develop 73 | 74 | This does not include pyside or pyqt so you will have to install that as well. 75 | 76 | .. _selenium: https://pypi.python.org/pypi/selenium 77 | 78 | 79 | Testing 80 | ======= 81 | 82 | To run the tests, you can simply do the following from the project's root 83 | directory:: 84 | 85 | nosetests 86 | 87 | You must make sure you have the required packages installed, and can install 88 | these via:: 89 | 90 | pip install -r requirements.txt 91 | 92 | This does not install pyside since that is a heavy dependency. You may install 93 | it using your package manager or via pip. 94 | 95 | Examples 96 | ======== 97 | 98 | There are several examples to play with in the ``examples`` directory. Each 99 | example demonstrates one particular feature of jigna. Start with the simplest 100 | one by running:: 101 | 102 | python ex1_simple_view.py 103 | 104 | This requires Qt so if you are only interested in Web UI examples, you may run 105 | the following examples: 106 | 107 | - ``ex3_simple_view_web.py`` 108 | - ``ex7_model_updates_web.py`` 109 | - ``ex20_mayavi_webgl_demo.py`` -- this requires Mayavi to be installed. 110 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | build: false 2 | shallow_clone: true 3 | 4 | environment: 5 | 6 | global: 7 | distutils_use_sdk: 1 8 | ets_toolkit: "qt4" 9 | 10 | matrix: 11 | - python: "C:/Python27-x64" 12 | sdkver: "v7.0" 13 | 14 | cache: 15 | - c:\temp 16 | 17 | init: 18 | - ps : $Env:sdkbin = "C:\Program Files\Microsoft SDKs\Windows" + $Env:sdkver + "\Bin" 19 | - ps : $Env:sdkverpath = "C:/Program Files/Microsoft SDKs/Windows" + $Env:sdkver + "/Setup/WindowsSdkVer.exe" 20 | - ps : $Env:path = $Env:python + ";" + $Env:python + "\scripts;" + $Env:sdkbin + ";" + $Env:path 21 | 22 | install: 23 | - ps: if ((Test-Path "c:/temp") -eq 0) {mkdir c:/temp} 24 | - ps: python -m pip install --upgrade --no-binary wheel pip 25 | - ps: pip install --upgrade wheel 26 | - ps: pip --version 27 | 28 | - cmd /v:on /e:on /c ".\ci\appveyor-install-deps.cmd" 29 | 30 | test_script: 31 | - cmd /v:on /e:on /c ".\ci\appveyor-run-tests.cmd" 32 | -------------------------------------------------------------------------------- /ci/appveyor-install-deps.cmd: -------------------------------------------------------------------------------- 1 | "%sdkverpath%" -q -version:"%sdkver%" 2 | call setenv /x64 3 | 4 | rem install python packages 5 | pip install --cache-dir c:/temp traits 6 | pip install --cache-dir c:/temp pyside 7 | pip install --cache-dir c:/temp tornado 8 | pip install --cache-dir c:/temp nose 9 | pip install --cache-dir c:/temp selenium 10 | pip install --cache-dir c:/temp coverage 11 | pip install --cache-dir c:/temp mock 12 | 13 | rem install jigna 14 | python setup.py develop 15 | -------------------------------------------------------------------------------- /ci/appveyor-run-tests.cmd: -------------------------------------------------------------------------------- 1 | "%sdkverpath%" -q -version:"%sdkver%" 2 | call setenv /x64 3 | 4 | mkdir testrun 5 | copy .coveragerc testrun 6 | cd testrun 7 | 8 | coverage run -m nose.core -v jigna 9 | if %errorlevel% neq 0 exit /b %errorlevel% 10 | coverage report 11 | -------------------------------------------------------------------------------- /ci/travis_ci_bootstrap.sh: -------------------------------------------------------------------------------- 1 | # Install edm and geckodriver for selenium firefox 2 | 3 | EDM_VERSION=1.5.0 4 | EDM_VERSION_MAJOR=1.5 5 | cd $HOME 6 | if [[ ${TRAVIS_OS_NAME} == "osx" ]] 7 | then 8 | # download and install EDM 9 | FILENAME=edm_${EDM_VERSION}.pkg 10 | wget https://package-data.enthought.com/edm/osx_x86_64/${EDM_VERSION_MAJOR}/$FILENAME 11 | sudo installer -pkg $FILENAME -target / 12 | wget https://github.com/mozilla/geckodriver/releases/download/v0.15.0/geckodriver-v0.15.0-macos.tar.gz 13 | else 14 | # download and install EDM 15 | FILENAME=edm_${EDM_VERSION}_linux_x86_64.sh 16 | wget https://package-data.enthought.com/edm/rh5_x86_64/${EDM_VERSION_MAJOR}/$FILENAME 17 | chmod u+x $FILENAME 18 | ./$FILENAME -b -p ~ 19 | wget https://github.com/mozilla/geckodriver/releases/download/v0.15.0/geckodriver-v0.15.0-linux64.tar.gz 20 | fi 21 | 22 | mkdir -p ~/bin 23 | cd ~/bin 24 | tar xvf ~/geckodriver-*.tar.gz 25 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Jigna.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Jigna.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Jigna" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Jigna" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Jigna.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Jigna.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/source/_static/et.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enthought/jigna/00192450515099d8abc74866ffe1e4f743fe2279/docs/source/_static/et.ico -------------------------------------------------------------------------------- /docs/source/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enthought/jigna/00192450515099d8abc74866ffe1e4f743fe2279/docs/source/_static/logo.png -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Jigna documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Feb 9 19:23:03 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = 'Jigna' 44 | copyright = '2014-2017, Jigna developers' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | d = {} 51 | fname = os.path.join(os.pardir, os.pardir, 'jigna', '__init__.py') 52 | exec(compile(open(fname).read(), fname, 'exec'), d) 53 | version = release = d['__version__'] 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = [] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'sphinx_rtd_theme' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = { 100 | # 'navbar_fixed_top': "false", 101 | #} 102 | 103 | # Add any paths that contain custom themes here, relative to this directory. 104 | html_theme_path = ['themes'] 105 | 106 | # The name for this set of Sphinx documents. If None, it defaults to 107 | # " v documentation". 108 | #html_title = None 109 | 110 | # A shorter title for the navigation bar. Default is the same as html_title. 111 | #html_short_title = None 112 | 113 | # The name of an image file (relative to this directory) to place at the top 114 | # of the sidebar. 115 | html_logo = '_static/logo.png' 116 | 117 | # The name of an image file (within the static path) to use as favicon of the 118 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 119 | # pixels large. 120 | html_favicon = '_static/et.ico' 121 | 122 | # Add any paths that contain custom static files (such as style sheets) here, 123 | # relative to this directory. They are copied after the builtin static files, 124 | # so a file named "default.css" will overwrite the builtin "default.css". 125 | html_static_path = ['_static'] 126 | 127 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 128 | # using the given strftime format. 129 | #html_last_updated_fmt = '%b %d, %Y' 130 | 131 | # If true, SmartyPants will be used to convert quotes and dashes to 132 | # typographically correct entities. 133 | #html_use_smartypants = True 134 | 135 | # Custom sidebar templates, maps document names to template names. 136 | #html_sidebars = {} 137 | 138 | # Additional templates that should be rendered to pages, maps page names to 139 | # template names. 140 | #html_additional_pages = {} 141 | 142 | # If false, no module index is generated. 143 | #html_domain_indices = True 144 | 145 | # If false, no index is generated. 146 | #html_use_index = True 147 | 148 | # If true, the index is split into individual pages for each letter. 149 | #html_split_index = False 150 | 151 | # If true, links to the reST sources are added to the pages. 152 | #html_show_sourcelink = True 153 | 154 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 155 | #html_show_sphinx = True 156 | 157 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 158 | #html_show_copyright = True 159 | 160 | # If true, an OpenSearch description file will be output, and all pages will 161 | # contain a tag referring to it. The value of this option must be the 162 | # base URL from which the finished HTML is served. 163 | #html_use_opensearch = '' 164 | 165 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 166 | #html_file_suffix = None 167 | 168 | # Output file base name for HTML help builder. 169 | htmlhelp_basename = 'Jignadoc' 170 | 171 | 172 | # -- Options for LaTeX output -------------------------------------------------- 173 | 174 | latex_elements = { 175 | # The paper size ('letterpaper' or 'a4paper'). 176 | #'papersize': 'letterpaper', 177 | 178 | # The font size ('10pt', '11pt' or '12pt'). 179 | #'pointsize': '10pt', 180 | 181 | # Additional stuff for the LaTeX preamble. 182 | #'preamble': '', 183 | } 184 | 185 | # Grouping the document tree into LaTeX files. List of tuples 186 | # (source start file, target name, title, author, documentclass [howto/manual]). 187 | latex_documents = [ 188 | ('index', 'Jigna.tex', 'Jigna Documentation', 189 | 'Jigna developers', 'manual'), 190 | ] 191 | 192 | # The name of an image file (relative to this directory) to place at the top of 193 | # the title page. 194 | #latex_logo = None 195 | 196 | # For "manual" documents, if this is true, then toplevel headings are parts, 197 | # not chapters. 198 | #latex_use_parts = False 199 | 200 | # If true, show page references after internal links. 201 | #latex_show_pagerefs = False 202 | 203 | # If true, show URL addresses after external links. 204 | #latex_show_urls = False 205 | 206 | # Documents to append as an appendix to all manuals. 207 | #latex_appendices = [] 208 | 209 | # If false, no module index is generated. 210 | #latex_domain_indices = True 211 | 212 | 213 | # -- Options for manual page output -------------------------------------------- 214 | 215 | # One entry per manual page. List of tuples 216 | # (source start file, name, description, authors, manual section). 217 | man_pages = [ 218 | ('index', 'jigna', 'Jigna Documentation', 219 | ['Jigna developers'], 1) 220 | ] 221 | 222 | # If true, show URL addresses after external links. 223 | #man_show_urls = False 224 | 225 | 226 | # -- Options for Texinfo output ------------------------------------------------ 227 | 228 | # Grouping the document tree into Texinfo files. List of tuples 229 | # (source start file, target name, title, author, 230 | # dir menu entry, description, category) 231 | texinfo_documents = [ 232 | ('index', 'Jigna', 'Jigna Documentation', 233 | 'Jigna developers', 'Jigna', 'One line description of project.', 234 | 'Miscellaneous'), 235 | ] 236 | 237 | # Documents to append as an appendix to all manuals. 238 | #texinfo_appendices = [] 239 | 240 | # If false, no module index is generated. 241 | #texinfo_domain_indices = True 242 | 243 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 244 | #texinfo_show_urls = 'footnote' 245 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Jigna documentation master file, created by 2 | sphinx-quickstart on Sun Feb 9 19:23:03 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to the Jigna documentation! 7 | =================================== 8 | 9 | Jigna allows you to create a HTML/CSS/Javascript based user interface for a 10 | Python application. The UI can be viewed either in a Qt-based application or 11 | entirely on a web-browser. 12 | 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | 17 | intro 18 | 19 | 20 | 21 | 22 | Indices and tables 23 | ================== 24 | 25 | * :ref:`genindex` 26 | * :ref:`modindex` 27 | * :ref:`search` 28 | -------------------------------------------------------------------------------- /docs/source/intro.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============= 3 | 4 | Jigna aims to provide an HTML based solution for creating beautiful user 5 | interfaces for Python applications, as opposed to widget based toolkits like 6 | Qt/wx or native toolkits. It provides a seamless two-way data binding between 7 | the Python model and the HTML view by creating a Python-JS communication 8 | bridge. This ensures that the view is always live as it can automatically 9 | update itself when the model changes, and update the model when user actions 10 | take place on the UI. The Jigna view can be rendered in an in-process Qt 11 | widget or over the web in a browser. 12 | 13 | The primary use of jigna is to provide a way to write desktop GUI applications 14 | using HTML, and we use the Qt backend for that. You can also use jigna to write 15 | GUIs that run on the browser. This is done via the web backend. 16 | 17 | 18 | Installation 19 | ============ 20 | 21 | Jigna works with Python 2.x and Python 3.x (although for 3.x, you have to install 22 | the dependencies yourself). 23 | 24 | The setup procedure is simple. Make sure you have a Python installation to 25 | which you have write access and run the following command: 26 | 27 | .. code:: bash 28 | 29 | $ pip install jigna 30 | 31 | This will not pull in any Qt requirements but will require tornado_ as it is 32 | easy to install. The test requirements can be installed via:: 33 | 34 | $ pip install jigna[test] 35 | 36 | This will install, PySide, nose, mock, coverage, and selenium_ if you do not 37 | already have them. 38 | 39 | You may also use the ``requirements.txt`` file to install the necessary 40 | dependencies. This does not install PyQt or PySide though. 41 | 42 | 43 | Examples 44 | ======== 45 | 46 | There are several examples to get started, look in the ``examples/`` directory 47 | and run them. 48 | 49 | Tests 50 | ===== 51 | 52 | Running the tests requires selenium_ and nose_. The tests are in 53 | ``jigna/tests/``. You may run them from the root directory of the sources 54 | using ``nosetests``. 55 | 56 | .. _selenium: https://pypi.python.org/pypi/selenium 57 | .. _nose: https://pypi.python.org/pypi/nose 58 | .. _Traits: http://code.enthought.com/projects/traits/ 59 | .. _tornado: http://tornadoweb.org 60 | 61 | Getting Started 62 | =============== 63 | 64 | Let us say we have a nice application model written in Python (specifically, 65 | in Traits_):: 66 | 67 | from traits.api import HasTraits, Str 68 | 69 | class Person(HasTraits): 70 | name = Str 71 | greeting = Str 72 | def _name_changed(self): 73 | self.greeting = "Hello " + self.name 74 | 75 | person = Person(name='Fred') 76 | 77 | Jigna lets you visualize this model using HTML such that the model and the 78 | view are fully connected. Here is a sample HTML (an AngularJS_ template):: 79 | 80 | body_html = """ 81 | Name:
82 | Greeting: 83 |

{{person.greeting}}

84 | """ 85 | 86 | Notice how the HTML is directly referencing model attributes via ``person.name`` 87 | and ``person.greeting``. We now bind this declarative view to the model and 88 | create a Qt based UI:: 89 | 90 | from jigna.api import HTMLWidget, Template 91 | template = Template(body_html=body_html) 92 | 93 | from jigna.qt import QtGui 94 | app = QtGui.QApplication([]) 95 | widget = HTMLWidget(template=template, context={'person': person}) 96 | widget.show() 97 | app.exec_() 98 | 99 | This produces a Qt window containing an HTML UI which responds automatically to 100 | any changes in the model and vice-versa. It can optionally be styled with CSS 101 | and made interactive with Javascript. This provides a nice way of easily 102 | building rich, live user interfaces for Python apps. Note that the Qt imports 103 | were made using the ``jigna.qt`` module. This module allows one to switch 104 | between PySide and PyQt easily by exporting an environment variable ``QT_API``, 105 | if this is set to ``pyside`` then ``PySide`` will be used and if it is set to 106 | ``pyqt`` then ``PyQt`` will be used. 107 | 108 | This is nice for several reasons: 109 | 110 | * The view code is declarative and hence easy to read. 111 | 112 | * The binding between the model and the view is automatic. 113 | 114 | * HTML/CSS/JS today is very powerful 115 | 116 | - there are many JS libraries for a variety of tasks 117 | 118 | - your development team doesn't have to worry about creating widgets or the 119 | limitations in the toolkit’s widget set as there are thousands of 120 | developers worldwide creating awesome CSS/JS widgets for you. 121 | 122 | * Much easier to find people who know HTML/CSS/JS than Qt or a native toolkit. 123 | 124 | * There is a complete separation of view from the model and this allows us to 125 | hand off the entire UI to an HTML/CSS/JS guru. 126 | 127 | And since this is HTML, the jigna ``Template`` can also be easily served 128 | on a web browser if you don't have Qt installed. The wiring changes a bit for 129 | this, so the final example looks like this:: 130 | 131 | from tornado.ioloop import IOLoop 132 | from jigna.web_app import WebApp 133 | 134 | ioloop = IOLoop.instance() 135 | 136 | app = WebApp(template=template, context={'person': person}) 137 | app.listen(8000) 138 | 139 | # Serving the app on http://localhost:8000/. 140 | ioloop.start() 141 | 142 | This starts up a web server to which one can connect multiple browsers to see 143 | and interact with the model. 144 | 145 | Please note that any imports from ``jigna.api`` will try and import Qt modules 146 | if they are installed, so if you need an application with a pure web-based UI 147 | and wish to avoid those imports, you may wish to use the following imports:: 148 | 149 | from jigna.template import Template 150 | from jigna.web_app import WebApp 151 | 152 | The above example was very simple as the purpose of that was to show how 153 | jigna is wired together. For further use cases of jigna, please refer to the 154 | examples in the ``examples`` directory. They are numbered and are meant to act 155 | as a tutorial if followed in sequence. 156 | 157 | .. _AngularJS: http://angularjs.org/ 158 | 159 | 160 | One may also use `Vue.js`_ to build the HTML UI. The approach is very similar to 161 | that using AngularJS. A Vue template is available from:: 162 | 163 | from jigna.vue_template import VueTemplate 164 | 165 | An example showing how to use this with a Qt app is in 166 | `examples/ex22_vuejs_demo.py 167 | `_ 168 | with the corresponding HTML file in `examples/ex22_vuejs_demo.html 169 | `_. 170 | 171 | A similar example showing how to use this with a pure web-based app is in 172 | `examples/ex23_vuejs_demo.py 173 | `_. 174 | The HTML file is the same. 175 | 176 | 177 | .. _`Vue.js`: http://vuejs.org 178 | 179 | 180 | Changelog 181 | ========== 182 | 183 | .. include:: ../../CHANGES.rst 184 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enthought/jigna/00192450515099d8abc74866ffe1e4f743fe2279/examples/__init__.py -------------------------------------------------------------------------------- /examples/ex10_list_of_primitives.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows two-way data binding for the `List` traits whose items are of 3 | the primitive type. 4 | """ 5 | 6 | #### Imports #### 7 | from __future__ import print_function 8 | 9 | from traits.api import HasTraits, Str, List 10 | from jigna.api import HTMLWidget, Template 11 | from jigna.qt import QtGui 12 | from jigna.utils.gui import do_after 13 | 14 | #### Domain model #### 15 | 16 | class Basket(HasTraits): 17 | fruits = List(Str) 18 | 19 | #### UI layer #### 20 | 21 | # Looping over the list of primitive variables using `ng-repeat` has two gotchas 22 | # (both of them are standard AngularJS gotchas): 23 | # 24 | # 1. We need to add `track by $index` for looping through primitive variables 25 | # otherwise angular creates new DOM nodes for every edit of the variable. One 26 | # manifestation of this behaviour is that the input field gets out of focus for 27 | # every edit since each edit really creates a new input field node. 28 | # See http://www.bennadel.com/blog/2556-using-track-by-with-ngrepeat-in-angularjs-1-2.htm 29 | # 30 | # 2. We can't write to the `fruit` variable by making a local assignment i.e. 31 | # `ng-model="fruit"`. This is because `fruit` refers to a local scope variable, 32 | # so any writes to this local variable won't update the model values in Python 33 | # which are bound to the $rootScope. 34 | # See http://stackoverflow.com/questions/15488342/binding-inputs-to-an-array-of-primitives-using-ngrepeat-uneditable-inputs 35 | 36 | body_html = """ 37 |
38 | Fruits in the basket: 39 |
40 | 41 |
42 |
43 | """ 44 | 45 | template = Template(body_html=body_html) 46 | 47 | #### Entry point #### 48 | 49 | def main(): 50 | # Start the Qt application 51 | app = QtGui.QApplication([]) 52 | 53 | # Instantiate the domain model 54 | basket = Basket(fruits=['peach', 'pear']) 55 | 56 | # Create the jigna based HTML widget which renders the given HTML template 57 | # with the given context. 58 | widget = HTMLWidget(template=template, context={'basket':basket}) 59 | widget.show() 60 | 61 | # Schedule some operations on the list. 62 | # 63 | # We're trying to append and insert primitives to the list in the future. 64 | # This should be reflected in the UI. 65 | do_after(2500, basket.fruits.append, 'mango') 66 | do_after(5000, basket.fruits.insert, 0, 'banana') 67 | 68 | # Start the event loop 69 | app.exec_() 70 | 71 | # Check the final values of the list attribute 72 | print(basket.fruits) 73 | 74 | if __name__ == "__main__": 75 | main() 76 | 77 | #### EOF ###################################################################### 78 | -------------------------------------------------------------------------------- /examples/ex11_method_call.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example demonstrates Jigna's ability to call public methods of the 3 | traits model from the HTML interface. You can supply primitive arguments and 4 | also pass instances. 5 | """ 6 | 7 | #### Imports #### 8 | from __future__ import print_function 9 | 10 | from traits.api import HasTraits, Int, Str, Instance 11 | from jigna.api import HTMLWidget, Template 12 | from jigna.qt import QtGui 13 | 14 | #### Domain model #### 15 | 16 | class Person(HasTraits): 17 | name = Str 18 | age = Int 19 | spouse = Instance('Person') 20 | 21 | def greet(self): 22 | """ Simple method without any arguments. """ 23 | print('Greetings %s, from the Javascript world!' % self.name) 24 | 25 | def update_age(self, age): 26 | """ Method which takes a primitive argument. """ 27 | self.age = age 28 | 29 | def marry(self, person): 30 | """ Method which takes an instance argument. """ 31 | self.spouse = person 32 | person.spouse = self 33 | 34 | #### UI layer #### 35 | 36 | body_html = """ 37 |
38 | Name:
39 | Age: {{person.age}}
40 | Spouse: {{person.spouse.name}}
41 | 42 | 43 | 44 | 45 |
46 | """ 47 | 48 | template = Template(body_html=body_html) 49 | 50 | #### Entry point #### 51 | 52 | def main(): 53 | # Start the Qt application 54 | app = QtGui.QApplication([]) 55 | 56 | # Instantiate the domain models 57 | fred = Person(name='Fred', age=14) 58 | wilma = Person(name='Wilma', age=25) 59 | 60 | # Create the jigna based HTML widget which renders the given HTML template 61 | # with the given context. 62 | widget = HTMLWidget( 63 | template=template, context={'person': fred, 'spouse': wilma} 64 | ) 65 | widget.show() 66 | 67 | # Start the event loop. 68 | # 69 | # Clicking on the buttons in the UI will make blocking calls to the 70 | # corresponding methods on the domain model. You can supply primitive as 71 | # well as instance objects as arguments of the method. 72 | app.exec_() 73 | 74 | # Check the final values after the UI is closed 75 | print(fred.name, fred.age, fred.spouse.name) 76 | 77 | if __name__ == '__main__': 78 | main() 79 | 80 | #### EOF ###################################################################### 81 | -------------------------------------------------------------------------------- /examples/ex12_method_call_slow.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to call slow methods of the application model such 3 | that the UI doesn't block while the method is executing. We present an API to 4 | call such methods in a thread. 5 | 6 | Note: It's the user's responsibility to make sure that there aren't thread 7 | safety related issues with that method. 8 | """ 9 | 10 | #### Imports #### 11 | from __future__ import print_function 12 | 13 | from traits.api import HasTraits, Int, Str, Instance 14 | from jigna.api import HTMLWidget, Template 15 | from jigna.qt import QtGui 16 | import time 17 | 18 | #### Domain model #### 19 | 20 | class Package(HasTraits): 21 | name = Str 22 | version = Str 23 | 24 | class Installer(HasTraits): 25 | current = Instance('Package') 26 | progress = Int 27 | 28 | def install(self, package): 29 | self.current = package 30 | while self.progress < 100: 31 | time.sleep(0.5) 32 | self.progress += 10 33 | 34 | #### UI layer #### 35 | 36 | body_html = """ 37 |
38 | 39 | 43 | 44 | 45 |
46 | Installing {{installer.current.name}}-{{installer.current.version}}... 47 |
48 | 49 | 50 |
51 | Done! 52 |
53 | 54 | 55 |
56 |
58 |
59 |
60 |
61 | 62 | 73 | """ 74 | 75 | template = Template(body_html=body_html) 76 | 77 | #### Entry point #### 78 | 79 | def main(): 80 | # Start the Qt application 81 | app = QtGui.QApplication([]) 82 | 83 | # Instantiate the domain models 84 | installer = Installer() 85 | pandas = Package(name='Pandas', version='1.0') 86 | 87 | # Create the jigna based HTML widget which renders the given HTML template 88 | # with the given context. 89 | widget = HTMLWidget( 90 | template=template, 91 | context={'installer': installer, 'new_package': pandas} 92 | ) 93 | widget.show() 94 | 95 | # Start the event loop. 96 | # 97 | # Clicking on the button in the UI will call the `install` method in a 98 | # thread so that the UI is still responsive while the method is executing. 99 | # The progress bar is also updated as the method progresses. 100 | app.exec_() 101 | 102 | # Check the final values 103 | print(installer.current.name, installer.current.version) 104 | 105 | if __name__ == '__main__': 106 | main() 107 | 108 | #### EOF ###################################################################### 109 | -------------------------------------------------------------------------------- /examples/ex13_success_error_callbacks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 13 | 48 | 49 | 50 | 51 |
52 | 54 | 57 | 58 | 60 | 63 | 64 |
{{status}}
65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /examples/ex13_success_error_callbacks.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to attach 'done' and 'error' callbacks to a slow 3 | method call. 4 | """ 5 | 6 | #### Imports #### 7 | 8 | from traits.api import HasTraits 9 | from jigna.api import HTMLWidget, Template 10 | from jigna.qt import QtGui 11 | import time 12 | 13 | #### Domain model #### 14 | 15 | class Worker(HasTraits): 16 | 17 | def do_work(self): 18 | """ Simulate doing some work by sleeping a bit :) """ 19 | time.sleep(1) 20 | 21 | def do_illegal_work(self): 22 | """ Simulate doing an illegal work that will raise an error """ 23 | time.sleep(1) 24 | raise Exception("Illegal operation") 25 | 26 | #### UI layer #### 27 | 28 | template = Template(html_file='success_error_callbacks.html') 29 | 30 | #### Entry point #### 31 | 32 | def main(): 33 | # Start the Qt application 34 | app = QtGui.QApplication([]) 35 | 36 | # Instantiate the domain models 37 | worker = Worker() 38 | 39 | # Create the jigna based HTML widget which renders the given HTML template 40 | # with the given context. 41 | widget = HTMLWidget(template=template, context={'worker': worker}) 42 | widget.show() 43 | 44 | # Start the event loop. 45 | # 46 | # The view related code is such that clicking on the buttons in the UI will 47 | # call methods on the domain model and do something when the method call 48 | # succeeded or failed. 49 | app.exec_() 50 | 51 | if __name__ == '__main__': 52 | main() 53 | 54 | #### EOF ###################################################################### 55 | -------------------------------------------------------------------------------- /examples/ex14_event_trait.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example demonstrates Jigna's ability to respond to `Event` trait firings. 3 | """ 4 | 5 | #### Imports #### 6 | 7 | from traits.api import HasTraits, Str, Event, List 8 | from jigna.api import HTMLWidget, Template 9 | from jigna.qt import QtGui 10 | import time 11 | 12 | #### Domain model #### 13 | 14 | class Downloader(HasTraits): 15 | 16 | file_urls = List(Str) 17 | file_downloaded = Event 18 | 19 | def download_files(self): 20 | """ Download the files specified by the file_urls. Fire a 21 | `file_downloaded` event with each file download. """ 22 | 23 | for file_url in self.file_urls: 24 | # Simulate downloading the file 25 | time.sleep(1) 26 | 27 | # This will fire the `file_downloaded` event and make `file_url` as 28 | # the event payload 29 | self.file_downloaded = file_url 30 | 31 | #### UI layer #### 32 | 33 | body_html = """ 34 |
35 | 36 | 39 | 40 | 41 | 57 |
58 | """ 59 | 60 | template = Template(body_html=body_html) 61 | 62 | #### Entry point #### 63 | 64 | def main(): 65 | # Start the Qt application 66 | app = QtGui.QApplication([]) 67 | 68 | # Instantiate the domain model 69 | file_urls = ['images/lena.png', 'videos/big-buck-bunny.mp4'] 70 | downloader = Downloader(file_urls=file_urls) 71 | 72 | # Create the jigna based HTML widget which renders the given HTML template 73 | # with the given context. 74 | widget = HTMLWidget(template=template, context={'downloader':downloader}) 75 | widget.show() 76 | 77 | # Start the event loop. 78 | # 79 | # Clicking on the button in the UI will call the `download_files` method in 80 | # a thread. After each file is "downloaded", a Javascript alert should come 81 | # up. This is in response to a Python event on each file download. 82 | app.exec_() 83 | 84 | if __name__ == "__main__": 85 | main() 86 | 87 | #### EOF ###################################################################### 88 | -------------------------------------------------------------------------------- /examples/ex15_embedding_in_qt.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to embed a jigna view to an existing QWidget framework. 3 | """ 4 | 5 | #### Imports #### 6 | 7 | from jigna.api import HTMLWidget, Template 8 | from jigna.qt import QtGui 9 | from traits.api import HasTraits, Int, Str 10 | 11 | #### Domain model #### 12 | 13 | class Employee(HasTraits): 14 | name = Str 15 | salary = Int 16 | 17 | def update_salary(self): 18 | self.salary += int(0.2*self.salary) 19 | 20 | #### UI layer #### 21 | 22 | body_html = """ 23 |
24 | Employee name is {{employee.name}}
25 | Salary is ${{employee.salary}}
26 | 27 | 30 |
31 | """ 32 | 33 | template = Template(body_html=body_html) 34 | 35 | def create_jigna_widget(): 36 | """ Create a jigna based HTML widget. This widget is embedded in a QtGui 37 | application. 38 | 39 | """ 40 | # Instantiate the domain model 41 | tom = Employee(name='Tom', salary=2000) 42 | 43 | # Create the jigna based HTML widget which renders the given HTML template 44 | # with the given context. 45 | return HTMLWidget(template=template, context={'employee': tom}) 46 | 47 | #### Entry point #### 48 | 49 | def main(): 50 | """ Create and show a QMainWindow which embeds the given widget on a button 51 | click. 52 | 53 | """ 54 | app = QtGui.QApplication([]) 55 | 56 | # Define a new QMainWindow 57 | window = QtGui.QMainWindow() 58 | window.setMinimumSize(600, 400) 59 | 60 | # Set up the QLayout 61 | layout = QtGui.QVBoxLayout() 62 | window.setCentralWidget(QtGui.QWidget()) 63 | window.centralWidget().setLayout(layout) 64 | 65 | # Add a button to the layout which embeds the supplied widget on click. 66 | button = QtGui.QPushButton("I'm a QPushButton. Press me to embed a widget") 67 | button.clicked.connect(lambda : layout.addWidget(create_jigna_widget())) 68 | layout.addWidget(button) 69 | 70 | # Show the window 71 | window.show() 72 | 73 | app.exec_() 74 | 75 | if __name__ == "__main__": 76 | main() 77 | 78 | #### EOF ###################################################################### 79 | -------------------------------------------------------------------------------- /examples/ex16_template_person.html: -------------------------------------------------------------------------------- 1 |
2 | {{person.name}}, {{person.age}} years old. 3 |
-------------------------------------------------------------------------------- /examples/ex16_templating.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('templating', ['jigna']); 2 | 3 | app.run(function(){ 4 | jigna.initialize(); 5 | }) 6 | 7 | app.directive('personView', function(){ 8 | return { 9 | scope: {person: '='}, 10 | templateUrl: 'ex16_template_person.html' 11 | } 12 | }) -------------------------------------------------------------------------------- /examples/ex16_templating.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to include HTML templates for creating reusable 3 | components. 4 | """ 5 | 6 | #### Imports #### 7 | 8 | from traits.api import HasTraits, Int, Str 9 | from jigna.api import HTMLWidget, Template 10 | from jigna.qt import QtGui 11 | 12 | #### Domain model #### 13 | 14 | class Person(HasTraits): 15 | name = Str 16 | age = Int 17 | 18 | #### UI layer #### 19 | 20 | html = """ 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | """ 32 | 33 | # The base_url field specifies where to look when trying to get external 34 | # resources(defaults to an empty string, i.e. the current directory) 35 | template = Template(html=html) 36 | 37 | #### Entry point #### 38 | 39 | 40 | def main(): 41 | # Start the Qt application 42 | app = QtGui.QApplication([]) 43 | 44 | # Instantiate the domain model 45 | fred = Person(name='Fred', age=28) 46 | wilma = Person(name='Wilma', age=25) 47 | 48 | # Create the jigna based HTML widget which renders the given HTML template 49 | # with the given context. 50 | widget = HTMLWidget( 51 | template=template, context={'fred': fred, 'wilma': wilma} 52 | ) 53 | widget.show() 54 | 55 | # Start the event loop. 56 | # 57 | # You should see that the person-view component's template is rendered with 58 | # the correct domain models. 59 | app.exec_() 60 | 61 | if __name__ == "__main__": 62 | main() 63 | 64 | #### EOF ###################################################################### 65 | -------------------------------------------------------------------------------- /examples/ex17_custom_angular_application.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 14 | 30 | 31 | 32 | 33 | Time: {{stop_watch.time | displayTime}}
34 | 35 | 37 | 41 | 42 | 43 | 47 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/ex17_custom_angular_application.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to use Jigna alongwith a custom angularjs application. We 3 | would need a custom angularjs application to add some view related logic. 4 | """ 5 | 6 | #### Imports #### 7 | from __future__ import print_function 8 | 9 | from jigna.api import HTMLWidget, Template 10 | from jigna.qt import QtGui 11 | import time 12 | from traits.api import HasTraits, Enum, Int 13 | 14 | #### Domain model #### 15 | 16 | class StopWatch(HasTraits): 17 | state = Enum('stopped', 'running') 18 | time = Int 19 | 20 | def start(self): 21 | self.state = 'running' 22 | while self.state == 'running': 23 | time.sleep(1) 24 | self.time += 1 25 | 26 | def stop(self): 27 | self.state = 'stopped' 28 | 29 | def reset(self): 30 | self.time = 0 31 | 32 | #### UI layer #### 33 | 34 | template = Template(html_file='ex17_custom_angular_application.html') 35 | 36 | #### Entry point #### 37 | 38 | def main(): 39 | # Start the Qt application 40 | app = QtGui.QApplication([]) 41 | 42 | # Instantiate the domain model 43 | stop_watch = StopWatch() 44 | 45 | # Create the jigna based HTML widget which renders the given HTML template 46 | # with the given context. 47 | # 48 | # The operations on the stop watch can be controlled from the UI. The view 49 | # related logic is such that it always displays the integer time of the 50 | # domain model in a proper hh:mm:ss format. 51 | widget = HTMLWidget(template=template, context={'stop_watch': stop_watch}) 52 | widget.show() 53 | 54 | # Start the event loop 55 | app.exec_() 56 | 57 | # Check the values after the UI is closed 58 | print(stop_watch.time, "seconds") 59 | 60 | if __name__ == "__main__": 61 | main() 62 | 63 | #### EOF ###################################################################### 64 | -------------------------------------------------------------------------------- /examples/ex18_embedding_chaco.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to embed a Chaco QWidget inside the jigna view using 3 | an tag. 4 | """ 5 | 6 | #### Imports #### 7 | 8 | from chaco.api import Plot, ArrayPlotData 9 | from jigna.api import HTMLWidget, Template 10 | from jigna.qt import QtGui 11 | from numpy import linspace, sin 12 | from traits.api import ( 13 | HasTraits, CInt, Instance, Array, Property, on_trait_change 14 | ) 15 | 16 | #### Domain model #### 17 | 18 | class DomainModel(HasTraits): 19 | """ 20 | The algorithmic domain model which specifies the mathematical relationship 21 | between x and y. 22 | """ 23 | 24 | #: Independent variable of the domain equation 25 | x = Array 26 | def _x_default(self): 27 | return linspace(-14, 14, 1000) 28 | 29 | #: Dependent variable of the domain equation 30 | y = Property(Array, depends_on=['x', 'scaling_factor']) 31 | def _get_y(self): 32 | return sin(self.scaling_factor * self.x) * self.x**3 33 | 34 | #: A scaling factor to tune the output 35 | scaling_factor = CInt 36 | 37 | #### Controller layer #### 38 | 39 | class PlotController(HasTraits): 40 | """ 41 | A Controller class which creates a Chaco plot object for the given domain 42 | model. 43 | """ 44 | 45 | #: Instance of the domain model which is being displayed by this controller 46 | domain_model = Instance(DomainModel) 47 | 48 | # The Chaco Plot object. This is the object which is usually visualized via 49 | # traitsui using the enable ComponentEditor 50 | plot = Instance(Plot) 51 | def _plot_default(self): 52 | plot = Plot(ArrayPlotData(x=self.domain_model.x, y=self.domain_model.y)) 53 | plot.plot(("x", "y"), type="line", color="blue") 54 | 55 | return plot 56 | 57 | @on_trait_change('domain_model.scaling_factor') 58 | def update_plot(self): 59 | self.plot.data.set_data('x', self.domain_model.x) 60 | self.plot.data.set_data('y', self.domain_model.y) 61 | 62 | def create_plot_widget(self): 63 | """ This method is used as a factory to create the QWidget for the Chaco 64 | 'plot' component. 65 | 66 | This is the standard way to obtain the QWidget out of a Chaco Plot object. 67 | We create a hidden traitsui view and return it's 'control' to obtain the 68 | required QWidget. 69 | """ 70 | from traitsui.api import View, Item 71 | from enable.api import ComponentEditor 72 | 73 | view = View(Item('plot', editor=ComponentEditor(), show_label=False)) 74 | ui = self.edit_traits(view=view, parent=None, kind='subpanel') 75 | 76 | return ui.control 77 | 78 | #### UI layer #### 79 | 80 | body_html = """ 81 |
82 | Scaling factor:
84 | Plot:
85 | 86 | 89 |
90 | 93 | 94 |
95 |
96 | """ 97 | 98 | template = Template(body_html=body_html, recommended_size=(600, 600)) 99 | 100 | #### Entry point #### 101 | 102 | def main(): 103 | # Start the Qt application 104 | app = QtGui.QApplication.instance() or QtGui.QApplication([]) 105 | 106 | # Instantiate the domain model and the plot controller 107 | domain_model = DomainModel(scaling_factor=50) 108 | plot_controller = PlotController(domain_model=domain_model) 109 | 110 | # Create the jigna based HTML widget which renders the given HTML template 111 | # with the given context. 112 | # 113 | # The widget contains an embedded Chaco QWidget showing a 2D plot of 114 | # the domain model. Moving the slider on the UI changes the domain model 115 | # and hence the Chaco plot. 116 | widget = HTMLWidget( 117 | template=template, 118 | context={ 119 | 'domain_model': domain_model, 120 | 'plot_controller': plot_controller 121 | } 122 | ) 123 | widget.show() 124 | 125 | # Start the event loop 126 | app.exec_() 127 | 128 | if __name__ == "__main__": 129 | main() 130 | 131 | #### EOF ###################################################################### 132 | -------------------------------------------------------------------------------- /examples/ex19_embedding_mayavi.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to embed a Mayavi QWidget inside the jigna view using 3 | an tag. 4 | 5 | Mayavi widget is different than a Chaco widget since you can interact with the 6 | widget so it demonstrates that mouse events are properly forwarded to the 7 | underlying QWidget when you embed it in jigna view. 8 | """ 9 | 10 | #### Imports #### 11 | 12 | from jigna.api import HTMLWidget, Template 13 | from jigna.qt import QtGui 14 | from mayavi.core.api import PipelineBase 15 | from mayavi.core.ui.api import MlabSceneModel 16 | from traits.api import HasTraits, Instance, CInt, on_trait_change 17 | 18 | 19 | #### Domain model #### 20 | 21 | class SceneController(HasTraits): 22 | n_meridional = CInt(10) 23 | n_longitudinal = CInt(10) 24 | 25 | scene = Instance(MlabSceneModel, ()) 26 | 27 | plot = Instance(PipelineBase) 28 | 29 | # When the scene is activated, or when the parameters are changed, we 30 | # update the plot. 31 | @on_trait_change('n_meridional,n_longitudinal,scene.activated') 32 | def update_plot(self): 33 | x, y, z, t = self.get_data() 34 | if self.plot is None: 35 | self.plot = self.scene.mlab.plot3d(x, y, z, t, 36 | tube_radius=0.025, colormap='Spectral') 37 | else: 38 | self.plot.mlab_source.set(x=x, y=y, z=z, scalars=t) 39 | 40 | def get_data(self): 41 | """ Obtain the x,y,z,t data for the domain equation and given values of 42 | n_meridional and n_longitudinal 43 | """ 44 | from numpy import arange, pi, cos, sin 45 | phi = arange(0.0, 2*pi + 0.5*pi/1000, pi/1000, 'd') 46 | 47 | mu = phi*self.n_meridional 48 | x = cos(mu) * (1 + cos(self.n_longitudinal * mu/self.n_meridional)*0.5) 49 | y = sin(mu) * (1 + cos(self.n_longitudinal * mu/self.n_meridional)*0.5) 50 | z = 0.5 * sin(self.n_longitudinal*mu/self.n_meridional) 51 | t = sin(mu) 52 | 53 | return x, y, z, t 54 | 55 | def create_scene_widget(self): 56 | """ Factory method to create the QWidget for the Mayavi scene. 57 | 58 | This follows the same approach as the Chaco example i.e. create a hidden 59 | traitsui view based on the mayavi SceneEditor and return it's 'control' 60 | to obtain the required QWidget. 61 | """ 62 | from traitsui.api import View, Item 63 | from mayavi.core.ui.api import MayaviScene, SceneEditor 64 | view = View(Item('scene', show_label=False, 65 | editor=SceneEditor(scene_class=MayaviScene)), 66 | resizable=True) 67 | ui = self.edit_traits(view=view, parent=None, kind='subpanel') 68 | 69 | return ui.control 70 | 71 | 72 | #### UI layer #### 73 | 74 | body_html = """ 75 |
76 | N meridonial:
78 | N longitudinal:
80 | Plot:
81 | 82 | 84 | 85 |
86 | """ 87 | 88 | template = Template(body_html=body_html) 89 | 90 | #### Entry point #### 91 | 92 | def main(): 93 | # Start the Qt application 94 | app = QtGui.QApplication.instance() or QtGui.QApplication([]) 95 | 96 | # Instantiate the domain model 97 | scene_controller = SceneController() 98 | 99 | # Create the jigna based HTML widget which renders the given HTML template 100 | # with the given context. 101 | # 102 | # The view contains an embedded Mayavi QWidget showing a visualization of 103 | # the domain model. Moving the sliders on the UI changes the domain model and 104 | # hence the Mayavi visualization. 105 | widget = HTMLWidget( 106 | template=template, context={'scene_controller': scene_controller} 107 | ) 108 | widget.show() 109 | 110 | # Start the event loop 111 | app.exec_() 112 | 113 | if __name__ == "__main__": 114 | main() 115 | 116 | #### EOF ###################################################################### 117 | -------------------------------------------------------------------------------- /examples/ex1_simple_view.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example is the simplest example which shows how to start a jigna view. 3 | """ 4 | 5 | #### Imports #### 6 | from __future__ import print_function 7 | 8 | from jigna.api import HTMLWidget, Template 9 | from jigna.qt import QtGui 10 | from traits.api import HasTraits, Int, Str 11 | 12 | #### Domain model #### 13 | 14 | class Person(HasTraits): 15 | name = Str 16 | age = Int 17 | 18 | #### UI layer #### 19 | 20 | body_html = """ 21 |
22 | Name:
23 | Age: 24 |
25 | """ 26 | 27 | template = Template(body_html=body_html) 28 | 29 | #### Entry point #### 30 | 31 | def main(): 32 | # Start a QtGui application 33 | app = QtGui.QApplication([]) 34 | 35 | # Instantiate the domain model 36 | fred = Person(name='Fred', age=42) 37 | 38 | # Create a jigna based HTML widget to render the HTML template with the 39 | # given context. 40 | widget = HTMLWidget(template=template, context={'person': fred}) 41 | widget.show() 42 | 43 | # Start the event loop 44 | app.exec_() 45 | 46 | # Check the values after the UI is closed 47 | print(fred.name, fred.age) 48 | 49 | if __name__ == "__main__": 50 | main() 51 | 52 | #### EOF ###################################################################### 53 | -------------------------------------------------------------------------------- /examples/ex20_mayavi_webgl_demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 75 | 76 | 77 | 78 | 79 | Expression:
80 | 81 | Number of contours: 83 | 84 |
86 |
87 | 88 | 89 | -------------------------------------------------------------------------------- /examples/ex20_mayavi_webgl_demo.py: -------------------------------------------------------------------------------- 1 | """ This example shows how to show mayavi in the web version using the webGL 2 | backend for VTK. 3 | """ 4 | 5 | #### Imports #### 6 | from __future__ import print_function 7 | 8 | import json 9 | import numpy as np 10 | from mayavi import mlab 11 | from mayavi.core.api import PipelineBase 12 | from traits.api import HasTraits, Instance, Int, Str 13 | from tvtk.api import tvtk 14 | from jigna.web_app import WebApp 15 | from jigna.template import Template 16 | from tornado.ioloop import IOLoop 17 | 18 | mlab.options.offscreen = True 19 | mlab.options.backend = 'test' 20 | 21 | #### Domain Model #### 22 | 23 | class MeshData(HasTraits): 24 | 25 | # Data in the format of an old style VTK file. 26 | filedata = Str 27 | 28 | # The colors to use for the points, in json format. 29 | colors = Str 30 | 31 | # The type of the mesh, for now only polygons are supported. 32 | type = Str("POLYGONS") 33 | 34 | @classmethod 35 | def from_dataset(cls, dataset, module_manager): 36 | filedata = cls._dataset_to_string(dataset) 37 | colors = cls._dataset_to_colors(dataset, module_manager) 38 | 39 | return MeshData(filedata=filedata, colors=colors) 40 | 41 | #### Private protocol ##################################################### 42 | 43 | @classmethod 44 | def _dataset_to_string(cls, dataset): 45 | """ 46 | Convert the dataset to a vtk filedata string. 47 | """ 48 | writer = tvtk.PolyDataWriter( 49 | input=dataset, 50 | write_to_output_string=True 51 | ) 52 | writer.write() 53 | return writer.output_string 54 | 55 | @classmethod 56 | def _dataset_to_colors(cls, dataset, module_manager): 57 | """ 58 | Given the dataset, extract the colors array in a jsonized format. 59 | """ 60 | scm = module_manager.scalar_lut_manager 61 | scalars = dataset.point_data.scalars 62 | 63 | colors = scm.lut.map_scalars(scalars, 0, -1).to_array()/255.0 64 | colors_on_points = colors[cls._point_indices(dataset)] 65 | 66 | return json.dumps(colors_on_points.tolist()) 67 | 68 | @classmethod 69 | def _point_indices(cls, dataset): 70 | """ 71 | Given the dataset, obtain the polygon connectivity array and generate 72 | the indices of the points in the polygon. 73 | """ 74 | conn = dataset.polys.to_array() 75 | npoly = conn.size / 4 76 | choice = np.zeros(npoly*3, dtype=int) 77 | for start in (1, 2, 3): 78 | choice[start-1::3] = np.arange(start, npoly*4, step=4) 79 | return conn[choice] 80 | 81 | class Plotter3D(HasTraits): 82 | 83 | #: expression to be visualized 84 | expression = Str 85 | def _expression_changed(self, expr): 86 | if self.plot is None: 87 | x, y, z = np.mgrid[-5:5:32j, -5:5:32j, -5:5:32j] 88 | else: 89 | x, y, z = self.x, self.y, self.z 90 | g = np.__dict__ 91 | try: 92 | s = eval(expr, g, dict(x=x, y=y, z=z)) 93 | except: 94 | pass 95 | else: 96 | if self.plot is None: 97 | self.x, self.y, self.z = x, y, z 98 | self.plot = mlab.contour3d(x, y, z, s, contours=self.n_contour) 99 | else: 100 | self.plot.mlab_source.set(scalars=s) 101 | self._update_mesh_data() 102 | 103 | #: number of contours for the visualization 104 | n_contour = Int(2) 105 | def _n_contour_changed(self, value): 106 | if 0 < value < 20: 107 | if self.plot: 108 | self.plot.contour.number_of_contours = value 109 | self._update_mesh_data() 110 | 111 | plot = Instance(PipelineBase) 112 | 113 | mesh_data = Instance(MeshData, ()) 114 | 115 | def _update_mesh_data(self): 116 | self.mesh_data.copy_traits( 117 | MeshData.from_dataset( 118 | dataset=self.plot.contour.outputs[0], 119 | module_manager=self.plot.module_manager 120 | ) 121 | ) 122 | 123 | #### UI layer #### 124 | 125 | template = Template(html_file='ex20_mayavi_webgl_demo.html') 126 | 127 | #### Entry point #### 128 | 129 | def main(): 130 | # Start the tornado ioloop application 131 | ioloop = IOLoop.instance() 132 | 133 | # Instantiate the domain model 134 | plotter = Plotter3D(expression="x*x*0.5 + y*y + z*z*2.0", n_contour=4) 135 | 136 | # Create a web app serving the view with the domain model added to its 137 | # context. 138 | app = WebApp(template=template, context={'plotter': plotter}) 139 | app.listen(8000) 140 | 141 | # Start serving the web app on port 8000. 142 | print('Serving on port 8000...') 143 | ioloop.start() 144 | 145 | if __name__ == '__main__': 146 | main() 147 | -------------------------------------------------------------------------------- /examples/ex21_matplotlib_web.py: -------------------------------------------------------------------------------- 1 | #### Imports #### 2 | from __future__ import print_function 3 | 4 | from jigna.api import Template, WebApp 5 | from numpy import linspace, sin, pi 6 | from tornado.ioloop import IOLoop 7 | from traits.api import ( 8 | HasTraits, CInt, Str, Property, Array, Instance, on_trait_change 9 | ) 10 | 11 | #### Domain model #### 12 | 13 | class DomainModel(HasTraits): 14 | """ 15 | The algorithmic domain model which specifies the mathematical relationship 16 | between x and y. 17 | """ 18 | 19 | #: Independent variable of the domain equation 20 | x = Array 21 | def _x_default(self): 22 | return linspace(-2*pi, 2*pi, 200) 23 | 24 | #: Dependent variable of the domain equation 25 | y = Property(Array, depends_on=['x', 'scaling_factor']) 26 | def _get_y(self): 27 | return sin(self.scaling_factor * self.x) / self.x 28 | 29 | #: A scaling factor to tune the output 30 | scaling_factor = CInt 31 | 32 | #### Controller layer #### 33 | 34 | class PlotController(HasTraits): 35 | """ 36 | A Controller class which creates a matplotlib plot object (in the form of a 37 | png image bytestream) for the given domain model. 38 | """ 39 | 40 | #: Instance of the domain model which is being displayed by this controller 41 | domain_model = Instance(DomainModel) 42 | 43 | #: The 'png' bytestream representation of the matplotlib plot object 44 | plot = Str 45 | 46 | @on_trait_change('domain_model.scaling_factor') 47 | def update_plot(self): 48 | # Use the Agg backend to generate images without making the window appear 49 | import matplotlib 50 | matplotlib.use('Agg') 51 | 52 | # Generate the plot 53 | from matplotlib import pyplot 54 | pyplot.clf() 55 | pyplot.plot(self.domain_model.x, self.domain_model.y) 56 | 57 | # Generate image data in png format 58 | from io import StringIO 59 | stream = StringIO() 60 | pyplot.savefig(stream, format='png') 61 | stream.seek(0) 62 | self.plot = stream.buf.encode('base64') 63 | 64 | #### UI layer #### 65 | 66 | body_html = """ 67 |
68 | Scaling factor:
70 | Plot:
71 | 72 |
73 | """ 74 | 75 | template = Template(body_html=body_html) 76 | 77 | #### Entry point #### 78 | 79 | def main(): 80 | # Start the tornado ioloop application 81 | ioloop = IOLoop.instance() 82 | 83 | # Instantiate the domain model and the plot controller 84 | domain_model = DomainModel(scaling_factor=15) 85 | plot_controller = PlotController(domain_model=domain_model) 86 | 87 | # Create a WebApp to render the HTML template with the given context. 88 | # 89 | # The web app shows a matplotlib plot in a browser by displaying the png 90 | # image of the plot. Moving the HTML slider on the UI changes the domain 91 | # model and hence the plot image. 92 | app = WebApp( 93 | template=template, 94 | context={ 95 | 'domain_model': domain_model, 96 | 'plot_controller': plot_controller 97 | } 98 | ) 99 | app.listen(8000) 100 | 101 | # Start serving the web app on port 8000. 102 | print('Serving on port 8000...') 103 | ioloop.start() 104 | 105 | if __name__ == "__main__": 106 | main() 107 | 108 | #### EOF ###################################################################### 109 | -------------------------------------------------------------------------------- /examples/ex22_vuejs_demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | Name: 16 | Age: 17 |
18 | Fruits:
19 | 20 | 21 |
22 |
+
23 | 24 | Friends:
25 | Name: 26 | Age: 27 | 28 |
29 |
+
30 | 31 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/ex22_vuejs_demo.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to use Jigna using the Vue.js Javascript framework, 3 | instead of AngularJS. 4 | """ 5 | 6 | #### Imports #### 7 | from __future__ import print_function 8 | 9 | from traits.api import HasTraits, Int, Str, List, Instance 10 | from jigna.api import HTMLWidget, VueTemplate 11 | from jigna.qt import QtGui 12 | from jigna.utils.gui import do_after 13 | 14 | #### Domain model #### 15 | 16 | class Person(HasTraits): 17 | name = Str 18 | age = Int 19 | fruits = List(Str) 20 | friends = List(Instance('Person')) 21 | 22 | def add_fruit(self, name='fruit'): 23 | self.fruits.append(name) 24 | 25 | def remove_fruit(self, index): 26 | del self.fruits[index] 27 | 28 | def update_name(self, name): 29 | print("Name updated to", name) 30 | self.name = name 31 | 32 | def add_friend(self): 33 | self.friends.append(Person(name='Person', age=0)) 34 | 35 | def remove_friend(self, index): 36 | del self.friends[index] 37 | 38 | def _fruits_items_changed(self, l_event): 39 | print(l_event.added, l_event.removed, l_event.index) 40 | 41 | def _name_changed(self, old, new): 42 | print("name", old, new) 43 | 44 | def _age_changed(self, old, new): 45 | print("age", old, new) 46 | 47 | 48 | #### Entry point #### 49 | 50 | def main(): 51 | # Start the Qt application 52 | app = QtGui.QApplication([]) 53 | 54 | # Instantiate the domain model 55 | 56 | fred = Person(name='Fred', age=42, fruits=['pear', 'apple']) 57 | 58 | template = VueTemplate(html_file='ex22_vuejs_demo.html') 59 | 60 | # Create the jigna based HTML widget which renders the given HTML template 61 | # with the given context. 62 | widget = HTMLWidget(template=template, context={'person': fred}, debug=True) 63 | widget.show() 64 | 65 | # Schedule an update to a model variable after 2.5 seconds. This update 66 | # will be reflected in the UI immediately. 67 | do_after(2500, fred.update_name, "Guido") 68 | do_after(2500, fred.add_fruit) 69 | do_after(2500, fred.add_friend) 70 | 71 | # Start the event loop 72 | app.exec_() 73 | 74 | # Check the values after the UI is closed 75 | print(fred.name, fred.age, fred.fruits, fred.friends) 76 | 77 | if __name__ == "__main__": 78 | main() 79 | 80 | #### EOF ###################################################################### 81 | -------------------------------------------------------------------------------- /examples/ex23_vuejs_web_demo.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to use Jigna using the Vue.js Javascript framework, 3 | instead of AngularJS. 4 | """ 5 | 6 | #### Imports #### 7 | from __future__ import print_function 8 | 9 | from traits.api import HasTraits, Int, Str, List, Instance 10 | from jigna.vue_template import VueTemplate 11 | from jigna.web_app import WebApp 12 | from tornado.ioloop import IOLoop 13 | 14 | 15 | #### Domain model #### 16 | 17 | class Person(HasTraits): 18 | name = Str 19 | age = Int 20 | fruits = List(Str) 21 | friends = List(Instance('Person')) 22 | 23 | def add_fruit(self, name='fruit'): 24 | self.fruits.append(name) 25 | 26 | def remove_fruit(self, index): 27 | del self.fruits[index] 28 | 29 | def update_name(self, name): 30 | print("Name updated to", name) 31 | self.name = name 32 | 33 | def add_friend(self): 34 | self.friends.append(Person(name='Person', age=0)) 35 | 36 | def remove_friend(self, index): 37 | del self.friends[index] 38 | 39 | def _fruits_items_changed(self, l_event): 40 | print(l_event.added, l_event.removed, l_event.index) 41 | 42 | def _name_changed(self, old, new): 43 | print("name", old, new) 44 | 45 | def _age_changed(self, old, new): 46 | print("age", old, new) 47 | 48 | 49 | #### Entry point #### 50 | 51 | def main(): 52 | # Start the tornado ioloop application 53 | ioloop = IOLoop.instance() 54 | 55 | # Instantiate the domain model 56 | 57 | fred = Person(name='Fred', age=42, fruits=['pear', 'apple']) 58 | template = VueTemplate(html_file='ex22_vuejs_demo.html') 59 | 60 | # Create the jigna based HTML widget which renders the given HTML template 61 | # with the given context. 62 | app = WebApp(template=template, context={'person': fred}) 63 | app.listen(8000) 64 | 65 | # Schedule an update to a model variable after 5 seconds. This update 66 | # will be reflected in the UI immediately. 67 | ioloop.call_later(5, setattr, fred, 'update_name', 'Guido') 68 | ioloop.call_later(5, fred.add_fruit) 69 | ioloop.call_later(5, fred.add_friend) 70 | 71 | # Start serving the web app on port 8000. 72 | # 73 | # Point your web browser to http://localhost:8000/ to connect to this jigna 74 | # web app. Any operation performed on the client directly update the 75 | # model attributes on the server. 76 | print('Serving on port 8000...') 77 | ioloop.start() 78 | 79 | 80 | if __name__ == "__main__": 81 | main() 82 | 83 | #### EOF ###################################################################### 84 | -------------------------------------------------------------------------------- /examples/ex2_simple_view_full.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 16 | 17 | Name: 18 | Age: 19 | 20 | 21 | -------------------------------------------------------------------------------- /examples/ex2_simple_view_full_html.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to initialize Jigna's HTML interface by reading 3 | a full html file, rather than specifying body_html and head_html. 4 | """ 5 | 6 | #### Imports #### 7 | from __future__ import print_function 8 | 9 | from traits.api import HasTraits, Int, Str 10 | from jigna.api import HTMLWidget, Template 11 | from jigna.qt import QtGui 12 | 13 | #### Domain model #### 14 | 15 | class Person(HasTraits): 16 | name = Str 17 | age = Int 18 | 19 | def update_name(self, name): 20 | self.name = name 21 | 22 | #### UI layer #### 23 | 24 | template = Template(html_file='ex2_simple_view_full.html') 25 | 26 | #### Entry point #### 27 | 28 | def main(): 29 | # Start the Qt application 30 | app = QtGui.QApplication([]) 31 | 32 | # Instantiate the domain model 33 | fred = Person(name='Fred', age=42) 34 | 35 | # Create the jigna based HTML widget which renders the given HTML template 36 | # with the given context. 37 | widget = HTMLWidget(template=template, context={'person': fred}) 38 | widget.show() 39 | 40 | # Start the event loop 41 | app.exec_() 42 | 43 | # Check the values after the UI is closed 44 | print(fred.name, fred.age) 45 | 46 | if __name__ == "__main__": 47 | main() 48 | 49 | #### EOF ###################################################################### 50 | -------------------------------------------------------------------------------- /examples/ex3_simple_view_web.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to serve a simple jigna view over the web. 3 | """ 4 | 5 | #### Imports #### 6 | from __future__ import print_function 7 | 8 | from tornado.ioloop import IOLoop 9 | from traits.api import HasTraits, Int, Str 10 | from jigna.web_app import WebApp 11 | from jigna.template import Template 12 | 13 | #### Domain model #### 14 | 15 | class Person(HasTraits): 16 | name = Str 17 | age = Int 18 | 19 | #### UI layer #### 20 | 21 | body_html = """ 22 |
23 | Name:
24 | Age: 25 |
26 | """ 27 | 28 | template = Template(body_html=body_html) 29 | 30 | #### Entry point #### 31 | 32 | def main(): 33 | # Start the tornado ioloop application 34 | ioloop = IOLoop.instance() 35 | 36 | # Instantiate the domain model 37 | fred = Person(name='Fred', age=42) 38 | 39 | # Create a web app serving the view with the domain model added to its 40 | # context. 41 | app = WebApp(template=template, context={'person': fred}) 42 | app.listen(8000) 43 | 44 | # Start serving the web app on port 8000. 45 | # 46 | # Point your web browser to http://localhost:8000/ to connect to this jigna 47 | # web app. Any operation performed on the client directly update the 48 | # model attributes on the server. 49 | print('Serving on port 8000...') 50 | ioloop.start() 51 | 52 | if __name__ == "__main__": 53 | main() 54 | 55 | #### EOF ###################################################################### 56 | -------------------------------------------------------------------------------- /examples/ex4_simple_view_sans_traits.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows the ability to view generic Python objects (not just 3 | HasTraits objects) in HTML using Jigna. The data binding will only be one-way 4 | in this case, i.e. from the UI to the model. 5 | """ 6 | 7 | #### Imports #### 8 | from __future__ import print_function 9 | 10 | from jigna.api import HTMLWidget, Template 11 | from jigna.qt import QtGui 12 | 13 | #### Domain model #### 14 | 15 | class Person(object): 16 | def __init__(self, name, age): 17 | self.name = name 18 | self.age = age 19 | 20 | #### UI layer #### 21 | 22 | body_html = """ 23 |
24 | Name: 25 | Age: 26 |
27 | """ 28 | 29 | template = Template(body_html=body_html) 30 | 31 | #### Entry point #### 32 | 33 | def main(): 34 | # Start the Qt application 35 | app = QtGui.QApplication([]) 36 | 37 | # Instantiate the domain model 38 | fred = Person(name='Fred', age=42) 39 | 40 | # Create the jigna based HTML widget which renders the given HTML template 41 | # with the given context. 42 | # 43 | # This will behave as a static page since we don't have the traits 44 | # machinery here to reflect model updates in the view. 45 | widget = HTMLWidget(template=template, context={'person': fred}) 46 | widget.show() 47 | 48 | # Start the event loop 49 | app.exec_() 50 | 51 | # Check the values after the UI is closed 52 | print(fred.name, fred.age) 53 | 54 | if __name__ == "__main__": 55 | main() 56 | 57 | #### EOF ###################################################################### 58 | -------------------------------------------------------------------------------- /examples/ex5_data/Lena.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enthought/jigna/00192450515099d8abc74866ffe1e4f743fe2279/examples/ex5_data/Lena.png -------------------------------------------------------------------------------- /examples/ex5_data/color_change.js: -------------------------------------------------------------------------------- 1 | // find the caption element 2 | var element = document.getElementById('caption'); 3 | 4 | // add the 'green' class on mouseover... 5 | element.addEventListener('mouseover', function(){ 6 | element.className = 'green'; 7 | }); 8 | 9 | // ... and 'red' on mouseout 10 | element.addEventListener('mouseout', function(){ 11 | element.className = 'red'; 12 | }); 13 | -------------------------------------------------------------------------------- /examples/ex5_data/colors.css: -------------------------------------------------------------------------------- 1 | .red { 2 | color: red; 3 | } 4 | 5 | .green { 6 | color: green; 7 | } 8 | -------------------------------------------------------------------------------- /examples/ex5_user_resources.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to add additional resources like CSS, javascript 3 | and image files in your html by specifying a base url. 4 | """ 5 | 6 | #### Imports #### 7 | 8 | from traits.api import HasTraits, Int, Str 9 | from jigna.api import HTMLWidget, Template 10 | from jigna.qt import QtGui 11 | 12 | #### Domain model #### 13 | 14 | class Person(HasTraits): 15 | name = Str 16 | age = Int 17 | 18 | #### UI layer #### 19 | 20 | body_html = """ 21 |
22 |
23 |
24 | {{person.name}} - {{person.age}} years old 25 |
26 |
27 | 28 | 29 | 30 | """ 31 | 32 | # The base_url field specifies where to look when trying to get external 33 | # resources(defaults to an empty string, i.e. the current directory) 34 | template = Template(body_html=body_html, base_url='ex5_data/', 35 | recommended_size=(600, 600) 36 | ) 37 | 38 | #### Entry point #### 39 | 40 | def main(): 41 | # Start the Qt application 42 | app = QtGui.QApplication([]) 43 | 44 | # Instantiate the domain model 45 | lena = Person(name='Lena', age=28) 46 | 47 | # Create the jigna based HTML widget which renders the given HTML template 48 | # with the given context. 49 | widget = HTMLWidget(template=template, context={'person':lena}) 50 | widget.show() 51 | 52 | # Start the event loop. 53 | # 54 | # You should see that user resources like CSS, images and custom JS are 55 | # pulled in properly from the `user_resources_data` directory and displayed 56 | # in the view. 57 | app.exec_() 58 | 59 | if __name__ == "__main__": 60 | main() 61 | 62 | #### EOF ###################################################################### 63 | -------------------------------------------------------------------------------- /examples/ex6_model_updates.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example demonstrates how updates to the application model's attributes are 3 | reflected in the HTML view. 4 | """ 5 | 6 | #### Imports #### 7 | 8 | from traits.api import HasTraits, Str 9 | from jigna.api import HTMLWidget, Template 10 | from jigna.qt import QtGui 11 | from jigna.utils.gui import do_after 12 | 13 | #### Domain model #### 14 | 15 | class MOTD(HasTraits): 16 | message = Str 17 | 18 | def update_message(self, message): 19 | self.message = message 20 | 21 | #### UI layer #### 22 | 23 | body_html = """ 24 |
25 | Message of the day: {{motd.message}} 26 |
27 | """ 28 | 29 | template = Template(body_html=body_html) 30 | 31 | #### Entry point #### 32 | 33 | def main(): 34 | # Start the Qt application 35 | app = QtGui.QApplication([]) 36 | 37 | # Instantiate the domain model 38 | motd = MOTD(message="Explicit is better than implicit.") 39 | 40 | # Create the jigna based HTML widget which renders the given HTML template 41 | # with the given context. 42 | widget = HTMLWidget(template=template, context={'motd':motd}) 43 | widget.show() 44 | 45 | # Schedule an update to a model variable after 2.5 seconds. This update 46 | # will be reflected in the UI immediately. 47 | do_after(2500, motd.update_message, "Flat is better than nested.") 48 | 49 | # Start the event loop 50 | app.exec_() 51 | 52 | if __name__ == "__main__": 53 | main() 54 | 55 | #### EOF ###################################################################### 56 | -------------------------------------------------------------------------------- /examples/ex7_model_updates_web.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example demonstrates how to schedule updates to the application model in 3 | the web version. 4 | """ 5 | 6 | #### Imports #### 7 | from __future__ import print_function 8 | 9 | import datetime 10 | from jigna.web_app import WebApp 11 | from jigna.template import Template 12 | from tornado.ioloop import IOLoop 13 | from traits.api import HasTraits, Str 14 | 15 | #### Domain model #### 16 | 17 | class MOTD(HasTraits): 18 | message = Str 19 | 20 | def update_message(self, message): 21 | self.message = message 22 | 23 | #### UI layer #### 24 | 25 | body_html = """ 26 |
27 | Message of the day: {{motd.message}} 28 |
29 | """ 30 | 31 | template = Template(body_html=body_html) 32 | 33 | #### Entry point #### 34 | 35 | def main(): 36 | # Obtain the tornado ioloop object 37 | ioloop = IOLoop.instance() 38 | 39 | # Instantiate the domain model 40 | motd = MOTD(message="Explicit is better than implicit.") 41 | 42 | # Create a web app serving the view with the domain model added to its 43 | # context. 44 | app = WebApp(template=template, context={'motd':motd}) 45 | app.listen(8000) 46 | 47 | # Schedule an update to a model variable after 10 seconds. If the user's 48 | # browser is connected to the web app before 10 seconds, it will see the 49 | # change in the model variable happening. Otherwise, it will only see the 50 | # updated value. 51 | # 52 | # NOTE: The way to schedule a task in the future has been greatly improved 53 | # in Tornado 4.1.dev version. You can specify additional arguments to 54 | # methods etc. 55 | delay = datetime.timedelta(0, 10) 56 | ioloop.add_timeout(delay, 57 | lambda : motd.update_message("Flat is better than nested.") 58 | ) 59 | 60 | # Start serving the web app on port 8000. 61 | print('Serving on port 8000...') 62 | ioloop.start() 63 | 64 | if __name__ == "__main__": 65 | main() 66 | 67 | #### EOF ###################################################################### 68 | -------------------------------------------------------------------------------- /examples/ex8_instance_trait.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows two-way data binding on an `Instance` trait. 3 | """ 4 | 5 | #### Imports #### 6 | from __future__ import print_function 7 | 8 | from traits.api import HasTraits, Instance, Str 9 | from jigna.api import HTMLWidget, Template 10 | from jigna.qt import QtGui 11 | from jigna.utils.gui import do_after 12 | 13 | #### Domain model #### 14 | 15 | class Person(HasTraits): 16 | name = Str 17 | spouse = Instance('Person') 18 | 19 | def marry(self, person): 20 | self.spouse = person 21 | person.spouse = self 22 | 23 | #### UI layer #### 24 | 25 | body_html = """ 26 |
27 | Name:
28 | Spouse's name:
29 | Spouse's spouse's name: 30 |
31 | """ 32 | 33 | template = Template(body_html=body_html) 34 | 35 | 36 | #### Entry point #### 37 | 38 | def main(): 39 | # Start the Qt application 40 | app = QtGui.QApplication([]) 41 | 42 | # Instantiate the domain model 43 | fred = Person(name='Fred') 44 | wilma = Person(name='Wilma') 45 | 46 | # Create the jigna based HTML widget which renders the given HTML template 47 | # with the given context. 48 | widget = HTMLWidget(template=template, context={'person':fred}) 49 | widget.show() 50 | 51 | # Schedule some operations on the domain model. 52 | # 53 | # The operation should fill in the `spouse` field of the person and this 54 | # should be reflected in the UI. 55 | do_after(2500, fred.marry, wilma) 56 | 57 | # Start the event loop. 58 | # 59 | # Initially, the `spouse` field of the person is empty, so the fields 60 | # related to the spouse should be empty in the UI. 61 | app.exec_() 62 | 63 | # Check the final values of the instance 64 | print(fred.name, fred.spouse.name, wilma.name) 65 | 66 | if __name__ == "__main__": 67 | main() 68 | 69 | #### EOF ###################################################################### 70 | -------------------------------------------------------------------------------- /examples/ex9_list_of_instances.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows two-way data binding for the `List` traits whose items are of 3 | an instance (non-primitive) type. 4 | """ 5 | 6 | #### Imports #### 7 | from __future__ import print_function 8 | 9 | from traits.api import HasTraits, Instance, Str, List 10 | from jigna.api import HTMLWidget, Template 11 | from jigna.qt import QtGui 12 | from jigna.utils.gui import do_after 13 | 14 | #### Domain model #### 15 | 16 | class Person(HasTraits): 17 | name = Str 18 | 19 | friends = List(Instance('Person')) 20 | 21 | #### UI layer #### 22 | 23 | # Looping over the list of instances. No surprises here. 24 | body_html = """ 25 |
26 | Name: {{person.name}}
27 | 28 | Friends: 29 |
30 | 31 |
32 |
33 | """ 34 | 35 | template = Template(body_html=body_html) 36 | 37 | #### Entry point #### 38 | 39 | def main(): 40 | # Start the Qt application 41 | app = QtGui.QApplication([]) 42 | 43 | # Instantiate the domain model 44 | fred = Person(name='Fred', friends=[Person(name='Dino')]) 45 | 46 | # Create the jigna based HTML widget which renders the given HTML template 47 | # with the given context. 48 | widget = HTMLWidget(template=template, context={'person':fred}) 49 | widget.show() 50 | 51 | # Schedule some operations on the list. 52 | # 53 | # We're trying to append and insert instances in the list in the future. 54 | # This should be reflected in the UI. 55 | do_after(2500, fred.friends.append, Person(name='Wilma')) 56 | do_after(5000, fred.friends.insert, 0, Person(name='Barney')) 57 | 58 | # Start the event loop 59 | app.exec_() 60 | 61 | # Check the final values of the list attribute 62 | print([friend.name for friend in fred.friends]) 63 | 64 | if __name__ == "__main__": 65 | main() 66 | 67 | #### EOF ###################################################################### 68 | -------------------------------------------------------------------------------- /examples/examples_notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "", 4 | "signature": "sha256:1ecc3704bc4bd7a202dfcaa8fcbd15cacb2d223a8cdc38bafa259eb55abd91bc" 5 | }, 6 | "nbformat": 3, 7 | "nbformat_minor": 0, 8 | "worksheets": [ 9 | { 10 | "cells": [ 11 | { 12 | "cell_type": "code", 13 | "collapsed": false, 14 | "input": [ 15 | "from jigna.utils.notebook import display_jigna" 16 | ], 17 | "language": "python", 18 | "metadata": {}, 19 | "outputs": [] 20 | }, 21 | { 22 | "cell_type": "heading", 23 | "level": 3, 24 | "metadata": {}, 25 | "source": [ 26 | "Simple View example" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "collapsed": false, 32 | "input": [ 33 | "from simple_view import Person, template\n", 34 | "\n", 35 | "fred = Person(name='fred', age=42)\n", 36 | "display_jigna(context={'person':fred}, template=template)" 37 | ], 38 | "language": "python", 39 | "metadata": {}, 40 | "outputs": [] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "collapsed": false, 45 | "input": [ 46 | "fred.name" 47 | ], 48 | "language": "python", 49 | "metadata": {}, 50 | "outputs": [] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "collapsed": false, 55 | "input": [ 56 | "wilma = Person(name='wilma', age=41)\n", 57 | "display_jigna(context={'person': wilma}, template=template)" 58 | ], 59 | "language": "python", 60 | "metadata": {}, 61 | "outputs": [] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "collapsed": false, 66 | "input": [ 67 | "wilma.name" 68 | ], 69 | "language": "python", 70 | "metadata": {}, 71 | "outputs": [] 72 | }, 73 | { 74 | "cell_type": "heading", 75 | "level": 3, 76 | "metadata": {}, 77 | "source": [ 78 | "Interactive matplotlib on the web" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "collapsed": false, 84 | "input": [ 85 | "from matplotlib_web import DomainModel, PlotController, template\n", 86 | "\n", 87 | "domain_model = DomainModel(scaling_factor=15)\n", 88 | "plot_controller = PlotController(domain_model=domain_model)\n", 89 | "\n", 90 | "display_jigna(context={'domain_model': domain_model, 'plot_controller': plot_controller}, template=template, size=(1000, 700))" 91 | ], 92 | "language": "python", 93 | "metadata": {}, 94 | "outputs": [] 95 | }, 96 | { 97 | "cell_type": "code", 98 | "collapsed": false, 99 | "input": [ 100 | "domain_model.scaling_factor" 101 | ], 102 | "language": "python", 103 | "metadata": {}, 104 | "outputs": [] 105 | }, 106 | { 107 | "cell_type": "heading", 108 | "level": 3, 109 | "metadata": {}, 110 | "source": [ 111 | "Mayavi on the web" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "collapsed": false, 117 | "input": [ 118 | "from mayavi_webgl_demo import Plotter3D, template\n", 119 | "\n", 120 | "plotter = Plotter3D(expression='x*x*0.5 + y*y + z*z*2.0', n_contour=4)\n", 121 | "\n", 122 | "display_jigna(context={'plotter': plotter}, template=template)" 123 | ], 124 | "language": "python", 125 | "metadata": {}, 126 | "outputs": [] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "collapsed": false, 131 | "input": [], 132 | "language": "python", 133 | "metadata": {}, 134 | "outputs": [] 135 | } 136 | ], 137 | "metadata": {} 138 | } 139 | ] 140 | } -------------------------------------------------------------------------------- /jigna/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.10.2.dev0' 2 | -------------------------------------------------------------------------------- /jigna/api.py: -------------------------------------------------------------------------------- 1 | from .template import Template 2 | from .vue_template import VueTemplate 3 | from .core.concurrent import Future 4 | from .html_widget import HTMLWidget 5 | 6 | # Wrapping the WebApp import so that you can use jigna even if you don't have 7 | # tornado install 8 | try: 9 | from .web_app import WebApp 10 | except ImportError: 11 | pass 12 | -------------------------------------------------------------------------------- /jigna/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enthought/jigna/00192450515099d8abc74866ffe1e4f743fe2279/jigna/core/__init__.py -------------------------------------------------------------------------------- /jigna/core/interoperation.py: -------------------------------------------------------------------------------- 1 | # 2 | # Jigna product code 3 | # 4 | # (C) Copyright 2013 Enthought, Inc., Austin, TX 5 | # All right reserved. 6 | # 7 | 8 | # Standard library imports. 9 | import inspect 10 | import logging 11 | 12 | # System library imports. 13 | from jigna.qt import QtCore 14 | 15 | # Logger. 16 | logger = logging.getLogger(__name__) 17 | 18 | def create_js_object_wrapper(callbacks=[], parent=None): 19 | """ Create a JS object wrapper containing the given callbacks as its 20 | methods. 21 | 22 | Note: Set the parent (setParent()) of the returned QObject to make sure 23 | it is destroyed when the parent is destroyed, or manually destroy the 24 | object when it is no longer needed (such as when the `window` js object 25 | is cleared or the webview/webpage/webframe object is destroyed); 26 | else stray references to the object may cause memory leaks. 27 | 28 | """ 29 | # Create the container class dict. 30 | class_dict = {} 31 | 32 | # Create the container callback slots, these need to be defined on the 33 | # class (associated with the QMetaObject). 34 | slot_map = {} 35 | for _callback in callbacks: 36 | name, callback = _callback 37 | 38 | wrapped = wrap_func(callback, name) 39 | if wrapped: 40 | class_dict[name] = wrapped 41 | slot_map[wrapped.name] = callback 42 | else: 43 | logger.error('Callback %r is not translatable to JavaScript', name) 44 | 45 | # Create the container class. 46 | container_class = type( 47 | 'CustomPythonContainer', (_PythonContainer, QtCore.QObject,), class_dict 48 | ) 49 | qobj = container_class(parent=parent, slot_map=slot_map) 50 | 51 | return qobj 52 | 53 | def wrap_func(func, name=None): 54 | """ Wraps a Python callable as a Qt Slot. 55 | """ 56 | try: 57 | args = inspect.getargspec(func).args 58 | except TypeError: 59 | return None 60 | 61 | if args and args[0] in ('self', 'cls'): 62 | args.pop(0) 63 | types = ['QVariant'] * len(args) 64 | 65 | slot_key = name if name is not None else 'func_' + hex(id(func)) 66 | 67 | @QtCore.Slot(*types, name=name, result='QVariant') 68 | def wrapped(self, *args): 69 | return self._slot_map[slot_key](*args) 70 | wrapped.name = slot_key 71 | return wrapped 72 | 73 | #### Private protocol ######################################################### 74 | 75 | class _PythonContainer(QtCore.QObject): 76 | """ Container class for python object to be exposed to js. """ 77 | def __init__(self, parent=None, slot_map=None): 78 | super(_PythonContainer, self).__init__(parent) 79 | 80 | # slot_name: callable 81 | self._slot_map = slot_map -------------------------------------------------------------------------------- /jigna/core/network_access.py: -------------------------------------------------------------------------------- 1 | # 2 | # Jigna product code 3 | # 4 | # (C) Copyright 2013 Enthought, Inc., Austin, TX 5 | # All right reserved. 6 | # 7 | 8 | # Standard library imports. 9 | try: 10 | from __builtin__ import unicode 11 | except ImportError: 12 | from builtins import str as unicode 13 | 14 | import logging 15 | import sys 16 | import threading 17 | from io import StringIO 18 | 19 | # System library imports. 20 | from jigna.qt import QtCore, QtNetwork, qt_api, QT_API_PYQT5 21 | 22 | # Logger. 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | class ProxyAccessManager(QtNetwork.QNetworkAccessManager): 27 | """ A QNetworkAccessManager subclass which proxies requests for a set of 28 | hosts and schemes. 29 | """ 30 | def __init__(self, root_paths={}, hosts={}): 31 | """ root_paths: Mapping of root paths to WSGI callables. 32 | hosts: Mapping of hosts to WSGI callables. 33 | """ 34 | super(ProxyAccessManager, self).__init__() 35 | self.root_paths = root_paths 36 | self.hosts = hosts 37 | 38 | def get_url_handler(self, url): 39 | """ Returns the WSGI callable to be used for specified url. 40 | """ 41 | handler = self.hosts.get(url.host()) 42 | 43 | if handler is None: 44 | for root, _handler in self.root_paths.items(): 45 | if url.path().split('/')[1] == root: 46 | handler = _handler 47 | 48 | return handler 49 | 50 | def inject(self, webview): 51 | """ Replace the old QNetworkAccessManager instance with this instance. 52 | """ 53 | old_manager = webview.page().networkAccessManager() 54 | 55 | self.setCache(old_manager.cache()) 56 | self.setCookieJar(old_manager.cookieJar()) 57 | self.setProxy(old_manager.proxy()) 58 | self.setProxyFactory(old_manager.proxyFactory()) 59 | webview.page().setNetworkAccessManager(self) 60 | 61 | ########################################################################### 62 | # QNetworkAccessManager interface 63 | ########################################################################### 64 | 65 | def createRequest(self, operation, request, data): 66 | """ Create a ProxyReply request if url handler is provided by 67 | `hosts`, else defer to the original QNetworkAccessManager 68 | `createRequest` method. 69 | """ 70 | url = request.url() 71 | handler = self.get_url_handler(url) 72 | if handler is not None: 73 | data_str = data and data.readAll() or '' 74 | return ProxyReply(self, url, operation, request, data_str, handler) 75 | 76 | # Default case, let superclass handle normal web access 77 | return super(ProxyAccessManager, self).createRequest( 78 | operation, request, data 79 | ) 80 | 81 | 82 | class ProxyReply(QtNetwork.QNetworkReply): 83 | """ QNetworkReply subclass to send a specific request to local wsgi app. 84 | """ 85 | def __init__(self, parent, url, operation, req, data, handler): 86 | """ handler is the wsgi app """ 87 | 88 | super(ProxyReply, self).__init__(parent) 89 | 90 | self.setRequest(req) 91 | self.setOperation(operation) 92 | self.setUrl(url) 93 | 94 | self.req_data = data 95 | self.handler = handler 96 | 97 | self.buffer = b'' 98 | self._buflock = threading.Lock() 99 | self.aborted = False 100 | 101 | self.open(self.ReadOnly) 102 | 103 | self._worker = ProxyReplyWorker(self) 104 | self._worker.start() 105 | 106 | # Handle synchronous requests (webkit sync ajax requests) 107 | # req.Attribute.QSynchronousHttpNetworkReply may not be defined for 108 | # pyside compiled with qt 4.7 but still works with qt 4.8 109 | # QSynchronousHttpNetworkReply = DownloadBufferAttribute + 1 = 16 110 | if req.attribute(req.Attribute(16)): 111 | self._worker.wait() 112 | 113 | ########################################################################### 114 | # QNetworkReply interface 115 | ########################################################################### 116 | 117 | def abort(self): 118 | if not self.aborted: 119 | self.aborted = True 120 | self.setError(self.OperationCanceledError, 121 | 'Request Aborted') 122 | 123 | def bytesAvailable(self): 124 | return super(ProxyReply, self).bytesAvailable() + len(self.buffer) 125 | 126 | def isSequential(self): 127 | return True 128 | 129 | def readData(self, maxSize): 130 | with self._buflock: 131 | data, self.buffer = self.buffer[:maxSize], self.buffer[maxSize:] 132 | return data 133 | 134 | 135 | class ProxyReplyWorker(QtCore.QThread): 136 | """ Worker thread to fetch urls for QNetworkProxy. """ 137 | 138 | # Signals to forward to ProxyReply 139 | metaDataChanged = QtCore.Signal() 140 | readyRead = QtCore.Signal() 141 | finished = QtCore.Signal() 142 | 143 | OPERATIONS = {QtNetwork.QNetworkAccessManager.GetOperation: 'GET', 144 | QtNetwork.QNetworkAccessManager.PostOperation: 'POST',} 145 | 146 | def __init__(self, reply, parent=None): 147 | super(ProxyReplyWorker, self).__init__(parent) 148 | self.reply = reply 149 | self.metaDataChanged.connect(self.reply.metaDataChanged) 150 | self.readyRead.connect(self.reply.readyRead) 151 | self.finished.connect(self.reply.finished) 152 | 153 | ########################################################################### 154 | # QThread interface. 155 | ########################################################################### 156 | 157 | def run(self): 158 | """ handles the request by acting as a WSGI forwarding server. """ 159 | reply = self.reply 160 | url = reply.url() 161 | req = reply.request() 162 | 163 | if qt_api == QT_API_PYQT5: 164 | query_string = url.query(QtCore.QUrl.FullyEncoded) 165 | else: 166 | query_string = url.encodedQuery() 167 | # WSGI environ variables 168 | env = { 169 | 'REQUEST_METHOD': self.OPERATIONS[reply.operation()], 170 | 'SCRIPT_NAME': '', 171 | 'PATH_INFO': url.path(), 172 | 'SERVER_NAME': url.host(), 173 | 'SERVER_PORT': '80', 174 | 'SERVER_PROTOCOL': 'HTTP/1.1', 175 | 'QUERY_STRING': str(query_string), 176 | 'wsgi.version': (1, 0), 177 | 'wsgi.url_scheme': url.scheme(), 178 | 'wsgi.input': StringIO(unicode(reply.req_data)), 179 | 'wsgi.errors': sys.stderr, 180 | 'wsgi.multithread': False, 181 | 'wsgi.multiprocess': True, 182 | 'wsgi.run_once': False, 183 | } 184 | 185 | # Set WSGI HTTP request headers 186 | for head_name in req.rawHeaderList(): 187 | env_name = 'HTTP_' + head_name.data().decode().replace('-','_').upper() 188 | head_val = req.rawHeader(head_name) 189 | env[env_name] = head_val.data() 190 | 191 | try: 192 | local_buf = [] 193 | local_buf_len = 0 194 | for read in reply.handler(env, self._start_response): 195 | if reply.aborted: 196 | return 197 | if not isinstance(read, bytes): 198 | read = str(read).encode('utf8') 199 | local_buf.append(read) 200 | local_buf_len += len(read) 201 | if local_buf_len >= 8192: 202 | # Do not write to buffer on every read, app is slowed down 203 | # due to lock contention 204 | with reply._buflock: 205 | reply.buffer += b''.join(local_buf) 206 | local_buf = [] 207 | local_buf_len = 0 208 | self.readyRead.emit() 209 | with reply._buflock: 210 | reply.buffer += b''.join(local_buf) 211 | 212 | except Exception as e: 213 | if reply.aborted: 214 | return 215 | reply.setAttribute( 216 | QtNetwork.QNetworkRequest.HttpStatusCodeAttribute, 500 217 | ) 218 | reply.setAttribute( 219 | QtNetwork.QNetworkRequest.HttpReasonPhraseAttribute, 220 | 'Internal Error' 221 | ) 222 | with reply._buflock: 223 | reply.buffer += b'WSGI Proxy "Server" Error.\n' + str(e) 224 | finally: 225 | self.readyRead.emit() 226 | self.finished.emit() 227 | 228 | ########################################################################### 229 | # Private interface. 230 | ########################################################################### 231 | 232 | def _start_response(self, status, response_headers): 233 | """ WSGI start_response callable. """ 234 | code, reason = status.split(' ', 1) 235 | self.reply.setAttribute( 236 | QtNetwork.QNetworkRequest.HttpStatusCodeAttribute, int(code) 237 | ) 238 | self.reply.setAttribute( 239 | QtNetwork.QNetworkRequest.HttpReasonPhraseAttribute, reason 240 | ) 241 | for name, value in response_headers: 242 | self.reply.setRawHeader(name, str(value)) 243 | 244 | self.metaDataChanged.emit() 245 | -------------------------------------------------------------------------------- /jigna/core/proxy_qwebview.py: -------------------------------------------------------------------------------- 1 | # 2 | # Jigna product code 3 | # 4 | # (C) Copyright 2013 Enthought, Inc., Austin, TX 5 | # All right reserved. 6 | # 7 | 8 | # Standard library imports. 9 | import logging 10 | from os.path import abspath, dirname, join 11 | 12 | # Local imports. 13 | from jigna.core.interoperation import create_js_object_wrapper 14 | from jigna.core.network_access import ProxyAccessManager 15 | from jigna.qt import QtCore, QtWidgets, QtWebKit, QtWebKitWidgets 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | class ProxyQWebView(QtWebKitWidgets.QWebView): 21 | 22 | DISABLED_ACTIONS = [ 23 | QtWebKitWidgets.QWebPage.OpenLinkInNewWindow, 24 | QtWebKitWidgets.QWebPage.DownloadLinkToDisk, 25 | QtWebKitWidgets.QWebPage.OpenImageInNewWindow, 26 | QtWebKitWidgets.QWebPage.OpenFrameInNewWindow, 27 | QtWebKitWidgets.QWebPage.DownloadImageToDisk, 28 | QtWebKitWidgets.QWebPage.Reload, 29 | QtWebKitWidgets.QWebPage.Back 30 | ] 31 | 32 | def __init__( 33 | self, parent=None, python_namespace=None, callbacks=[], 34 | debug=True, hosts={} 35 | ): 36 | super(ProxyQWebView, self).__init__(parent) 37 | 38 | self._page = ProxyQWebPage() 39 | self.setPage(self._page) 40 | 41 | # Connect JS with python. 42 | self.expose_python_namespace(python_namespace, callbacks) 43 | 44 | # Install custom access manager to delegate requests to custom WSGI 45 | # hosts. 46 | self._access_manager = ProxyAccessManager(hosts=hosts) 47 | self._page.setNetworkAccessManager(self._access_manager) 48 | 49 | # Disable some actions 50 | for action in self.DISABLED_ACTIONS: 51 | self.pageAction(action).setVisible(False) 52 | 53 | # Setup debug flag 54 | self._page.settings().setAttribute( 55 | QtWebKit.QWebSettings.DeveloperExtrasEnabled, debug 56 | ) 57 | 58 | # Set sizing policy 59 | self.setSizePolicy( 60 | QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding 61 | ) 62 | 63 | def execute_js(self, js): 64 | """ Execute JavaScript synchronously. 65 | 66 | Warning: under most circumstances, this method should not be called 67 | when the page is loading. 68 | 69 | """ 70 | frame = self._page.mainFrame() 71 | result = frame.evaluateJavaScript(js) 72 | result = self._apply_null_fix(result) 73 | 74 | return result 75 | 76 | def expose_python_namespace(self, python_namespace, callbacks): 77 | """ Exposes the given python namespace to Javascript. 78 | 79 | Javascript can access the given list of callbacks as if they were 80 | methods on the object described by the python namespace. 81 | 82 | python_namespace: str: 83 | Namespace to expose to the JS world. This creates an object of the 84 | same name and attaches it to window frame. 85 | 86 | callbacks: [method_name: callable]: 87 | This list of callbacks is what is exposed to the JS world via the 88 | given python namespace. 89 | 90 | Usage: 91 | ------ 92 | 93 | For example, doing this:: 94 | 95 | expose_python_namespace('python', ['say_hello', say_hello]) 96 | 97 | will create a window level object on the JS side which looks like 98 | this:: 99 | 100 | window.python.say_hello == 101 | 102 | """ 103 | frame = self._page.mainFrame() 104 | js_wrapper = create_js_object_wrapper(callbacks=callbacks,parent=frame) 105 | frame.javaScriptWindowObjectCleared.connect( 106 | lambda: self._on_js_window_cleared(python_namespace, js_wrapper) 107 | ) 108 | 109 | def _on_js_window_cleared(self, namespace, js_wrapper): 110 | frame = self._page.mainFrame() 111 | frame.addToJavaScriptWindowObject(namespace, js_wrapper) 112 | 113 | def setUrl(self, url): 114 | """ Reimplemented to make sure that when we return, the DOM is ready to 115 | use. 116 | 117 | Based on the local event loop approach described here: 118 | http://doc.qt.digia.com/qq/qq27-responsive-guis.html#waitinginalocaleventloop 119 | """ 120 | event_loop = QtCore.QEventLoop() 121 | page = self._page 122 | loaded = [False] 123 | 124 | def on_load(ok): 125 | loaded[0] = True 126 | event_loop.quit() 127 | 128 | page.loadFinished.connect(on_load) 129 | try: 130 | super(ProxyQWebView, self).setUrl(QtCore.QUrl(url)) 131 | if not loaded[0]: 132 | event_loop.exec_() 133 | finally: 134 | page.loadFinished.disconnect(on_load) 135 | 136 | #### Private protocol ##################################################### 137 | 138 | @staticmethod 139 | def _apply_null_fix(obj): 140 | """ Makes sure that None objects coming from Qt bridge are actually 141 | None. 142 | 143 | We need this because NoneType objects coming from PyQt are of a 144 | `QPyNullVariant` type, not None. This method converts such objects to 145 | the standard None type. 146 | 147 | """ 148 | if isinstance(obj, getattr(QtCore, 'QPyNullVariant', type(None))): 149 | return None 150 | 151 | return obj 152 | 153 | 154 | class ProxyQWebPage(QtWebKitWidgets.QWebPage): 155 | """ Overridden to open external links in a web browser. 156 | 157 | Source: http://www.expobrain.net/2012/03/01/open-urls-in-external-browser-by-javascript-in-webkit/ 158 | """ 159 | 160 | def acceptNavigationRequest(self, frame, request, type): 161 | # Checking this is same as checking if the client side tag making 162 | # the HTTP request had target="_blank" as an attribute. 163 | if frame is None: 164 | import webbrowser 165 | webbrowser.open_new(request.url().toString()) 166 | 167 | return False 168 | 169 | else: 170 | return super(ProxyQWebPage, self).acceptNavigationRequest( 171 | frame, request, type 172 | ) 173 | 174 | def createWindow(self, *args, **kwargs): 175 | return ProxyQWebPage() 176 | 177 | if __name__ == '__main__': 178 | app = QtWidgets.QApplication([]) 179 | w = ProxyQWebView() 180 | w.show() 181 | w.raise_() 182 | w.load(QtCore.QUrl('http://www.google.com/')) 183 | app.exec_() 184 | -------------------------------------------------------------------------------- /jigna/core/wsgi.py: -------------------------------------------------------------------------------- 1 | # Standard library imports 2 | import threading 3 | import mimetypes 4 | import logging 5 | from os.path import exists, join, sep 6 | 7 | # Enthought library imports 8 | from traits.api import HasTraits, Str, Dict, Directory, on_trait_change 9 | 10 | mimeLock = threading.Lock() 11 | mimeInitialized = False 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | def guess_type(path): 17 | """ Thread-safe wrapper around the @#%^$$# non-thread safe mimetypes module. NEVER 18 | call mimetypes directly. 19 | 20 | """ 21 | 22 | global mimeLock 23 | global mimeInitialized 24 | 25 | if not mimeInitialized: 26 | with mimeLock: 27 | if not mimeInitialized: 28 | mimetypes.init() 29 | 30 | # On Windows, the mime type for 'ttf' (True Type Fonts) isn't 31 | # recognized correctly. We, therefore, add the correct mime 32 | # type here. 33 | mimetypes.add_type('application/font-sfnt', '.ttf') 34 | 35 | mimeInitialized = True 36 | 37 | guessed = mimetypes.guess_type(path) 38 | 39 | return (guessed[0] or "", guessed[1] or "") 40 | 41 | 42 | class FileLoader(HasTraits): 43 | 44 | #: Root directory where it looks 45 | root = Directory 46 | 47 | #: A dictionary of overrides which holds canned responses for some special 48 | #: paths 49 | overrides = Dict 50 | 51 | #### WSGI protocol ######################################################## 52 | 53 | def __call__(self, env, start_response): 54 | 55 | # Clean up path and handle Windows paths 56 | path = env['PATH_INFO'].strip('/').replace('/', sep) 57 | 58 | # Check if it is handled by one of the overrides 59 | if self.overrides.get(path) is not None: 60 | start_response( 61 | '200 OK', [('Content-Type', '; '.join(guess_type(path)))] 62 | ) 63 | return [self.overrides[path]] 64 | 65 | # Continue, if the path wasn't handled by canned responses for special 66 | # paths 67 | path = join(self.root, path) 68 | if not exists(path): 69 | start_response('404 File not found', []) 70 | return [""] 71 | 72 | else: 73 | start_response( 74 | '200 OK', [('Content-Type', '; '.join(guess_type(path)))] 75 | ) 76 | with open(path, 'rb') as f: 77 | response = f.read() 78 | return [response] 79 | -------------------------------------------------------------------------------- /jigna/html_widget.py: -------------------------------------------------------------------------------- 1 | # 2 | # Enthought product code 3 | # 4 | # (C) Copyright 2013 Enthought, Inc., Austin, TX 5 | # All right reserved. 6 | # 7 | """ A Qt based HTML widget to render a jigna UI. """ 8 | 9 | 10 | from __future__ import absolute_import 11 | 12 | # Standard library. 13 | import os 14 | from os.path import join 15 | 16 | # Local library. 17 | from .qt import QtCore, QtWidgets 18 | from .qt_server import QtServer 19 | 20 | 21 | class HTMLWidget(QtWidgets.QWidget): 22 | """ A Qt based HTML widget to render a jigna UI. """ 23 | 24 | #### 'object' protocol #################################################### 25 | 26 | def __init__( 27 | self, parent=None, window_flags=QtCore.Qt.Widget, context=None, 28 | template=None, debug=False 29 | ): 30 | """ Constructor. """ 31 | 32 | super(HTMLWidget, self).__init__(parent, window_flags) 33 | 34 | # Private protocol. 35 | self._context = context 36 | self._template = template 37 | self._debug = debug 38 | self._server = self._create_server() 39 | 40 | # fixme: This has to be a public attribute for testing *only*. 41 | self.webview = self._server.webview 42 | 43 | # Set the widget layout. 44 | self.setLayout(QtWidgets.QVBoxLayout()) 45 | self.layout().setContentsMargins(0, 0, 0, 0) 46 | self.layout().addWidget(self._server.webview) 47 | self.resize(*template.recommended_size) 48 | 49 | def execute_js(self, js): 50 | """ Execute the given JS string in the HTML widget. """ 51 | 52 | return self._server.webview.execute_js(js) 53 | 54 | #### 'QWidget' protocol ################################################### 55 | 56 | def closeEvent(self, event): 57 | """ Called when there is a request to close the widget. """ 58 | 59 | if event.isAccepted(): 60 | self._server.shutdown() 61 | 62 | #### Private protocol ##################################################### 63 | 64 | def _create_server(self): 65 | """ Create a jigna Qt server to serve the domain models. """ 66 | 67 | server = QtServer( 68 | base_url = join(os.getcwd(), self._template.base_url), 69 | html = self._template.html, 70 | context = self._context, 71 | debug = self._debug 72 | ) 73 | 74 | return server 75 | -------------------------------------------------------------------------------- /jigna/js/build.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var util = require('util'); 3 | var path = require('path'); 4 | 5 | function concatenate(options) { 6 | var base_url = options.base_url || ""; 7 | var files = options.src; 8 | var dest = options.dest; 9 | 10 | var concat_str = ""; 11 | 12 | // Start by wrapping everything inside an anonymous function 13 | if (options.wrap === true) { 14 | concat_str = concat_str + "(function (){\n\n"; 15 | } 16 | 17 | // Concatenate all the files 18 | for (var i=0; i 0) { 113 | // Delete the proxy indices at the end. 114 | for (index=end; index > (end-removed) ; index--) { 115 | delete proxy[index-1]; 116 | } 117 | var to_remove = []; 118 | for (index=info.start; index 0; index--) { 123 | cache.splice(to_remove[index-1], 1); 124 | } 125 | } else { 126 | // When nothing is removed, just update the cache entries. 127 | for (var i=0; i < added.length; i++) { 128 | index = info.start + i*info.step; 129 | cache[index] = new jigna._SavedData(added.data[i]); 130 | } 131 | } 132 | } else { 133 | // This is not an extended slice. 134 | var splice_args = [info.index, info.removed].concat( 135 | info.added.data.map(function(x) {return new jigna._SavedData(x);}) 136 | ); 137 | 138 | var extra = splice_args.length - 2 - splice_args[1]; 139 | var cache = proxy.__cache__; 140 | var end = cache.length; 141 | if (extra < 0) { 142 | for (var index=end; index > (end+extra) ; index--) { 143 | delete proxy[index-1]; 144 | } 145 | } else { 146 | for (var index=0; index < extra; index++){ 147 | this._add_item_attribute(proxy, end+index); 148 | } 149 | } 150 | cache.splice.apply(cache, splice_args); 151 | } 152 | }; 153 | 154 | 155 | // Common for list and dict proxies //////////////////////////////////////////// 156 | 157 | jigna.AsyncProxyFactory.prototype._add_item_attribute = function(proxy, index){ 158 | var descriptor, get, set; 159 | 160 | get = function() { 161 | // In here, 'this' refers to the proxy! 162 | var value = this.__cache__[index]; 163 | if (value === undefined) { 164 | value = this.__client__.get_attribute(this, index); 165 | this.__cache__[index] = value; 166 | } else if (value instanceof jigna._SavedData) { 167 | value = this.__client__._unmarshal(value.data); 168 | this.__cache__[index] = value; 169 | } 170 | 171 | return value; 172 | }; 173 | 174 | set = function(value) { 175 | // In here, 'this' refers to the proxy! 176 | this.__cache__[index] = value; 177 | this.__client__.set_item(this.__id__, index, value); 178 | }; 179 | 180 | descriptor = {enumerable:true, get:get, set:set, configurable:true}; 181 | Object.defineProperty(proxy, index, descriptor); 182 | }; 183 | -------------------------------------------------------------------------------- /jigna/js/src/app/event_target.js: -------------------------------------------------------------------------------- 1 | ///// EventTarget ///////////////////////////////////////////////////////////// 2 | // Copyright (c) 2010 Nicholas C. Zakas. All rights reserved. 3 | // MIT License 4 | /////////////////////////////////////////////////////////////////////////////// 5 | 6 | var EventTarget = function(){ 7 | this._listeners = {}; 8 | }; 9 | 10 | EventTarget.prototype = { 11 | 12 | constructor: EventTarget, 13 | 14 | add_listener: function(obj, event_name, listener, thisArg){ 15 | var id = this._to_id(obj); 16 | 17 | if (this._listeners[id] === undefined){ 18 | this._listeners[id] = {}; 19 | } 20 | 21 | if (this._listeners[id][event_name] === undefined) { 22 | this._listeners[id][event_name] = []; 23 | } 24 | 25 | this._listeners[id][event_name].push({thisArg: thisArg, listener: listener}); 26 | }, 27 | 28 | fire_event: function(obj, event){ 29 | var id = this._to_id(obj); 30 | 31 | if (typeof event == "string"){ 32 | event = { name: event }; 33 | } 34 | if (!event.target){ 35 | event.target = obj; 36 | } 37 | 38 | if (!event.name){ //falsy 39 | throw new Error("Event object missing 'name' property."); 40 | } 41 | 42 | if (this._listeners[id] === undefined) { 43 | return; 44 | } 45 | 46 | if (this._listeners[id][event.name] instanceof Array){ 47 | var listeners = this._listeners[id][event.name]; 48 | for (var i=0, len=listeners.length; i < len; i++){ 49 | listener = listeners[i].listener; 50 | thisArg = listeners[i].thisArg; 51 | listener.call(thisArg, event); 52 | } 53 | } 54 | }, 55 | 56 | remove_listener: function(obj, event_name, listener){ 57 | var id = this._to_id(obj); 58 | 59 | if (this._listeners[id][event_name] instanceof Array){ 60 | var listeners = this._listeners[id][event_name]; 61 | for (var i=0, len=listeners.length; i < len; i++){ 62 | if (listeners[i] === listener){ 63 | listeners.splice(i, 1); 64 | break; 65 | } 66 | } 67 | } 68 | }, 69 | 70 | //// Private protocol ///////////////////////////////////////////////////// 71 | 72 | _to_id: function(obj){ 73 | if (obj.__id__ !== undefined) { 74 | return obj.__id__; 75 | } 76 | else { 77 | return obj; 78 | } 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /jigna/js/src/app/jigna-angular.js: -------------------------------------------------------------------------------- 1 | // An AngularJS app running the jigna app 2 | 3 | // Namespace for the angular app of jigna 4 | jigna.angular = {}; 5 | 6 | jigna.angular.app = angular.module('jigna', []); 7 | 8 | // Add initialization function on module run time 9 | jigna.angular.app.run(['$rootScope', '$compile', function($rootScope, $compile){ 10 | 11 | // add the 'jigna' namespace to the $rootScope. This is done so that 12 | // any special jigna methods are easily available in directives like 13 | // ng-click, ng-mouseover etc. 14 | $rootScope.jigna = jigna; 15 | 16 | var add_to_scope = function(models){ 17 | for (var model_name in models) { 18 | $rootScope[model_name] = models[model_name]; 19 | } 20 | }; 21 | // add the existing models to the angular scope 22 | add_to_scope(jigna.models); 23 | 24 | // Start the $digest cycle on rootScope whenever anything in the 25 | // model object is changed. 26 | // 27 | // Since the $digest cycle essentially involves dirty checking of 28 | // all the watchers, this operation means that it will trigger off 29 | // new GET requests for each model attribute that is being used in 30 | // the registered watchers. 31 | jigna.add_listener('jigna', 'object_changed', function() { 32 | add_to_scope(jigna.models); 33 | 34 | if ($rootScope.$$phase === null){ 35 | $rootScope.$digest(); 36 | } 37 | }); 38 | 39 | }]); 40 | -------------------------------------------------------------------------------- /jigna/js/src/app/jigna-vue.js: -------------------------------------------------------------------------------- 1 | // A Horrible hack to update objects. This was gleaned from the vuejs 2 | // code. The problem we have is that vuejs cannot listen to changes to 3 | // model changes because we use getters/setters. Internally vue uses an 4 | // observer to notify dependent elements. We use the __ob__ attribute 5 | // to get the observer and call its `dep.notify()`, this makes 6 | // everything work really well. 7 | jigna.add_listener('jigna', 'object_changed', function (event) { 8 | var obj = event.object; 9 | if (obj && obj.__ob__) { 10 | obj.__ob__.dep.notify(); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /jigna/js/src/app/jigna.js: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // Jigna 3 | /////////////////////////////////////////////////////////////////////////////// 4 | 5 | // Namespace for all Jigna-related objects. 6 | var jigna = new EventTarget(); 7 | 8 | jigna.initialize = function(options) { 9 | options = options || {}; 10 | this.ready = $.Deferred(); 11 | this.debug = options.debug; 12 | this.async = options.async; 13 | this.client = options.async ? new jigna.AsyncClient() : new jigna.Client(); 14 | this.client.initialize(); 15 | return this.ready; 16 | }; 17 | 18 | jigna.models = {}; 19 | 20 | jigna.add_listener('jigna', 'model_added', function(event){ 21 | var models = event.data; 22 | for (var model_name in models) { 23 | jigna.models[model_name] = models[model_name]; 24 | } 25 | 26 | jigna.fire_event('jigna', 'object_changed'); 27 | }); 28 | 29 | jigna.threaded = function(obj, method_name, args) { 30 | args = Array.prototype.slice.call(arguments, 2); 31 | return this.client.call_instance_method_thread(obj.__id__, method_name, args); 32 | }; 33 | -------------------------------------------------------------------------------- /jigna/js/src/app/list_proxy.js: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // ListProxy 3 | /////////////////////////////////////////////////////////////////////////////// 4 | 5 | // ListProxy is handled separately because it has to do special handling 6 | // to behave as regular Javascript `Array` objects 7 | // See "Wrappers. Prototype chain injection" section in this article: 8 | // http://perfectionkills.com/how-ecmascript-5-still-does-not-allow-to-subclass-an-array/ 9 | 10 | jigna.ListProxy = function(type, id, client) { 11 | 12 | var arr = new jigna.SubArray(); 13 | 14 | // fixme: repetition of property definition 15 | Object.defineProperty(arr, '__type__', {value : type}); 16 | Object.defineProperty(arr, '__id__', {value : id}); 17 | Object.defineProperty(arr, '__client__', {value : client}); 18 | Object.defineProperty(arr, '__cache__', {value : [], writable: true}); 19 | 20 | // The state for each attribute can be 'busy' or undefined, if 'busy' it 21 | // implies that the server is waiting to receive the value. 22 | Object.defineProperty(arr, '__state__', {value : {}}); 23 | 24 | return arr; 25 | }; 26 | -------------------------------------------------------------------------------- /jigna/js/src/app/proxy.js: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // Proxy 3 | /////////////////////////////////////////////////////////////////////////////// 4 | 5 | jigna.Proxy = function(type, id, client) { 6 | // We use the '__attribute__' pattern to reduce the risk of name clashes 7 | // with the actuall attribute and methods on the object that we are a 8 | // proxy for. 9 | Object.defineProperty(this, '__type__', {value : type}); 10 | Object.defineProperty(this, '__id__', {value : id}); 11 | Object.defineProperty(this, '__client__', {value : client}); 12 | Object.defineProperty(this, '__cache__', {value : {}, writable: true}); 13 | 14 | // The state for each attribute can be 'busy' or undefined, if 'busy' it 15 | // implies that the server is waiting to receive the value. 16 | Object.defineProperty(this, '__state__', {value : {}}); 17 | }; 18 | -------------------------------------------------------------------------------- /jigna/js/src/app/qt_bridge.js: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // QtBridge (intra-process) 3 | /////////////////////////////////////////////////////////////////////////////// 4 | 5 | jigna.QtBridge = function(client, qt_bridge) { 6 | this.ready = new $.Deferred(); 7 | 8 | // Private protocol 9 | this._client = client; 10 | this._qt_bridge = qt_bridge; 11 | 12 | this.ready.resolve(); 13 | }; 14 | 15 | jigna.QtBridge.prototype.handle_event = function(jsonized_event) { 16 | /* Handle an event from the server. */ 17 | this._client.handle_event(jsonized_event); 18 | }; 19 | 20 | jigna.QtBridge.prototype.send_request = function(jsonized_request) { 21 | /* Send a request to the server and wait for the reply. */ 22 | 23 | result = this._qt_bridge.handle_request(jsonized_request); 24 | 25 | return result; 26 | }; 27 | 28 | jigna.QtBridge.prototype.send_request_async = function(jsonized_request) { 29 | /* A dummy async version of the send_request method. Since QtBridge is 30 | single process, this method indeed waits for the reply but presents 31 | a deferred API so that the AsyncClient can use it. Mainly for testing 32 | purposes only. */ 33 | 34 | var deferred = new $.Deferred(); 35 | 36 | deferred.resolve(this._qt_bridge.handle_request(jsonized_request)); 37 | 38 | return deferred.promise(); 39 | }; 40 | -------------------------------------------------------------------------------- /jigna/js/src/app/subarray.js: -------------------------------------------------------------------------------- 1 | // SubArray.js //////////////////////////////////////////////////////////////// 2 | // (C) Copyright Juriy Zaytsev 3 | // Source: 1. https://github.com/kangax/array_subclassing 4 | // 2. http://perfectionkills.com/how-ecmascript-5-still-does-not-allow- 5 | // to-subclass-an-array/ 6 | /////////////////////////////////////////////////////////////////////////////// 7 | 8 | var makeSubArray = (function(){ 9 | 10 | var MAX_SIGNED_INT_VALUE = Math.pow(2, 32) - 1, 11 | hasOwnProperty = Object.prototype.hasOwnProperty; 12 | 13 | function ToUint32(value) { 14 | return value >>> 0; 15 | } 16 | 17 | function getMaxIndexProperty(object) { 18 | var maxIndex = -1, isValidProperty; 19 | 20 | for (var prop in object) { 21 | 22 | // int conversion of the property 23 | int_prop = ToUint32(prop); 24 | 25 | isValidProperty = ( 26 | String(int_prop) === prop && 27 | int_prop !== MAX_SIGNED_INT_VALUE && 28 | hasOwnProperty.call(object, prop) 29 | ); 30 | 31 | if (isValidProperty && int_prop > maxIndex) { 32 | maxIndex = prop; 33 | } 34 | } 35 | return maxIndex; 36 | } 37 | 38 | return function(methods) { 39 | var length = 0; 40 | methods = methods || { }; 41 | 42 | methods.length = { 43 | get: function() { 44 | var maxIndexProperty = +getMaxIndexProperty(this); 45 | return Math.max(length, maxIndexProperty + 1); 46 | }, 47 | set: function(value) { 48 | var constrainedValue = ToUint32(value); 49 | if (constrainedValue !== +value) { 50 | throw new RangeError(); 51 | } 52 | for (var i = constrainedValue, len = this.length; i < len; i++) { 53 | delete this[i]; 54 | } 55 | length = constrainedValue; 56 | } 57 | }; 58 | 59 | methods.toString = { 60 | value: Array.prototype.join 61 | }; 62 | 63 | return Object.create(Array.prototype, methods); 64 | }; 65 | })(); 66 | 67 | jigna.SubArray = function() { 68 | var arr = makeSubArray(); 69 | 70 | if (arguments.length === 1) { 71 | arr.length = arguments[0]; 72 | } 73 | else { 74 | arr.push.apply(arr, arguments); 75 | } 76 | return arr; 77 | }; 78 | -------------------------------------------------------------------------------- /jigna/js/src/app/web_bridge.js: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // WebBridge 3 | /////////////////////////////////////////////////////////////////////////////// 4 | 5 | jigna.WebBridge = function(client) { 6 | this._client = client; 7 | 8 | // The jigna_server attribute can be set by a client to point to a 9 | // different Jigna server. 10 | var jigna_server = window['jigna_server']; 11 | if (jigna_server === undefined) { 12 | jigna_server = window.location.host; 13 | } 14 | this._server_url = 'http://' + jigna_server; 15 | 16 | var url = 'ws://' + jigna_server + '/_jigna_ws'; 17 | 18 | this._deferred_requests = {}; 19 | this._request_ids = []; 20 | for (var index=0; index < 1024; index++) { 21 | this._request_ids.push(index); 22 | } 23 | 24 | this._web_socket = new WebSocket(url); 25 | this.ready = new $.Deferred(); 26 | var bridge = this; 27 | this._web_socket.onopen = function() { 28 | bridge.ready.resolve(); 29 | }; 30 | this._web_socket.onmessage = function(event) { 31 | bridge.handle_event(event.data); 32 | }; 33 | }; 34 | 35 | jigna.WebBridge.prototype.handle_event = function(jsonized_event) { 36 | /* Handle an event from the server. */ 37 | var response = JSON.parse(jsonized_event); 38 | var request_id = response[0]; 39 | var jsonized_response = response[1]; 40 | if (request_id === -1) { 41 | this._client.handle_event(jsonized_response); 42 | } 43 | else { 44 | var deferred = this._pop_deferred_request(request_id); 45 | deferred.resolve(jsonized_response); 46 | } 47 | }; 48 | 49 | jigna.WebBridge.prototype.send_request = function(jsonized_request) { 50 | /* Send a request to the server and wait for the reply. */ 51 | 52 | var jsonized_response; 53 | 54 | $.ajax( 55 | { 56 | url : '/_jigna', 57 | type : 'GET', 58 | data : {'data': jsonized_request}, 59 | success : function(result) {jsonized_response = result;}, 60 | error : function(status, error) { 61 | console.warning("Error: " + error); 62 | }, 63 | async : false 64 | } 65 | ); 66 | 67 | return jsonized_response; 68 | }; 69 | 70 | jigna.WebBridge.prototype.send_request_async = function(jsonized_request) { 71 | /* Send a request to the server and do not wait and return a Promise 72 | which is resolved upon completion of the request. 73 | */ 74 | 75 | var deferred = new $.Deferred(); 76 | var request_id = this._push_deferred_request(deferred); 77 | var bridge = this; 78 | this.ready.done(function() { 79 | bridge._web_socket.send(JSON.stringify([request_id, jsonized_request])); 80 | }); 81 | return deferred.promise(); 82 | }; 83 | 84 | //// Private protocol ///////////////////////////////////////////////////// 85 | 86 | jigna.WebBridge.prototype._pop_deferred_request = function(request_id) { 87 | var deferred = this._deferred_requests[request_id]; 88 | delete this._deferred_requests[request_id]; 89 | this._request_ids.push(request_id); 90 | return deferred; 91 | }; 92 | 93 | jigna.WebBridge.prototype._push_deferred_request = function(deferred) { 94 | var id = this._request_ids.pop(); 95 | if (id === undefined) { 96 | console.error("In _push_deferred_request, request_id is undefined."); 97 | } 98 | this._deferred_requests[id] = deferred; 99 | return id; 100 | }; 101 | -------------------------------------------------------------------------------- /jigna/js/watcher.js: -------------------------------------------------------------------------------- 1 | var watchr = require('watchr'), 2 | exec = require('child_process').exec; 3 | 4 | var path = 'src/'; 5 | 6 | // Watch a directory or file 7 | console.log('Watching path:', path); 8 | 9 | // Watch a the given path for changes and execute the build command whenever there 10 | // is any change in the source 11 | watchr.watch({ 12 | path: path, 13 | listener: function(){ 14 | console.log("Reloading..."); 15 | 16 | var cmd = 'node build.js'; 17 | exec(cmd, function(error, stdout, stderr){ 18 | console.log(stdout); 19 | console.error(stderr); 20 | console.log("Done!"); 21 | }); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /jigna/qt.py: -------------------------------------------------------------------------------- 1 | """ Simple minimal compatibility code for using PyQt4 and PySide 2 | interchangeably only to the extent needed by jigna. 3 | 4 | """ 5 | 6 | QT_API_PYSIDE = 'pyside' 7 | QT_API_PYQT4 = 'pyqt' 8 | QT_API_PYQT5 = 'pyqt5' 9 | 10 | 11 | def load_pyside(): 12 | global qt_api 13 | global QtCore, QtGui, QtWidgets, QtNetwork, QtWebKit, QtWebKitWidgets 14 | qt_api = QT_API_PYSIDE 15 | from PySide import ( 16 | QtCore, QtGui, QtNetwork, QtWebKit, 17 | QtWebKit as QtWebKitWidgets, QtGui as QtWidgets) 18 | 19 | 20 | def load_pyqt5(): 21 | global qt_api 22 | global QtCore, QtGui, QtWidgets, QtNetwork, QtWebKit, QtWebKitWidgets 23 | qt_api = QT_API_PYQT5 24 | from PyQt5 import ( 25 | QtCore, QtGui, QtNetwork, QtWebKit, QtWebKitWidgets, QtWidgets) 26 | 27 | 28 | def load_pyqt4(): 29 | global qt_api 30 | global QtCore, QtGui, QtWidgets, QtNetwork, QtWebKit, QtWebKitWidgets 31 | 32 | qt_api = QT_API_PYQT4 33 | 34 | import sip 35 | sip.setapi('QDate', 2) 36 | sip.setapi('QDateTime', 2) 37 | sip.setapi('QString', 2) 38 | sip.setapi('QTextStream', 2) 39 | sip.setapi('QTime', 2) 40 | sip.setapi('QUrl', 2) 41 | sip.setapi('QVariant', 2) 42 | 43 | from PyQt4 import ( 44 | QtCore, QtGui, QtNetwork, QtWebKit, 45 | QtWebKit as QtWebKitWidgets, QtGui as QtWidgets) 46 | 47 | QtCore.Property = QtCore.pyqtProperty 48 | QtCore.Signal = QtCore.pyqtSignal 49 | QtCore.Slot = QtCore.pyqtSlot 50 | 51 | 52 | def main(): 53 | import os 54 | import sys 55 | 56 | if os.environ.get('QT_API') == QT_API_PYQT5 or 'PyQt5' in sys.modules: 57 | load_pyqt5() 58 | elif os.environ.get('QT_API') == QT_API_PYQT4 or 'PyQt4' in sys.modules: 59 | load_pyqt4() 60 | elif os.environ.get('QT_API') == QT_API_PYSIDE or 'PySide' in sys.modules: 61 | load_pyside() 62 | else: 63 | try: 64 | load_pyqt5() 65 | except ImportError: 66 | try: 67 | load_pyqt4() 68 | except ImportError: 69 | load_pyside() 70 | 71 | main() 72 | -------------------------------------------------------------------------------- /jigna/qt_server.py: -------------------------------------------------------------------------------- 1 | # 2 | # Enthought product code 3 | # 4 | # (C) Copyright 2013 Enthought, Inc., Austin, TX 5 | # All right reserved. 6 | # 7 | 8 | """ Qt implementations of the Jigna Server and Bridge. """ 9 | 10 | 11 | # Standard library. 12 | import json 13 | import os 14 | from os.path import abspath, dirname, join 15 | 16 | # Enthought library. 17 | from traits.api import Any, Bool, Str, Instance 18 | from traits.trait_notifiers import set_ui_handler 19 | 20 | # Jigna library. 21 | from jigna.core.proxy_qwebview import ProxyQWebView 22 | from jigna.core.wsgi import FileLoader 23 | from jigna.server import Bridge, Server 24 | from jigna.qt import QtWebKit 25 | from jigna.utils.gui import ui_handler 26 | 27 | #: Path to jigna.js file 28 | JIGNA_JS_FILE = join(abspath(dirname(__file__)), 'js', 'dist', 'jigna.js') 29 | JIGNA_VUE_JS_FILE = join( 30 | abspath(dirname(__file__)), 'js', 'dist', 'jigna-vue.js' 31 | ) 32 | 33 | 34 | class QtBridge(Bridge): 35 | """ Qt (via QWebkit) bridge implementation. """ 36 | 37 | #### 'Bridge' protocol #################################################### 38 | 39 | def send_event(self, event): 40 | """ Send an event. """ 41 | 42 | try: 43 | jsonized_event = json.dumps(event) 44 | except TypeError: 45 | return 46 | 47 | if self.webview is None: 48 | raise RuntimeError("WebView does not exist") 49 | 50 | else: 51 | # This looks weird but this is how we fake an event being 52 | # 'received' on the client side when using the Qt bridge! 53 | self.webview.execute_js( 54 | 'jigna.client.bridge.handle_event(%r);' % jsonized_event 55 | ) 56 | 57 | return 58 | 59 | #### 'QtBridge' protocol ################################################## 60 | 61 | #: The 'WebViewContainer' that contains the QtWebKit malarky. 62 | webview = Any 63 | 64 | 65 | class QtServer(Server): 66 | """ Qt (via QWebkit) server implementation. """ 67 | 68 | #### 'Server' protocol #################################################### 69 | 70 | def __init__(self, **traits): 71 | """ Initialize the Qt server. 72 | 73 | This simply configures the widget to serve the Python model. 74 | 75 | """ 76 | 77 | # This statement makes sure that when we dispatch traits events on the 78 | # 'ui' thread, it passes on those events through the Qt layer. 79 | set_ui_handler(ui_handler) 80 | 81 | super(QtServer, self).__init__(**traits) 82 | self.webview.setUrl(self.home_url) 83 | self._enable_qwidget_embedding() 84 | 85 | return 86 | 87 | #: Whether to switch on the debug flag or not. 88 | #: 89 | #: In debug mode, we show the QtWebkit inspect tools. 90 | debug = Bool(False) 91 | 92 | #: The trait change dispatch mechanism to use when traits change. 93 | trait_change_dispatch = Str('ui') 94 | 95 | def shutdown(self): 96 | """ Shutdown the server. 97 | 98 | Overridden to make sure we close up the QWebView. 99 | 100 | """ 101 | 102 | super(QtServer, self).shutdown() 103 | 104 | self.webview.close() 105 | 106 | ### 'QtServer' protocol ################################################## 107 | 108 | #: The `ProxyQWebView` object which specifies rules about how to handle 109 | #: different requests etc. 110 | webview = Instance(ProxyQWebView) 111 | def _webview_default(self): 112 | user_root, index_file = self.home_url.split('/')[-2:] 113 | 114 | return ProxyQWebView( 115 | python_namespace = 'qt_bridge', 116 | callbacks = [('handle_request', self.handle_request)], 117 | debug = self.debug, 118 | hosts = { 119 | user_root: FileLoader( 120 | root = abspath(self.base_url), 121 | overrides = { 122 | index_file: self.html, 123 | join('jigna', 'jigna.js'): open(JIGNA_JS_FILE).read(), 124 | join('jigna', 'jigna-vue.js'): 125 | open(JIGNA_VUE_JS_FILE).read() 126 | } 127 | ), 128 | 'root.filesystem': FileLoader(root=abspath(os.sep)) 129 | } 130 | ) 131 | 132 | #### Private protocol ##################################################### 133 | 134 | _bridge = Instance(QtBridge) 135 | def __bridge_default(self): 136 | return QtBridge(webview=self.webview) 137 | 138 | _plugin_factory = Instance('QtWebPluginFactory') 139 | 140 | def _enable_qwidget_embedding(self): 141 | """ Allow generic qwidgets to be embedded in the generated QWebView. 142 | """ 143 | global_settings = QtWebKit.QWebSettings.globalSettings() 144 | global_settings.setAttribute(QtWebKit.QWebSettings.PluginsEnabled,True) 145 | 146 | self._plugin_factory = QtWebPluginFactory(context=self.context) 147 | self.webview.page().setPluginFactory(self._plugin_factory) 148 | 149 | 150 | class QtWebPluginFactory(QtWebKit.QWebPluginFactory): 151 | 152 | MIME_TYPE = 'application/x-qwidget' 153 | 154 | def __init__(self, context): 155 | self.context = context 156 | super(self.__class__, self).__init__() 157 | 158 | def plugins(self): 159 | plugin = QtWebKit.QWebPluginFactory.Plugin() 160 | plugin.name = 'QWidget' 161 | mimeType = QtWebKit.QWebPluginFactory.MimeType() 162 | mimeType.name = self.MIME_TYPE 163 | plugin.mimeTypes = [mimeType] 164 | 165 | return [plugin] 166 | 167 | def create(self, mimeType, url, argNames, argVals): 168 | """ Return the QWidget to be embedded. 169 | """ 170 | if mimeType != self.MIME_TYPE: 171 | return 172 | 173 | args = dict(zip(argNames, argVals)) 174 | widget_factory = eval(args.get('widget-factory'), self.context) 175 | 176 | return widget_factory() 177 | 178 | #### EOF ###################################################################### 179 | -------------------------------------------------------------------------------- /jigna/template.py: -------------------------------------------------------------------------------- 1 | # 2 | # Enthought product code 3 | # 4 | # (C) Copyright 2013-2016 Enthought, Inc., Austin, TX 5 | # All right reserved. 6 | # 7 | 8 | from textwrap import dedent 9 | 10 | # Enthought library. 11 | from traits.api import Bool, HasTraits, Str, Property, Tuple, Int 12 | 13 | 14 | #### HTML template ############################################################ 15 | 16 | class Template(HasTraits): 17 | """ Encapsulation of the HTML/AngularJS template which can be rendered by 18 | jigna. 19 | """ 20 | 21 | #### 'Template' protocol ################################################## 22 | 23 | #: Should we use the async client or not. 24 | #: 25 | #: Async client presents a deferred API and is useful when you want to have 26 | #: your View served over the web where you don't want to freeze the browser 27 | #: during synchronous GET calls from the server. 28 | #: 29 | #: NOTE: When you're specifying the full HTML for the View, the option to 30 | #: start an async client is specified at the Javascript level instead of 31 | #: here, using the Javascript statement: `jigna.initialize({async: true})`. 32 | #: In that case, the value of this trait becomes moot. 33 | async = Bool(False) 34 | 35 | #: The base url for all resources (relative urls are resolved corresponding 36 | #: to the current working directory). 37 | base_url = Str 38 | 39 | #: The inner HTML for the *body* of the view's document. 40 | body_html = Str 41 | 42 | #: The inner HTML for the *head* of the view's document. 43 | head_html = Str 44 | 45 | #: The file which contains the html. `html_file` takes precedence over 46 | #: `body_html` and `head_html`. 47 | html_file = Str 48 | 49 | #: The HTML template used for this. 50 | html_template = Str 51 | 52 | #: The HTML for the entire document. 53 | #: 54 | #: The order of precedence in determining its value is: 55 | #: 1. Directly specified `html` trait 56 | #: 2. Read the contents of the file specified by the `html_file` trait 57 | #: 3. Create the jigna template out of specified `body_html` and `head_html` 58 | #: traits 59 | html = Property(Str) 60 | _html = Str 61 | 62 | #: Recommended size of the HTML (in a (width, height) format) 63 | recommended_size = Tuple(Int(600), Int(400)) 64 | 65 | def _get_html(self): 66 | """ Get the default HTML document for the given model. """ 67 | 68 | # Return the cached html value if the trait is specified directly 69 | if len(self._html) > 0: 70 | return self._html 71 | 72 | # Else, read from the html file if it is specified... 73 | if len(self.html_file) > 0: 74 | with open(self.html_file) as f: 75 | html = f.read() 76 | 77 | # ...otherwise, create the template out of body and head htmls 78 | else: 79 | async = 'true' if self.async else 'false' 80 | html = self.html_template.format( 81 | body_html = self.body_html, 82 | head_html = self.head_html, 83 | async = async, 84 | ) 85 | 86 | return html 87 | 88 | def _set_html(self, html): 89 | self._html = html 90 | 91 | def _html_template_default(self): 92 | return dedent(""" 93 | 94 | 95 | 96 | 99 | 100 | {head_html} 101 | 102 | 103 | 104 | 105 | {body_html} 106 | 107 | 108 | 109 | """) 110 | -------------------------------------------------------------------------------- /jigna/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enthought/jigna/00192450515099d8abc74866ffe1e4f743fe2279/jigna/tests/__init__.py -------------------------------------------------------------------------------- /jigna/tests/test_html_widget.py: -------------------------------------------------------------------------------- 1 | try: 2 | from __builtin__ import basestring 3 | except ImportError: 4 | from builtins import str as basestring 5 | 6 | from jigna.api import HTMLWidget, Template 7 | from jigna.qt import QtWebKitWidgets, QtWidgets 8 | 9 | from traits.api import HasTraits, Str, Int 10 | 11 | import unittest 12 | 13 | class MyModel(HasTraits): 14 | attr1 = Str 15 | attr2 = Int 16 | 17 | class TestHTMLWidget(unittest.TestCase): 18 | 19 | def setUp(self): 20 | self.model = MyModel(attr1="Attr1", attr2=2) 21 | self.template = Template(body_html="") 22 | 23 | self.app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) 24 | 25 | def test_widget_is_created(self): 26 | # Create a widget 27 | widget = HTMLWidget( 28 | template=self.template, context={'model': self.model} 29 | ) 30 | 31 | # Check if a qwebview widget was created 32 | self.assertIsNotNone(widget) 33 | self.assertIsInstance(widget, QtWidgets.QWidget) 34 | self.assertIsInstance(widget.webview, QtWebKitWidgets.QWebView) 35 | 36 | def test_created_widget_loads_html(self): 37 | # Create a widget 38 | widget = HTMLWidget( 39 | template=self.template, context={'model': self.model} 40 | ) 41 | 42 | # Check if the widget has html loaded in it 43 | webview = widget.webview 44 | frame = webview.page().mainFrame() 45 | self.assertIsInstance(frame.toHtml(), basestring) 46 | self.assertGreater(len(frame.toHtml()), 0) 47 | 48 | def test_created_widget_can_access_jigna(self): 49 | # Create a widget 50 | widget = HTMLWidget( 51 | template=self.template, context={'model': self.model} 52 | ) 53 | 54 | # Check if `jigna` was loaded on the JS side 55 | jigna = widget.execute_js("jigna"); 56 | self.assertIsNotNone(jigna) 57 | 58 | def test_jigna_was_initialized(self): 59 | # Create a widget 60 | widget = HTMLWidget( 61 | template=self.template, context={'model': self.model} 62 | ) 63 | 64 | # Check if `jigna` was initialised on the JS side 65 | client = widget.execute_js("jigna.client"); 66 | self.assertIsNotNone(client) 67 | 68 | def test_jigna_was_initialized_with_python_models(self): 69 | # Create a widget 70 | widget = HTMLWidget( 71 | template=self.template, context={'model': self.model} 72 | ) 73 | 74 | # Check if `jigna` was initialised with the correct python models 75 | # (check by making sure that primitive variables are the same) 76 | attr1 = widget.execute_js("jigna.models.model.attr1") 77 | attr2 = widget.execute_js("jigna.models.model.attr2") 78 | self.assertEqual(attr1, self.model.attr1) 79 | self.assertEqual(attr2, self.model.attr2) 80 | 81 | def test_two_widgets_are_created(self): 82 | widget1 = HTMLWidget( 83 | template=self.template, context={'model': self.model} 84 | ) 85 | 86 | widget2 = HTMLWidget( 87 | template=self.template, context={'model': self.model} 88 | ) 89 | 90 | # Check if `jigna` was initialised with the correct python models 91 | # (check by making sure that primitive variables are the same) 92 | attr1 = widget1.execute_js("jigna.models.model.attr1") 93 | attr2 = widget2.execute_js("jigna.models.model.attr2") 94 | self.assertEqual(attr1, self.model.attr1) 95 | self.assertEqual(attr2, self.model.attr2) 96 | 97 | def test_events_are_not_sent_after_widget_is_closed(self): 98 | # Given 99 | template = Template(body_html="Attr1: {{model.attr1}}") 100 | widget = HTMLWidget(template=template, context={'model': self.model}) 101 | 102 | # When 103 | self.model.attr1 = "one" 104 | widget.close() 105 | self.model.attr1 = "two" 106 | 107 | # Then 108 | # 109 | # After the widget is closed, we should not fire any further object 110 | # changed events. 111 | self.assertEqual(widget.execute_js("jigna.models.model.attr1"), "one") 112 | 113 | if __name__ == "__main__": 114 | unittest.main() 115 | -------------------------------------------------------------------------------- /jigna/tests/test_jigna_vue_qt.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import unittest 4 | 5 | from .test_jigna_qt import Person, TestJignaQt, AddressBook 6 | 7 | body_vue_html = """ 8 |
9 | Name: 10 | Age: 11 |
12 | Fruits: 13 |
    14 |
  • 15 | 16 |
  • 17 |
18 | 19 |
20 | 21 | Friends: 22 |
    23 |
  • 24 | Name: 25 | Age: 26 | Fruits: 27 |
      28 |
    • 29 | 30 |
    • 31 |
    32 |
  • 33 |
34 |
35 | 36 | Spouse:
37 | Name: {{model.spouse.name}} Age: {{model.spouse.age}} 38 | 39 |

Addressbook

40 | 43 |
44 |
    45 |
  • 46 | 47 | 48 |
  • 49 |
50 | """ 51 | 52 | class TestJignaVueQt(TestJignaQt): 53 | 54 | @classmethod 55 | def setUpClass(cls): 56 | from jigna.api import HTMLWidget, VueTemplate 57 | from jigna.utils import gui 58 | from jigna.qt import QtWidgets 59 | qapp = QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) 60 | template = VueTemplate(body_html=body_vue_html) 61 | fred = Person(name='Fred', age=42) 62 | addressbook = AddressBook() 63 | widget = HTMLWidget( 64 | template=template, 65 | context={'model':fred, 'addressbook': addressbook}) 66 | widget.show() 67 | gui.process_events() 68 | cls.widget = widget 69 | cls.fred = fred 70 | cls.addressbook = addressbook 71 | 72 | del TestJignaQt 73 | 74 | if __name__ == "__main__": 75 | unittest.main() 76 | -------------------------------------------------------------------------------- /jigna/tests/test_jigna_vue_web.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import sys 3 | from threading import Thread 4 | import unittest 5 | 6 | try: 7 | from tornado.ioloop import IOLoop 8 | except ImportError: 9 | raise unittest.SkipTest("Tornado not installed") 10 | 11 | try: 12 | from selenium import webdriver 13 | except ImportError: 14 | raise unittest.SkipTest("selenium not installed") 15 | 16 | # Local imports. 17 | from jigna.utils.web import get_free_port 18 | from .test_jigna_web import TestJignaWebSync, Person, AddressBook, \ 19 | patch_sys_modules, start_io_loop 20 | from .test_jigna_vue_qt import body_vue_html 21 | 22 | 23 | class TestJignaVueWebSync(TestJignaWebSync): 24 | @classmethod 25 | def setUpClass(cls, async=False): 26 | cls._backup_modules = patch_sys_modules() 27 | from jigna.vue_template import VueTemplate 28 | from jigna.web_app import WebApp 29 | fred = Person(name='Fred', age=42) 30 | addressbook = AddressBook() 31 | template = VueTemplate(body_html=body_vue_html, async=async) 32 | port = get_free_port() 33 | app = WebApp( 34 | template=template, 35 | context={'model':fred, 'addressbook':addressbook}, 36 | async=async 37 | ) 38 | app.listen(port) 39 | 40 | # Start the tornado server in a different thread so that we can write 41 | # test statements here in the main loop 42 | t = Thread(target=start_io_loop) 43 | t.setDaemon(True) 44 | t.start() 45 | 46 | # Note: Firefox needs geckodriver with recent releases 47 | # https://github.com/mozilla/geckodriver/releases 48 | browser = webdriver.Firefox() 49 | 50 | browser.get('http://localhost:%d'%port) 51 | cls.app = app 52 | cls.fred = fred 53 | cls.browser = browser 54 | cls.addressbook = addressbook 55 | cls.thread = t 56 | 57 | 58 | del TestJignaWebSync 59 | 60 | 61 | if __name__ == "__main__": 62 | unittest.main() 63 | -------------------------------------------------------------------------------- /jigna/tests/test_jigna_web.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import sys 3 | from textwrap import dedent 4 | from threading import Thread 5 | import time 6 | import sys 7 | import unittest 8 | from unittest import skip 9 | 10 | try: 11 | from tornado.ioloop import IOLoop 12 | except ImportError: 13 | raise unittest.SkipTest("Tornado not installed") 14 | 15 | try: 16 | from selenium import webdriver 17 | except ImportError: 18 | raise unittest.SkipTest("selenium not installed") 19 | 20 | # Local imports. 21 | from jigna.utils.web import get_free_port 22 | from .test_jigna_qt import (TestJignaQt, Person, body_html, AddressBook, 23 | sleep_while) 24 | 25 | 26 | def patch_sys_modules(): 27 | backup = sys.modules.copy() 28 | for name in list(sys.modules.keys()): 29 | if 'PySide' in name or 'PyQt' in name: 30 | del sys.modules[name] 31 | 32 | return backup 33 | 34 | 35 | def restore_sys_modules(backup): 36 | sys.modules = backup 37 | 38 | 39 | def assert_no_qt_in_sys_modules(): 40 | for name in list(sys.modules.keys()): 41 | if 'PySide' in name or 'PyQt' in name: 42 | raise AssertionError("Qt import found: %s." % name) 43 | 44 | 45 | def start_io_loop(): 46 | ioloop = IOLoop.instance() 47 | ioloop.make_current() 48 | ioloop.start() 49 | 50 | 51 | def stop_io_loop(): 52 | IOLoop.instance().stop() 53 | 54 | 55 | class TestJignaWebSync(TestJignaQt): 56 | @classmethod 57 | def setUpClass(cls, async=False): 58 | 59 | cls._backup_modules = patch_sys_modules() 60 | 61 | from jigna.template import Template 62 | from jigna.web_app import WebApp 63 | fred = Person(name='Fred', age=42) 64 | addressbook = AddressBook() 65 | template = Template(body_html=body_html, async=async) 66 | port = get_free_port() 67 | app = WebApp( 68 | template=template, 69 | context={'model':fred, 'addressbook': addressbook}, async=async 70 | ) 71 | app.listen(port) 72 | 73 | # Start the tornado server in a different thread so that we can write 74 | # test statements here in the main loop. 75 | t = Thread(target=start_io_loop) 76 | t.setDaemon(True) 77 | t.start() 78 | 79 | browser = webdriver.Firefox() 80 | browser.implicitly_wait(10) 81 | browser.get('http://localhost:%d' % port) 82 | cls.app = app 83 | cls.fred = fred 84 | cls.browser = browser 85 | cls.addressbook = addressbook 86 | cls.thread = t 87 | 88 | @classmethod 89 | def tearDownClass(cls): 90 | cls.browser.quit() 91 | 92 | IOLoop.instance().add_callback(stop_io_loop) 93 | thread = cls.thread 94 | count = 0 95 | while thread.is_alive() and count < 50: 96 | time.sleep(0.1) 97 | cls.thread.join() 98 | 99 | # Smells a bit but doing this here ensures that none of the tested 100 | # cases imports Qt. 101 | assert_no_qt_in_sys_modules() 102 | 103 | restore_sys_modules(cls._backup_modules) 104 | 105 | def setUp(self): 106 | cls = self.__class__ 107 | self.app = cls.app 108 | self.browser = cls.browser 109 | self.fred = cls.fred 110 | self.fred.spouse = None 111 | self.fred.fruits = [] 112 | self.fred.friends = [] 113 | # Wait for the model to be setup before running the tests. 114 | self.get_attribute('jigna.models.model.name', self.fred.name) 115 | 116 | def process_events(self): 117 | pass 118 | 119 | def execute_js(self, js): 120 | return self.browser.execute_script(js) 121 | 122 | def get_attribute(self, js, expect): 123 | get_js = dedent(""" 124 | var result; 125 | try { 126 | result = eval(%r); 127 | } catch (err) { 128 | result = undefined; 129 | } 130 | return result; 131 | """%js) 132 | self.execute_js(get_js) 133 | 134 | check_js = get_js 135 | result = self.execute_js(check_js) 136 | count = 0 137 | while result != expect and count < 20: 138 | time.sleep(0.1) 139 | result = self.execute_js(check_js) 140 | count += 1 141 | return result 142 | 143 | def assertJSEqual(self, js, value): 144 | result = self.get_attribute(js, value) 145 | if isinstance(value, (list, tuple)): 146 | msg = "Lengths different: expected %d, got %d" % \ 147 | (len(value), len(result)) 148 | self.assertEqual(len(value), len(result), msg) 149 | for index in range(len(value)): 150 | expect = value[index] 151 | got = result[str(index)] 152 | if got != expect: 153 | got = self.get_attribute(js+"['%d']"%index, expect) 154 | msg = "%s[%s] != %s, got %s"%(js, index, expect, got) 155 | self.assertEqual(expect, got, msg) 156 | else: 157 | msg = "%s != %s, got %s"%(js, value, result) 158 | self.assertEqual(value, result, msg) 159 | 160 | def test_instance_trait(self): 161 | # Overridden to work with the web backend. 162 | self.assertJSEqual("jigna.models.model.spouse", None) 163 | wilma = Person(name='Wilma', age=40) 164 | self.fred.spouse = wilma 165 | self.assertJSEqual("jigna.models.model.spouse.name", 'Wilma') 166 | self.assertJSEqual("jigna.models.model.spouse.age", 40) 167 | 168 | # Set in the JS side. 169 | self.execute_js("jigna.models.model.spouse.name = 'Wilmaji'") 170 | self.execute_js("jigna.models.model.spouse.age = 41") 171 | self.wait_and_assert(lambda: wilma.name != 'Wilmaji') 172 | self.wait_and_assert(lambda: wilma.age != 41) 173 | 174 | def test_reload_works_correctly(self): 175 | # Given 176 | fred = self.fred 177 | fred.fruits = ["peach", "pear"] 178 | dino = Person(name="Dino", age=10) 179 | fred.friends = [dino] 180 | fred.phonebook = {'joe' : 123, 'joan' : 345} 181 | wilma = Person(name='Wilma', age=40) 182 | fred.spouse = wilma 183 | 184 | # When 185 | self.execute_js("window.location.reload();") 186 | time.sleep(1) 187 | self.get_attribute("jigna.models.model.name", fred.name) 188 | 189 | # Then 190 | self.assertJSEqual("jigna.models.model.name", fred.name) 191 | self.assertJSEqual("jigna.models.model.friends[0].name", "Dino") 192 | self.assertJSEqual("jigna.models.model.friends[0].age", 10) 193 | self.assertJSEqual("jigna.models.model.phonebook.joe", 123) 194 | self.assertJSEqual("jigna.models.model.phonebook.joan", 345) 195 | self.assertJSEqual("jigna.models.model.phonebook", fred.phonebook) 196 | self.assertJSEqual("jigna.models.model.fruits", fred.fruits) 197 | self.assertJSEqual("jigna.models.model.spouse.name", 'Wilma') 198 | self.assertJSEqual("jigna.models.model.spouse.age", 40) 199 | 200 | # Delete this so running just this file does not run all the tests. 201 | del TestJignaQt 202 | 203 | if __name__ == "__main__": 204 | unittest.main() 205 | 206 | #### EOF ###################################################################### 207 | -------------------------------------------------------------------------------- /jigna/tests/test_jigna_web_async.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import sys 3 | import unittest 4 | from unittest import skipIf 5 | 6 | from .test_jigna_qt import sleep_while 7 | from .test_jigna_web import TestJignaWebSync, Person 8 | 9 | class TestJignaWebAsync(TestJignaWebSync): 10 | @classmethod 11 | def setUpClass(cls): 12 | super(TestJignaWebAsync, cls).setUpClass(async=True) 13 | 14 | def test_callable(self): 15 | fred = self.fred 16 | wilma = Person(name='Wilma', age=40) 17 | self.fred.spouse = wilma 18 | self.execute_js("var x; jigna.models.model.method('hello').done(function(r){x=r;}); return x;") 19 | self.wait_and_assert(lambda: fred.called_with != "hello") 20 | 21 | self.execute_js("var x; jigna.models.model.method(1).done(function(r){x=r;}); return x;") 22 | self.wait_and_assert(lambda: fred.called_with != 1) 23 | 24 | self.execute_js("var x; jigna.models.model.method(10.0).done(function(r){x=r;}); return x;") 25 | self.wait_and_assert(lambda: fred.called_with != 10.0) 26 | 27 | self.execute_js("var x; jigna.models.model.method([1,2]).done(function(r){x=r;}); return x;") 28 | self.wait_and_assert(lambda: fred.called_with != [1,2]) 29 | 30 | self.execute_js("var x; jigna.models.model.method(jigna.models.model.spouse).done(function(r){x=r;}); return x;") 31 | self.wait_and_assert(lambda: fred.called_with != wilma) 32 | 33 | 34 | @skipIf(sys.platform.startswith('linux'), "Fails on Linux") 35 | def test_list_sortable(self): 36 | super(TestJignaWebAsync, self).test_list_sortable() 37 | 38 | 39 | # Delete this so running just this file does not run all the tests. 40 | del TestJignaWebSync 41 | 42 | if __name__ == "__main__": 43 | unittest.main() 44 | -------------------------------------------------------------------------------- /jigna/tests/test_web_server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import tempfile 4 | import unittest 5 | 6 | import mock 7 | 8 | from tornado.web import Application 9 | from tornado.httputil import HTTPServerRequest 10 | 11 | from jigna.web_server import MainHandler, normalize_slice 12 | 13 | # A dummy image to write and test with. 14 | DATA = b"""\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x05\x00\x00\x00\x05\x08\x06\x00\x00\x00\x8do&\xe5\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\x0b\xfca\x05\x00\x00\x00 cHRM\x00\x00z&\x00\x00\x80\x84\x00\x00\xfa\x00\x00\x00\x80\xe8\x00\x00u0\x00\x00\xea`\x00\x00:\x98\x00\x00\x17p\x9c\xbaQ<\x00\x00\x00\tpHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x01YiTXtXML:com.adobe.xmp\x00\x00\x00\x00\x00\n \n \n 1\n \n \n\nL\xc2\'Y\x00\x00\x00tIDAT\x08\x1d\x01i\x00\x96\xff\x01\x00\x1cj\xff}e0\x00;8*\x00\xcb\xcd\xd9\x00\xa2\xad\xd3\x00\x04gP!\x00<9)\x00\x03\x03\x03\x00YVC\x00\xd7\xd9\xe5\x00\x04\x08\x08\x01\x00\xb0\xb4\xc3\x00\n\x08\r\x00\x0f\x0e\x08\x00\xf7\xf8\xfd\x00\x04\xe1\xe3\xf1\x0030\x18\x00\xfc\xfb\x03\x00>>0\x00\x04\x05\x03\x00\x03\xef\xff0\x80\xef\xed\xf3\x00>:$\x00\xdc\xdc\xe5\x00y\x88\xc9\x00\x9a\xa5"\x98\x19\x929\xa9\x00\x00\x00\x00IEND\xaeB`\x82""" 15 | 16 | 17 | class DummyServer: 18 | def __init__(self): 19 | self.html = b'html' 20 | self.base_url = '/' 21 | 22 | 23 | class TestableMainHandler(MainHandler): 24 | def write(self, data): 25 | self.test_data = data 26 | 27 | 28 | class TestMainHandler(unittest.TestCase): 29 | 30 | def setUp(self): 31 | self.tmpfile = '' 32 | self.fd = None 33 | 34 | def tearDown(self): 35 | if len(self.tmpfile) > 0 and os.path.exists(self.tmpfile): 36 | os.close(self.fd) 37 | os.remove(self.tmpfile) 38 | 39 | def _make_text_data(self): 40 | self.fd, self.tmpfile = tempfile.mkstemp('.txt') 41 | with open(self.tmpfile, 'w') as fp: 42 | fp.write('hello') 43 | 44 | def _make_binary_data(self): 45 | self.fd, self.tmpfile = tempfile.mkstemp('.png') 46 | with open(self.tmpfile, 'wb') as fp: 47 | fp.write(DATA) 48 | 49 | def _make_request(self, path): 50 | request = mock.MagicMock(spec=HTTPServerRequest)() 51 | if sys.platform.startswith('win'): 52 | request.path = '/' + path 53 | else: 54 | request.path = path 55 | return request 56 | 57 | def test_get_root(self): 58 | # Given 59 | request = self._make_request('') 60 | app = Application() 61 | server = DummyServer() 62 | 63 | # When 64 | h = TestableMainHandler(app, request, server=server) 65 | h.get() 66 | 67 | # Then 68 | self.assertEqual(h.test_data, b'html') 69 | 70 | def test_get_text_data(self): 71 | # Given 72 | self._make_text_data() 73 | request = self._make_request(self.tmpfile) 74 | app = Application() 75 | server = DummyServer() 76 | 77 | # When 78 | h = TestableMainHandler(app, request, server=server) 79 | h.get() 80 | 81 | # Then 82 | self.assertEqual(h.test_data, b'hello') 83 | 84 | def test_get_binary_data(self): 85 | # Given 86 | self._make_binary_data() 87 | request = self._make_request(self.tmpfile) 88 | app = Application() 89 | server = DummyServer() 90 | 91 | # When 92 | h = TestableMainHandler(app, request, server=server) 93 | h.get() 94 | 95 | # Then 96 | self.assertEqual(h.test_data, DATA) 97 | 98 | 99 | class TestNormalizeSlice(unittest.TestCase): 100 | def test_simple_slice(self): 101 | # Given 102 | s = slice(1, 10) 103 | 104 | # When 105 | s1 = normalize_slice(s, 10) 106 | 107 | # Then 108 | self.assertEqual(s1, slice(1, 10, 1)) 109 | 110 | # Given 111 | s = slice(1, 10, 2) 112 | 113 | # When 114 | s1 = normalize_slice(s, 10) 115 | 116 | # Then 117 | self.assertEqual(s1, slice(1, 10, 2)) 118 | 119 | def test_slice_with_missing_start_stop(self): 120 | # Given 121 | s = slice(None, 10) 122 | 123 | # When 124 | s1 = normalize_slice(s, 10) 125 | 126 | # Then 127 | self.assertEqual(s1, slice(0, 10, 1)) 128 | 129 | # Given 130 | s = slice(1, None) 131 | 132 | # When 133 | s1 = normalize_slice(s, 10) 134 | 135 | # Then 136 | self.assertEqual(s1, slice(1, 10, 1)) 137 | 138 | def test_slice_with_negative_indices(self): 139 | # Given 140 | s = slice(-5, -1) 141 | 142 | # When 143 | s1 = normalize_slice(s, 10) 144 | 145 | # Then 146 | self.assertEqual(s1, slice(5, 9, 1)) 147 | 148 | def test_slice_with_negative_stride(self): 149 | # Given 150 | s = slice(None, None, -2) 151 | 152 | # When 153 | s1 = normalize_slice(s, 10) 154 | 155 | # Then 156 | self.assertEqual(s1, slice(1, 10, 2)) 157 | data = list(range(10)) 158 | self.assertEqual(data[s], data[s1][::-1]) 159 | 160 | # Given 161 | s = slice(None, None, -2) 162 | 163 | # When 164 | s1 = normalize_slice(s, 11) 165 | 166 | # Then 167 | self.assertEqual(s1, slice(0, 11, 2)) 168 | data = list(range(11)) 169 | self.assertEqual(data[s], data[s1][::-1]) 170 | 171 | # Given 172 | s = slice(None, None, -3) 173 | 174 | # When 175 | s1 = normalize_slice(s, 10) 176 | 177 | # Then 178 | self.assertEqual(s1, slice(0, 10, 3)) 179 | data = list(range(10)) 180 | self.assertEqual(data[s], data[s1][::-1]) 181 | 182 | # Given 183 | s = slice(-1, -5, -2) 184 | 185 | # When 186 | s1 = normalize_slice(s, 10) 187 | 188 | # Then 189 | self.assertEqual(s1, slice(7, 10, 2)) 190 | data = list(range(10)) 191 | self.assertEqual(data[s], data[s1][::-1]) 192 | 193 | 194 | if __name__ == '__main__': 195 | unittest.main() 196 | -------------------------------------------------------------------------------- /jigna/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enthought/jigna/00192450515099d8abc74866ffe1e4f743fe2279/jigna/utils/__init__.py -------------------------------------------------------------------------------- /jigna/utils/gui.py: -------------------------------------------------------------------------------- 1 | # Standard library imports 2 | import sys 3 | 4 | # Local imports 5 | from ..qt import QtWidgets, QtCore 6 | 7 | 8 | def ui_handler(handler, *args, **kw): 9 | """ Handles UI notification handler requests that occur on a thread other 10 | than the UI thread. 11 | """ 12 | invoke_later(handler, *args, **kw) 13 | 14 | 15 | def set_trait_later(obj, trait_name, value): 16 | """ Set the given trait name on the given object in the GUI thread. 17 | """ 18 | invoke_later(setattr, obj, trait_name, value) 19 | 20 | 21 | def invoke_later(callable, *args, **kw): 22 | """ Invoke the callable in the GUI thread. 23 | """ 24 | _FutureCall(0, callable, *args, **kw) 25 | 26 | 27 | def do_after(ms, callable, *args, **kw): 28 | """ Invoke the callable after the given number of milliseconds. 29 | """ 30 | app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv) 31 | QtCore.QTimer.singleShot(ms, lambda : callable(*args, **kw)) 32 | 33 | 34 | def process_events(): 35 | """ Process all events. 36 | """ 37 | QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents) 38 | 39 | 40 | #### Private protocol ######################################################### 41 | 42 | class _FutureCall(QtCore.QObject): 43 | """ This is a helper class to facilitate execution of a function in the 44 | future (after all events are finished processing) in the GUI thread. 45 | """ 46 | 47 | # Keep a list of references so that they don't get garbage collected. 48 | _calls = [] 49 | 50 | # Manage access to the list of instances. 51 | _calls_mutex = QtCore.QMutex() 52 | 53 | # A new Qt event type for _FutureCalls 54 | _gui_event = QtCore.QEvent.Type(QtCore.QEvent.registerEventType()) 55 | 56 | def __init__(self, ms, callable, *args, **kw): 57 | super(_FutureCall, self).__init__() 58 | 59 | # Save the arguments. 60 | self._ms = ms 61 | self._callable = callable 62 | self._args = args 63 | self._kw = kw 64 | 65 | # Save the instance. 66 | self._calls_mutex.lock() 67 | self._calls.append(self) 68 | self._calls_mutex.unlock() 69 | 70 | # Move to the main GUI thread. 71 | self.moveToThread(QtWidgets.QApplication.instance().thread()) 72 | 73 | # Post an event to be dispatched on the main GUI thread. Note that 74 | # we do not call QTimer.singleShot here, which would be simpler, because 75 | # that only works on QThreads. We want regular Python threads to work. 76 | event = QtCore.QEvent(self._gui_event) 77 | QtWidgets.QApplication.postEvent(self, event) 78 | QtWidgets.QApplication.sendPostedEvents() 79 | 80 | def event(self, event): 81 | """ QObject event handler. 82 | """ 83 | if event.type() == self._gui_event: 84 | # Invoke the callable (puts it at the end of the event queue) 85 | QtCore.QTimer.singleShot(self._ms, self._dispatch) 86 | return True 87 | 88 | return super(_FutureCall, self).event(event) 89 | 90 | def _dispatch(self): 91 | """ Invoke the callable. 92 | """ 93 | try: 94 | self._callable(*self._args, **self._kw) 95 | finally: 96 | self._finished() 97 | 98 | def _finished(self): 99 | """ Remove the call from the list, so it can be garbage collected. 100 | """ 101 | self._calls_mutex.lock() 102 | try: 103 | self._calls.remove(self) 104 | finally: 105 | self._calls_mutex.unlock() 106 | -------------------------------------------------------------------------------- /jigna/utils/notebook.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility methods for the IPython notebook 3 | """ 4 | 5 | from jigna.api import WebApp 6 | from jigna.utils.web import get_free_port 7 | from IPython.display import HTML 8 | 9 | def display_jigna(context, template, size=None): 10 | """ 11 | A `display_html` style method to show rich jigna display for the objects 12 | within the context. 13 | """ 14 | PORT = get_free_port() 15 | 16 | app = WebApp(context=context, template=template, port=PORT) 17 | application = app.create_application() 18 | application.listen(app.port) 19 | 20 | width, height = size or template.recommended_size 21 | html = ('' 22 | % (PORT, width, height)) 23 | 24 | return HTML(html) 25 | -------------------------------------------------------------------------------- /jigna/utils/web.py: -------------------------------------------------------------------------------- 1 | # 2 | # Enthought product code 3 | # 4 | # (C) Copyright 2013 Enthought, Inc., Austin, TX 5 | # All right reserved. 6 | # 7 | 8 | """ General utility methods for the web version """ 9 | 10 | import socket 11 | 12 | def get_free_port(): 13 | """ 14 | Returns a free socket port. It works by creating an empty socket, binding it 15 | to port 0 so that the OS automatically assigns a free port to it, obtaining 16 | the port using `getsockname` and then immediately closing it. 17 | 18 | The application intending to use this port should bind to it immediately so 19 | that no other application binds to it before us. 20 | """ 21 | sock = socket.socket() 22 | 23 | # bind to a random port (so that the OS automatically assigns us a free port) 24 | sock.bind(('', 0)) 25 | 26 | # obtain the random port value 27 | port = sock.getsockname()[1] 28 | 29 | # close the socket so that the port gets free 30 | sock.close() 31 | 32 | return port 33 | 34 | 35 | def start_web_app(template, context, port=8000): 36 | """ 37 | Start a web app at the given port for serving the jigna view for the given 38 | template and context. 39 | """ 40 | from tornado.ioloop import IOLoop 41 | from jigna.web_app import WebApp 42 | 43 | ioloop = IOLoop.instance() 44 | 45 | app = WebApp(template=template, context=context) 46 | app.listen(port) 47 | 48 | print('Starting the web app on port %s ...' % port) 49 | ioloop.start() 50 | -------------------------------------------------------------------------------- /jigna/vue_template.py: -------------------------------------------------------------------------------- 1 | # 2 | # Enthought product code 3 | # 4 | # (C) Copyright 2013-2016 Enthought, Inc., Austin, TX 5 | # All right reserved. 6 | # 7 | 8 | from textwrap import dedent 9 | 10 | # Local imports 11 | from .template import Template 12 | 13 | 14 | class VueTemplate(Template): 15 | """A template for vue.js templates. Note that this assumes that the 16 | ViewModel is attached to the body and all the jigna models are exposed. 17 | """ 18 | def _html_template_default(self): 19 | return dedent(""" 20 | 21 | 22 | 23 | {head_html} 24 | 25 | 26 | 27 | {body_html} 28 | 29 | 45 | 46 | 47 | 48 | """) 49 | -------------------------------------------------------------------------------- /jigna/web_app.py: -------------------------------------------------------------------------------- 1 | # 2 | # Enthought product code 3 | # 4 | # (C) Copyright 2013 Enthought, Inc., Austin, TX 5 | # All right reserved. 6 | # 7 | 8 | # Standard Library 9 | import os 10 | from os.path import join 11 | 12 | # Tornado Library 13 | from tornado import web 14 | 15 | # Local Library 16 | from .web_server import WebServer, AsyncWebServer 17 | 18 | 19 | class WebApp(web.Application): 20 | """ A web based App to serve the jigna template with a given context over 21 | the web where it can be viewed using a regular web browser. """ 22 | 23 | def __init__(self, handlers=None, default_host="", transforms=None, 24 | context=None, template=None, trait_change_dispatch="same", 25 | async=False, **kw): 26 | 27 | if template is not None: 28 | template.async = async 29 | self.context = context 30 | self.template = template 31 | self.trait_change_dispatch = trait_change_dispatch 32 | self.async = async 33 | 34 | if handlers is None: 35 | handlers = [] 36 | handlers += self._create_handlers() 37 | 38 | super(WebApp, self).__init__(handlers, default_host, transforms, **kw) 39 | 40 | #### Private protocol ##################################################### 41 | 42 | def _create_handlers(self): 43 | """ 44 | Create the web application serving the given context. Returns the 45 | tornado application created. 46 | """ 47 | 48 | # Set up the WebServer to serve the domain models in context 49 | klass = AsyncWebServer if self.async else WebServer 50 | server = klass( 51 | base_url = join(os.getcwd(), self.template.base_url), 52 | html = self.template.html, 53 | context = self.context, 54 | trait_change_dispatch = self.trait_change_dispatch 55 | ) 56 | 57 | return server.handlers 58 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | traits 2 | tornado 3 | nose 4 | mock 5 | coverage 6 | selenium 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # (C) Copyright 2013-2017 Enthought, Inc., Austin, TX 3 | # All right reserved. 4 | # 5 | 6 | import os.path 7 | from setuptools import setup, find_packages 8 | 9 | data = {} 10 | fname = os.path.join('jigna', '__init__.py') 11 | exec(compile(open(fname).read(), fname, 'exec'), data) 12 | 13 | requires = ['traits', 'tornado'] 14 | pyside = requires + ['pyside'] 15 | test = pyside + ['nose', 'mock', 'coverage', 'selenium'] 16 | 17 | with open('README.rst') as fp: 18 | long_desc = fp.read() 19 | 20 | 21 | setup( 22 | name='jigna', 23 | version=data['__version__'], 24 | author='Enthought, Inc', 25 | author_email='info@enthought.com', 26 | url='https://github.com/enthought/jigna', 27 | license='BSD', 28 | description='HTML frontend for Traits models.', 29 | long_description=long_desc, 30 | packages=find_packages(), 31 | package_dir={'jigna': 'jigna'}, 32 | include_package_data=True, 33 | package_data={'jigna': ['js/dist/*.js']}, 34 | install_requires=requires, 35 | extras_require={ 36 | 'pyside': pyside, 37 | 'test': test, 38 | }, 39 | classifiers=[ 40 | c.strip() for c in """\ 41 | Development Status :: 4 - Beta 42 | Environment :: Web Environment 43 | Intended Audience :: Developers 44 | Intended Audience :: Science/Research 45 | License :: OSI Approved :: BSD License 46 | Operating System :: MacOS :: MacOS X 47 | Operating System :: Microsoft :: Windows 48 | Operating System :: OS Independent 49 | Operating System :: POSIX 50 | Operating System :: Unix 51 | Programming Language :: Python 52 | Programming Language :: JavaScript 53 | Topic :: Software Development 54 | Topic :: Software Development :: Libraries 55 | Topic :: Software Development :: User Interfaces 56 | """.splitlines() if len(c.split()) > 0 57 | ], 58 | zip_safe=False 59 | ) 60 | --------------------------------------------------------------------------------