././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1714924509.6896565 qtconsole-5.5.2/0000775000175000017500000000000000000000000014325 5ustar00carloscarlos00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603554021.0 qtconsole-5.5.2/.coveragerc0000664000175000017500000000005400000000000016445 0ustar00carloscarlos00000000000000[run] omit = # Omit tests */tests/* ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1714924509.6656566 qtconsole-5.5.2/.github/0000775000175000017500000000000000000000000015665 5ustar00carloscarlos00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1714924509.6656566 qtconsole-5.5.2/.github/workflows/0000775000175000017500000000000000000000000017722 5ustar00carloscarlos00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1714874034.0 qtconsole-5.5.2/.github/workflows/linux-tests.yml0000664000175000017500000000507100000000000022747 0ustar00carloscarlos00000000000000name: Linux tests on: push: branches: - main pull_request: branches: - main workflow_dispatch: concurrency: group: linux-tests-${{ github.ref }} cancel-in-progress: true jobs: linux: name: Linux Py${{ matrix.PYTHON_VERSION }} - ${{ matrix.INSTALL_TYPE }} - ${{ matrix.QT_LIB }} runs-on: ubuntu-latest env: CI: True QTCONSOLE_TESTING: True PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }} RUNNER_OS: 'ubuntu' COVERALLS_REPO_TOKEN: XWVhJf2AsO7iouBLuCsh0pPhwHy81Uz1v strategy: fail-fast: false matrix: PYTHON_VERSION: ['3.8', '3.9', '3.10', '3.11'] INSTALL_TYPE: ['conda', 'pip'] QT_LIB: ['pyqt5', 'pyqt6'] exclude: - INSTALL_TYPE: 'conda' QT_LIB: 'pyqt6' timeout-minutes: 15 steps: - name: Checkout branch uses: actions/checkout@v3 - name: Install System Packages run: | sudo apt-get update sudo apt-get install -y --no-install-recommends '^libxcb.*-dev' libx11-xcb-dev libglu1-mesa-dev libxrender-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev libegl1 - name: Install Conda uses: conda-incubator/setup-miniconda@v2 with: activate-environment: test auto-update-conda: false auto-activate-base: false channels: conda-forge channel-priority: strict miniforge-variant: Mambaforge python-version: ${{ matrix.PYTHON_VERSION }} - name: Install dependencies with conda if: matrix.INSTALL_TYPE == 'conda' shell: bash -l {0} run: mamba env update --file requirements/environment.yml - name: Install dependencies with pip if: matrix.INSTALL_TYPE == 'pip' shell: bash -l {0} run: | pip install -e .[test] if [ ${{ matrix.QT_LIB }} = "pyqt6" ]; then pip install pyqt6!=6.4.0 pyqt6-qt6!=6.4.0 coveralls pytest-cov else pip install ${{ matrix.QT_LIB }} coveralls pytest-cov fi - name: Show environment information shell: bash -l {0} run: | conda info conda list pip list - name: Run tests shell: bash -l {0} run: xvfb-run --auto-servernum pytest -vv -s --full-trace --color=yes --cov=qtconsole qtconsole env: QT_API: ${{ matrix.QT_LIB }} PYTEST_QT_API: ${{ matrix.QT_LIB }} - name: Upload coverage to Codecov if: matrix.PYTHON_VERSION == '3.8' shell: bash -l {0} run: coveralls ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1714874034.0 qtconsole-5.5.2/.github/workflows/macos-tests.yml0000664000175000017500000000247700000000000022721 0ustar00carloscarlos00000000000000name: Macos tests on: push: branches: - main pull_request: branches: - main concurrency: group: macos-tests-${{ github.ref }} cancel-in-progress: true jobs: macos: name: Mac Py${{ matrix.PYTHON_VERSION }} runs-on: macos-latest env: CI: True QTCONSOLE_TESTING: True PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }} RUNNER_OS: 'macos' strategy: fail-fast: false matrix: PYTHON_VERSION: ['3.8', '3.9', '3.10', '3.11'] timeout-minutes: 15 steps: - name: Checkout branch uses: actions/checkout@v3 - name: Install Conda uses: conda-incubator/setup-miniconda@v2 with: activate-environment: test auto-update-conda: false auto-activate-base: false python-version: ${{ matrix.PYTHON_VERSION }} miniforge-variant: Mambaforge channels: conda-forge channel-priority: strict - name: Install package dependencies shell: bash -l {0} run: mamba env update --file requirements/environment.yml - name: Show environment information shell: bash -l {0} run: | conda info conda list - name: Run tests shell: bash -l {0} run: pytest -vv --color=yes --cov=qtconsole qtconsole ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1714874034.0 qtconsole-5.5.2/.github/workflows/windows-tests.yml0000664000175000017500000000251500000000000023302 0ustar00carloscarlos00000000000000name: Windows tests on: push: branches: - main pull_request: branches: - main concurrency: group: windows-tests-${{ github.ref }} cancel-in-progress: true jobs: windows: name: Windows Py${{ matrix.PYTHON_VERSION }} runs-on: windows-latest env: CI: True QTCONSOLE_TESTING: True PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }} RUNNER_OS: 'windows' strategy: fail-fast: false matrix: PYTHON_VERSION: ['3.8', '3.9', '3.10', '3.11'] timeout-minutes: 15 steps: - name: Checkout branch uses: actions/checkout@v3 - name: Install Conda uses: conda-incubator/setup-miniconda@v2 with: activate-environment: test auto-update-conda: false auto-activate-base: false python-version: ${{ matrix.PYTHON_VERSION }} miniforge-variant: Mambaforge channels: conda-forge channel-priority: strict - name: Install package dependencies shell: bash -l {0} run: mamba env update --file requirements/environment.yml - name: Show environment information shell: bash -l {0} run: | conda info conda list - name: Run tests shell: bash -l {0} run: pytest -vv --color=yes --cov=qtconsole qtconsole ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1683308066.0 qtconsole-5.5.2/.gitignore0000664000175000017500000000053600000000000016321 0ustar00carloscarlos00000000000000MANIFEST build dist _build docs/man/*.gz docs/source/api/generated docs/source/config_options.rst docs/source/interactive/magics-generated.txt docs/gh-pages jupyter_notebook/notebook/static/mathjax jupyter_notebook/static/style/*.map *.py[co] __pycache__ *.egg-info *~ *.bak .ipynb_checkpoints .tox .DS_Store \#*# .#* .coverage .pytest_cache .vscode ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1450622414.0 qtconsole-5.5.2/.mailmap0000664000175000017500000002506700000000000015760 0ustar00carloscarlos00000000000000A. J. Holyoake ajholyoake Aaron Culich Aaron Culich Aron Ahmadia ahmadia Benjamin Ragan-Kelley Benjamin Ragan-Kelley Min RK Benjamin Ragan-Kelley MinRK Barry Wark Barry Wark Ben Edwards Ben Edwards Bradley M. Froehle Bradley M. Froehle Bradley M. Froehle Bradley Froehle Brandon Parsons Brandon Parsons Brian E. Granger Brian Granger Brian E. Granger Brian Granger <> Brian E. Granger bgranger <> Brian E. Granger bgranger Christoph Gohlke cgohlke Cyrille Rossant rossant Damián Avila damianavila Damián Avila damianavila Damon Allen damontallen Darren Dale darren.dale <> Darren Dale Darren Dale <> Dav Clark Dav Clark <> Dav Clark Dav Clark David Hirschfeld dhirschfeld David P. Sanders David P. Sanders David Warde-Farley David Warde-Farley <> Doug Blank Doug Blank Eugene Van den Bulke Eugene Van den Bulke Evan Patterson Evan Patterson Evan Patterson Evan Patterson Evan Patterson epatters Evan Patterson epatters Ernie French Ernie French Ernie French ernie french Ernie French ernop Fernando Perez Fernando Perez Fernando Perez Fernando Perez fperez <> Fernando Perez fptest <> Fernando Perez fptest1 <> Fernando Perez Fernando Perez Fernando Perez Fernando Perez <> Fernando Perez Fernando Perez Frank Murphy Frank Murphy Gabriel Becker gmbecker Gael Varoquaux gael.varoquaux <> Gael Varoquaux gvaroquaux Gael Varoquaux Gael Varoquaux <> Ingolf Becker watercrossing Jake Vanderplas Jake Vanderplas Jakob Gager jakobgager Jakob Gager jakobgager Jakob Gager jakobgager Jason Grout Jason Grout Jason Gors jason gors Jason Gors jgors Jens Hedegaard Nielsen Jens Hedegaard Nielsen Jens Hedegaard Nielsen Jens H Nielsen Jens Hedegaard Nielsen Jens H. Nielsen Jez Ng Jez Ng Jonathan Frederic Jonathan Frederic Jonathan Frederic Jonathan Frederic Jonathan Frederic Jonathan Frederic Jonathan Frederic jon Jonathan Frederic U-Jon-PC\Jon Jonathan March Jonathan March Jonathan March jdmarch Jörgen Stenarson Jörgen Stenarson Jörgen Stenarson Jorgen Stenarson Jörgen Stenarson Jorgen Stenarson <> Jörgen Stenarson jstenar Jörgen Stenarson jstenar <> Jörgen Stenarson Jörgen Stenarson Juergen Hasch juhasch Juergen Hasch juhasch Julia Evans Julia Evans Kester Tong KesterTong Kyle Kelley Kyle Kelley Kyle Kelley rgbkrk Laurent Dufréchou Laurent Dufréchou Laurent Dufréchou laurent dufrechou <> Laurent Dufréchou laurent.dufrechou <> Laurent Dufréchou Laurent Dufrechou <> Laurent Dufréchou laurent.dufrechou@gmail.com <> Laurent Dufréchou ldufrechou Lorena Pantano Lorena Luis Pedro Coelho Luis Pedro Coelho Marc Molla marcmolla Martín Gaitán Martín Gaitán Matthias Bussonnier Matthias BUSSONNIER Matthias Bussonnier Bussonnier Matthias Matthias Bussonnier Matthias BUSSONNIER Matthias Bussonnier Matthias Bussonnier Michael Droettboom Michael Droettboom Nicholas Bollweg Nicholas Bollweg (Nick) Nicolas Rougier Nikolay Koldunov Nikolay Koldunov Omar Andrés Zapata Mesa Omar Andres Zapata Mesa Omar Andrés Zapata Mesa Omar Andres Zapata Mesa Pankaj Pandey Pankaj Pandey Pascal Schetelat pascal-schetelat Paul Ivanov Paul Ivanov Pauli Virtanen Pauli Virtanen <> Pauli Virtanen Pauli Virtanen Pierre Gerold Pierre Gerold Pietro Berkes Pietro Berkes Piti Ongmongkolkul piti118 Prabhu Ramachandran Prabhu Ramachandran <> Puneeth Chaganti Puneeth Chaganti Robert Kern rkern <> Robert Kern Robert Kern Robert Kern Robert Kern Robert Kern Robert Kern <> Robert Marchman Robert Marchman Satrajit Ghosh Satrajit Ghosh Satrajit Ghosh Satrajit Ghosh Scott Sanderson Scott Sanderson smithj1 smithj1 smithj1 smithj1 Steven Johnson stevenJohnson Steven Silvester blink1073 S. Weber s8weber Stefan van der Walt Stefan van der Walt Silvia Vinyes Silvia Silvia Vinyes silviav12 Sylvain Corlay Sylvain Corlay sylvain.corlay Ted Drain TD22057 Théophile Studer Théophile Studer Thomas Kluyver Thomas Thomas Spura Thomas Spura Timo Paulssen timo vds vds2212 vds vds Ville M. Vainio Ville M. Vainio ville Ville M. Vainio ville Ville M. Vainio vivainio <> Ville M. Vainio Ville M. Vainio Ville M. Vainio Ville M. Vainio Walter Doerwald walter.doerwald <> Walter Doerwald Walter Doerwald <> W. Trevor King W. Trevor King Yoval P. y-p ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1714872940.0 qtconsole-5.5.2/.readthedocs.yaml0000664000175000017500000000071500000000000017557 0ustar00carloscarlos00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "mambaforge-4.10" sphinx: builder: html fail_on_warning: true configuration: docs/source/conf.py conda: environment: docs/environment.yml python: install: - method: pip path: . ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1648405558.0 qtconsole-5.5.2/CONTRIBUTING.md0000664000175000017500000000116000000000000016554 0ustar00carloscarlos00000000000000# Contributing We follow the [IPython Contributing Guide](https://github.com/ipython/ipython/blob/master/CONTRIBUTING.md). ## To set up a development environment Fork the repository and clone the forked repository locally. Use Conda to install dependencies and activate the development environment. ``` conda create -n qtdev python=3 conda activate qtdev conda env update --file requirements/environment.yml ``` To run after the changes have been made to source (preferred): ``` pip install -e . ``` **or,** for running immediate changes: ``` python setup.py develop ``` Finally, to run the tests: ``` pytest ``` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603554021.0 qtconsole-5.5.2/LICENSE0000664000175000017500000000277000000000000015340 0ustar00carloscarlos00000000000000BSD 3-Clause License Copyright (c) 2017, Project Jupyter Contributors All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603554021.0 qtconsole-5.5.2/MANIFEST.in0000664000175000017500000000056300000000000016067 0ustar00carloscarlos00000000000000include LICENSE include CONTRIBUTING.md include README.md # Documentation graft docs exclude docs/\#* # Examples graft examples # docs subdirs we want to skip prune docs/build prune docs/gh-pages prune docs/dist # Patterns to exclude from any directory global-exclude *~ global-exclude *.pyc global-exclude *.pyo global-exclude .git global-exclude .ipynb_checkpoints ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1714924509.6896565 qtconsole-5.5.2/PKG-INFO0000644000175000017500000001170000000000000015417 0ustar00carloscarlos00000000000000Metadata-Version: 2.1 Name: qtconsole Version: 5.5.2 Summary: Jupyter Qt console Home-page: http://jupyter.org Author: Jupyter Development Team Author-email: jupyter@googlegroups.com Maintainer: Spyder Development Team License: BSD Keywords: Interactive,Interpreter,Shell Platform: Linux Platform: Mac OS X Platform: Windows Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Requires-Python: >= 3.8 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: traitlets!=5.2.1,!=5.2.2 Requires-Dist: jupyter_core Requires-Dist: jupyter_client>=4.1 Requires-Dist: pygments Requires-Dist: ipykernel>=4.1 Requires-Dist: qtpy>=2.4.0 Requires-Dist: pyzmq>=17.1 Requires-Dist: packaging Provides-Extra: test Requires-Dist: flaky; extra == "test" Requires-Dist: pytest; extra == "test" Requires-Dist: pytest-qt; extra == "test" Provides-Extra: doc Requires-Dist: Sphinx>=1.3; extra == "doc" # Jupyter QtConsole ![Windows tests](https://github.com/jupyter/qtconsole/workflows/Windows%20tests/badge.svg) ![Macos tests](https://github.com/jupyter/qtconsole/workflows/Macos%20tests/badge.svg) ![Linux tests](https://github.com/jupyter/qtconsole/workflows/Linux%20tests/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/jupyter/qtconsole/badge.svg?branch=master)](https://coveralls.io/github/jupyter/qtconsole?branch=master) [![Documentation Status](https://readthedocs.org/projects/qtconsole/badge/?version=stable)](https://qtconsole.readthedocs.io/en/stable/) [![Google Group](https://img.shields.io/badge/-Google%20Group-lightgrey.svg)](https://groups.google.com/forum/#!forum/jupyter) A rich Qt-based console for working with Jupyter kernels, supporting rich media output, session export, and more. The Qtconsole is a very lightweight application that largely feels like a terminal, but provides a number of enhancements only possible in a GUI, such as inline figures, proper multiline editing with syntax highlighting, graphical calltips, and more. ![qtconsole](https://raw.githubusercontent.com/jupyter/qtconsole/master/docs/source/_images/qtconsole.png) ## Install Qtconsole The Qtconsole requires Python bindings for Qt, such as [PyQt6](https://pypi.org/project/PyQt6/), [PySide6](https://pypi.org/project/PySide6/), [PyQt5](https://pypi.org/project/PyQt5/) or [PySide2](https://pypi.org/project/PySide2/). Although [pip](https://pypi.python.org/pypi/pip) and [conda](http://conda.pydata.org/docs) may be used to install the Qtconsole, conda is simpler to use since it automatically installs PyQt5. Alternatively, the Qtconsole installation with pip needs additional steps since pip doesn't install the Qt requirement. ### Install using conda To install: conda install qtconsole **Note:** If the Qtconsole is installed using conda, it will **automatically** install the Qt requirement as well. ### Install using pip To install: pip install qtconsole **Note:** Make sure that Qt is installed. Unfortunately, Qt is not installed when using pip. The next section gives instructions on doing it. ### Installing Qt (if needed) You can install PyQt5 with pip using the following command: pip install pyqt5 or with a system package manager on Linux. For Windows, PyQt binary packages may be used. **Note:** Additional information about using a system package manager may be found in the [qtconsole documentation](https://qtconsole.readthedocs.io). More installation instructions for PyQt can be found in the [PyQt5 documentation](http://pyqt.sourceforge.net/Docs/PyQt5/installation.html) and [PyQt4 documentation](http://pyqt.sourceforge.net/Docs/PyQt4/installation.html) Source packages for Windows/Linux/MacOS can be found here: [PyQt5](https://www.riverbankcomputing.com/software/pyqt/download5) and [PyQt4](https://riverbankcomputing.com/software/pyqt/download). ## Usage To run the Qtconsole: jupyter qtconsole ## Resources - [Project Jupyter website](https://jupyter.org) - Documentation for the Qtconsole * [latest version](https://qtconsole.readthedocs.io/en/latest/) [[PDF](https://media.readthedocs.org/pdf/qtconsole/latest/qtconsole.pdf)] * [stable version](https://qtconsole.readthedocs.io/en/stable/) [[PDF](https://media.readthedocs.org/pdf/qtconsole/stable/qtconsole.pdf)] - [Documentation for Project Jupyter](https://jupyter.readthedocs.io/en/latest/index.html) [[PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf)] - [Issues](https://github.com/jupyter/qtconsole/issues) - [Technical support - Jupyter Google Group](https://groups.google.com/forum/#!forum/jupyter) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693579838.0 qtconsole-5.5.2/README.md0000664000175000017500000000711500000000000015610 0ustar00carloscarlos00000000000000# Jupyter QtConsole ![Windows tests](https://github.com/jupyter/qtconsole/workflows/Windows%20tests/badge.svg) ![Macos tests](https://github.com/jupyter/qtconsole/workflows/Macos%20tests/badge.svg) ![Linux tests](https://github.com/jupyter/qtconsole/workflows/Linux%20tests/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/jupyter/qtconsole/badge.svg?branch=master)](https://coveralls.io/github/jupyter/qtconsole?branch=master) [![Documentation Status](https://readthedocs.org/projects/qtconsole/badge/?version=stable)](https://qtconsole.readthedocs.io/en/stable/) [![Google Group](https://img.shields.io/badge/-Google%20Group-lightgrey.svg)](https://groups.google.com/forum/#!forum/jupyter) A rich Qt-based console for working with Jupyter kernels, supporting rich media output, session export, and more. The Qtconsole is a very lightweight application that largely feels like a terminal, but provides a number of enhancements only possible in a GUI, such as inline figures, proper multiline editing with syntax highlighting, graphical calltips, and more. ![qtconsole](https://raw.githubusercontent.com/jupyter/qtconsole/master/docs/source/_images/qtconsole.png) ## Install Qtconsole The Qtconsole requires Python bindings for Qt, such as [PyQt6](https://pypi.org/project/PyQt6/), [PySide6](https://pypi.org/project/PySide6/), [PyQt5](https://pypi.org/project/PyQt5/) or [PySide2](https://pypi.org/project/PySide2/). Although [pip](https://pypi.python.org/pypi/pip) and [conda](http://conda.pydata.org/docs) may be used to install the Qtconsole, conda is simpler to use since it automatically installs PyQt5. Alternatively, the Qtconsole installation with pip needs additional steps since pip doesn't install the Qt requirement. ### Install using conda To install: conda install qtconsole **Note:** If the Qtconsole is installed using conda, it will **automatically** install the Qt requirement as well. ### Install using pip To install: pip install qtconsole **Note:** Make sure that Qt is installed. Unfortunately, Qt is not installed when using pip. The next section gives instructions on doing it. ### Installing Qt (if needed) You can install PyQt5 with pip using the following command: pip install pyqt5 or with a system package manager on Linux. For Windows, PyQt binary packages may be used. **Note:** Additional information about using a system package manager may be found in the [qtconsole documentation](https://qtconsole.readthedocs.io). More installation instructions for PyQt can be found in the [PyQt5 documentation](http://pyqt.sourceforge.net/Docs/PyQt5/installation.html) and [PyQt4 documentation](http://pyqt.sourceforge.net/Docs/PyQt4/installation.html) Source packages for Windows/Linux/MacOS can be found here: [PyQt5](https://www.riverbankcomputing.com/software/pyqt/download5) and [PyQt4](https://riverbankcomputing.com/software/pyqt/download). ## Usage To run the Qtconsole: jupyter qtconsole ## Resources - [Project Jupyter website](https://jupyter.org) - Documentation for the Qtconsole * [latest version](https://qtconsole.readthedocs.io/en/latest/) [[PDF](https://media.readthedocs.org/pdf/qtconsole/latest/qtconsole.pdf)] * [stable version](https://qtconsole.readthedocs.io/en/stable/) [[PDF](https://media.readthedocs.org/pdf/qtconsole/stable/qtconsole.pdf)] - [Documentation for Project Jupyter](https://jupyter.readthedocs.io/en/latest/index.html) [[PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf)] - [Issues](https://github.com/jupyter/qtconsole/issues) - [Technical support - Jupyter Google Group](https://groups.google.com/forum/#!forum/jupyter) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603554021.0 qtconsole-5.5.2/RELEASE.md0000664000175000017500000000124600000000000015732 0ustar00carloscarlos00000000000000To release a new version of qtconsole you need to follow these steps: * git pull or git fetch/merge * Close the current milestone on Github * Update docs/source/changelog.rst with a PR. * git clean -xfdi * Update version in `_version.py` (set release version, remove 'dev0') * git add and git commit with `Release X.X.X` * python setup.py sdist * activate pyenv-with-latest-setuptools && python setup.py bdist_wheel * twine check dist/* * twine upload dist/* * git tag -a X.X.X -m 'Release X.X.X' * Update version in `_version.py` (add 'dev0' and increment minor) * git add and git commit with `Back to work` * git push upstream master * git push upstream --tags ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1714924509.6696565 qtconsole-5.5.2/docs/0000775000175000017500000000000000000000000015255 5ustar00carloscarlos00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1450622414.0 qtconsole-5.5.2/docs/Makefile0000664000175000017500000001731600000000000016725 0ustar00carloscarlos00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " gh-pages clone qtconsole docs in ./gh-pages/ , build doc, autocommit" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* rm -rf source/config_options.rst html: source/config_options.rst $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." source/config_options.rst: python3 autogen_config.py @echo "Created docs for config options" gh-pages: clean html # if VERSION is unspecified, it will be dev # For releases, VERSION should be just the major version, # e.g. VERSION=2 make gh-pages python3 gh-pages.py $(VERSION) dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/JupyterQtConsole.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/JupyterQtConsole.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/JupyterQtConsole" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/JupyterQtConsole" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603554021.0 qtconsole-5.5.2/docs/autogen_config.py0000775000175000017500000000175700000000000020633 0ustar00carloscarlos00000000000000#!/usr/bin/env python """Generates a configuration options document for Sphinx. Using this helper tool, a reStructuredText document can be created from reading the config options from the JupyterQtConsole source code that may be set in config file, `jupyter_qtconsole_config.py`, and writing to the rST doc, `config_options.rst`. """ import os.path from qtconsole.qtconsoleapp import JupyterQtConsoleApp header = """\ Configuration options ===================== These options can be set in the configuration file, ``~/.jupyter/jupyter_qtconsole_config.py``, or at the command line when you start Qt console. You may enter ``jupyter qtconsole --help-all`` to get information about all available configuration options. Options ------- """ destination = os.path.join(os.path.dirname(__file__), 'source/config_options.rst') def main(): with open(destination, 'w') as f: f.write(header) f.write(JupyterQtConsoleApp().document_config_options()) if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1648405558.0 qtconsole-5.5.2/docs/environment.yml0000664000175000017500000000024100000000000020341 0ustar00carloscarlos00000000000000name: qtconsole_docs channels: - conda-forge dependencies: - python - ipykernel - pexpect - pygments - pyqt - qtpy - sphinx - sphinx_rtd_theme ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1615943229.0 qtconsole-5.5.2/docs/gh-pages.py0000775000175000017500000001010600000000000017323 0ustar00carloscarlos00000000000000#!/usr/bin/env python """Script to commit the doc build outputs into the github-pages repo. Use: gh-pages.py [tag] If no tag is given, the current output of 'git describe' is used. If given, that is how the resulting directory will be named. In practice, you should use either actual clean tags from a current build or something like 'current' as a stable URL for the most current version of the """ #----------------------------------------------------------------------------- # Imports #----------------------------------------------------------------------------- import os import shutil import sys from os import chdir as cd from os.path import join as pjoin from subprocess import Popen, PIPE, CalledProcessError, check_call #----------------------------------------------------------------------------- # Globals #----------------------------------------------------------------------------- pages_dir = 'gh-pages' html_dir = 'build/html' pdf_dir = 'build/latex' pages_repo = 'git@github.com:jupyter/qtconsole.git' #----------------------------------------------------------------------------- # Functions #----------------------------------------------------------------------------- def sh(cmd): """Execute command in a subshell, return status code.""" return check_call(cmd, shell=True) def sh2(cmd): """Execute command in a subshell, return stdout. Stderr is unbuffered from the subshell.x""" p = Popen(cmd, stdout=PIPE, shell=True) out = p.communicate()[0] retcode = p.returncode if retcode: raise CalledProcessError(retcode, cmd) else: return out.rstrip() def sh3(cmd): """Execute command in a subshell, return stdout, stderr If anything appears in stderr, print it out to sys.stderr""" p = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True) out, err = p.communicate() retcode = p.returncode if retcode: raise CalledProcessError(retcode, cmd) else: return out.rstrip(), err.rstrip() def init_repo(path): """clone the gh-pages repo if we haven't already.""" sh("git clone %s %s"%(pages_repo, path)) here = os.getcwd() cd(path) sh('git checkout gh-pages') cd(here) #----------------------------------------------------------------------------- # Script starts #----------------------------------------------------------------------------- if __name__ == '__main__': # The tag can be given as a positional argument try: tag = sys.argv[1] except IndexError: tag = "dev" startdir = os.getcwd() if not os.path.exists(pages_dir): # init the repo init_repo(pages_dir) else: # ensure up-to-date before operating cd(pages_dir) sh('git checkout gh-pages') sh('git pull') cd(startdir) dest = pjoin(pages_dir, tag) # don't `make html` here, because gh-pages already depends on html in Makefile # sh('make html') if tag != 'dev': # only build pdf for non-dev targets #sh2('make pdf') pass # This is pretty unforgiving: we unconditionally nuke the destination # directory, and then copy the html tree in there shutil.rmtree(dest, ignore_errors=True) shutil.copytree(html_dir, dest) if tag != 'dev': #shutil.copy(pjoin(pdf_dir, 'ipython.pdf'), pjoin(dest, 'ipython.pdf')) pass try: cd(pages_dir) branch = sh2('git rev-parse --abbrev-ref HEAD').strip().decode('ascii', 'replace') if branch != 'gh-pages': e = 'On %r, git branch is %r, MUST be "gh-pages"' % (pages_dir, branch) raise RuntimeError(e) sh('git add -A %s' % tag) sh('git commit -m"Updated doc release: %s"' % tag) print() print('Most recent 3 commits:') sys.stdout.flush() # Need 3 commits in the repo before this will work #sh('git --no-pager log --oneline HEAD~3..') finally: cd(startdir) print() print('Now verify the build in: %r' % dest) print("If everything looks good, 'git push'") ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1714924509.6696565 qtconsole-5.5.2/docs/source/0000775000175000017500000000000000000000000016555 5ustar00carloscarlos00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1714924509.6696565 qtconsole-5.5.2/docs/source/_images/0000775000175000017500000000000000000000000020161 5ustar00carloscarlos00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1450622414.0 qtconsole-5.5.2/docs/source/_images/qtconsole.png0000664000175000017500000045324100000000000022707 0ustar00carloscarlos00000000000000PNG  IHDRR!A pHYs!3 tEXtTitleIPython IDATxw|TnzJ zD^m Rl(?˳+*EEDi!!@f۽cK&!>?ݙ3gΔsg=wϞ vW0vHV2Y @55֕4~"-BBVG G:m6/1o[;v_dc8,ɄLR¼_ /ϒ_/_|i\B \vR !S\Z5x4s֣3k4*bHk_duMc+C!b%E4VÊ_IEdR9׮NSRZov 8r-$Qj0@1`d}?wM޹cpNjz˩jť/_p%1 ӱDi 1N$HE"T)"9{1aP4X{(`9`00HSG%dOQ|ĒD!0p<_+SjHde=as^ O"-l<YFZڹ|'Ә:sg9mt^7>ݜ:=_Y[nl[JvXءwi)s,8$b= JkjL]UITviYlo"];6Xg>>^+U%$ 28h`>C xp8Pg醙2/RELb"xj1(La0:gv=~[\ ۘwu[I4:ߝڡr㻽/\&NC5+6|!jْ0~&.' 2\ѭsB|skV.))Q "\ѵsBv1&YMx{sWJʬE StKL3-0c?_*]`EB!ar y"yzZ' Xg΋&L"c ÈHeeܰy}g뻳ݎJCK&Ϲzt߹Mh>y±O:ZgG#*M{>{hN$mzcZ8y!ЎEXu2BtmfS amwTr>*ryZrRh{mE v < @\bpܯ>l"qUb 32'b٫)zHcNC>[)zE;znZ 0PSrr!kŹ(RU3Qֲgm ii2Bp  2PZRN/^ UQǙhSf\vPCWB:SgD!g5޶s5?.{+9V',syλnF<3!3Tм5y,7sR hZ >b>Gl3x(IXׄ67aL*F60F*&$A7(mndN1>x`EeӍ==R H jc88 ϊڜ`158#X;(`16 ,[[[eVYDrD!hQZ=۳RFaUpe}~UmZ4DZKB~ހI5x''󟵕%j, NR B"2,&h>LDŭ&@iY=.aN^6| 8g[݉g(@lվU9O+9pv#qr8cPTYY`01V{Ȩ+YcJKKP( /9א8R,0bO'_:Mkn.碌Ҥ2Y]]ekjk==1 #ɬoE ^}q\wX@`z㗘p0<|Ţ S//vȌ6߶agōs8I舉}OebAaL^ >dtTwznPB'`y]Ι]" F"ݠb#|,ԩ "=C=XueֵkfWE{3ڜE/F[}TZiI9u&ka+:-2;9Ǘ9\o`wXNp:M s"ۦ"8s >@zr/`Рd`8~a;" 0ƃK>zC#F8|peUU8#-(1FHlS+k`1ջYsLbzrcYCN8\ֶ0s`9t\R*i4u5ÈRٸ6kQU $'m/?;K26o,wȘ|n~y1|Dt(p+'*\HV1"H9*D!o`@֮GdEq'K/xJV;GتL,<20]89)Ad2nGGrK4a#p\umfN!c[+|V>XmA0f2!QB"q` K:Zi4`ǒ+ZS?` ּ0\1H;_|xSbgl"MuG#5xpMqvѮF6@~PD^RMDYHBH"`oq}.GV[rfOqPEMz`حZ`@aH 3 0kăji/u uLwDF%@]aZ,72*3aFH|`,%y_'q޲@_Td@wPj 5r\Ga*WxԱ\O|rO_c=/86bQ@HsoZgFjJ<-)M ҂,K4x!? S.akU19rN&?`0B pY~0 krss02k e9d%)M+(I|_߃s!B.(  `sQ:!cRêZ@dzuˢ>}cgA X@wWCz!ƜB@ZgV=V7&NT_ʼnHbFl>)@H,t,H@,"@VA&r H$Е׊u!GG#LS 6j[mkͺ,\ñ#xX8,89 , Iqeֹ(km8VWW'y?qla0T @$`4p} BC=cDbD4:#Q8Z^F󝗶QftZ1bDDqju&uGBX0ȫ7@HHشCo! @ ca嵳0bF$5V67Get:_@-zadܐ1$1 @狺=F x j " /@ : S,o h3~g+g#VS)Ҋp<г]q`:i_ճ/X_F`Ř)F 1NNNVo'&'2$s,fBL#,Yޞ^Ç0,PݓrQ5fկ3t:D"EEtI,[,ZBبD*3IZa Bq@SmχbĈ~csgB8ԗa1f @ B<=LU7_*#CU9u"WAWUf]=`,iJ3txYБҼIX02ga[ezo^]ޖ1l{skKNJtL0d0˨8z?cL7A3302M|!8J??1"fA'NL>zJ+ı  Y8N$YB > ˲CWwEvy!20` df0F5"W&cJm"4Y0xI!$ي2-;_Tu͢##6]SNgpE 6GB"q gQRѵ|mD"PpJbg_Wn5 6TiM a!q֖ݸt>% `^1D[{oM利 *Yn8Z0 ғ2-Xe9?rH$BK%cG JFi| f9l WVW 8D$1 B 8pɕՖ}e i!+G*Jpk4zD79]#`zd gpP$h\a#̢)=RL`r -b1 &Î=VurcID"U^+Y:gߎkƓ@4@"ߴF uf`^2$"ЁS,r{Dk5BefiӨ5j)`%Fois[6'Wk CBŀN#F BȉS;՛9neâZy3m`,ZzS^|5T("'v-)-ɻ|5J!C0q1aC CJzD!d\1 ok&EuFСҏ1{o7hs4eUKW307d ,[\ZwjFiqV/^(3RZZwjFY9rʉb[QhWaK+K ,NUk(WjԮ̌b._!Q 9mSgЩJ,K}iiL ɹ%FjBܢ (mõuZsγ45GtZD^b|ԅL+Ij݈<"-[Ek:AAĶJ0?k~u\a| O`IS׸{5(s j4FbWD)1 0=BiZ`'9Gmzl#7Li HFN!(7DVyP(ւ#(m8!"Vۣm䖑B!}+W n2 B4/b0[ BP(͋>oСZ BP\(̙g4w>}q㧻w,xzgV6 c=h.}[o`e>‹Y4CO5V[F/WKA(sv;#3Ξ9j=)W*J?_ū§,]EU%VƔ=ݹ%E)kU5sm:9gc^ܕ3[gi7>|r_7oЬUdLz>7wY7n1>Й lWdLAv#ɧ={|K/^|}w+SXkRA/~.iA9[_Zƙc?rL.#zGML; ^;P$c*GԜ~wd~Q$ZhbAdӹKF vϕ^αv23_銏r olݴ)9) [:8+'j1O10>S)t_ ڹ Щ5 ƘS_ڄ Ex)oXudNoE~|5 nh˘O_Ҿu aI'F 6[pJYkr+}vdZc]hdxKe.t'Uu=Zϸ ٚ';=~#Cz/P}qIq &))LۀA>6b}A,hR}/w =6 >BUq Z߿)Ъ΋ ̘ȅִdh rJM@W;Ov9?oO{^_ U=C{]+Yx1TU)AvWTUi7,^,bJ ?܈{kK/[8%,8POO]9,j{ˎU㚓Bre_ܵ{@}o}K>HvkLx`r\js5\T zNԗߩع+:8+7"l/,}nKuZeѓ_%rXS^\+-RNJe h3;1! WҊdRc:x^.(v(tH@ҩ޾;3W}p/wZ _ww`#S$Ejn=kSvNt }CV >P4aXE2H LNt'Ak`dRwEvˮmo81ҌTB9_+JzUqqq.nr0Tbg#B;^AgKz)"yX"ax]vVUu;k,yml>/<=8hʏJEA=Gr/ƺIO"C5fy07a{T._t_V:9=[׵9橞b͜MjVz[Kgf)K{e'-X3atzՕ_9U˱ZW}hs\m K=6. IDATD0JNjUAݦ|¸}' j.>b^N K3 ;RCҚg# N"? Wd2 no*+.g鯎O6so UwS"o7VY~+ tPut4~x%.`ڟzI}M)mjO?щ|j*xtәJұZk.Ǐ|AK⯫%%ɟ 82g(D/ܵ{H &/.YÿݨyOt7X7/wgh.{ ݅7z) 3S|`zGX{? mt[ ApR 1sLlb76se?)|TlhxX}k j~8!e:jOK=0: $=x+C<7<|($e[sz>1ܵ|ޠ.-g^`w*u ~?`pcPO'UdgԲC3dֶ"@P+\Qk⾳ R֖Zu_;q<kI=@(@T-]wѱqW>/=YjUUetތu᯼1O񹛾q:'me.SzQiuF]sz_ 7.(Zsy{?wbޅ>g~LJʙ>OzNiujXk7K,)+y\ { I)<ƼFobHhwrORMMI9s:3A_>Q@P,:fn4 iOrڠyP Cnڬ_^RN1W:2}KƓme'=(V!R |2$`{dT1ckϼ?4,xUf;_1XȊ%#);\)>dwS-;pLeR{bXlIPt])1w~\#}{Kg.#Mݍ-/1Xh? k9S^e0=s?SkX.}Ё]da 6kĘj~TX^6x@5kcyAx "NA7}tBSwuINj|_tPZ9ƓY g<ɉf}cʨnɩXuߠw|1ȧwD}fIGgDié([_J~*N IMg>L 0Hk+߿pMPtɒz3++M6 X_[]e#rם+>ቪ qrkEdw m/r d]hL)ZݿK8q>GwϮpj YZVmh68jR(L]P( g!Q( Ҧ ywCBK) ҆ӧpw7P R{>\*=Ifr->Cذ/:AvݮQDrp?P[g7FyArzW6<3\+;?TBJ6 .ŪI괷3PZ={|K,oQQqӐfT7.\ |W jzE^A^[) ̎-F{kAHu=t Z-\S8Q2@l!Fnj;- GC,kQOZțH$JRTKRT 7MSf}a}?\4ӀE>28׌Uk0%W<7V)W}xR#f__v.k*NB0ӋZ\w'Pȕ=+G PXI1www k1^.k+v DTͿT!M> ^yY͕E}rB>L=_# -o(:AreX%׌u;o7gBTc/6\MR(A|LGRMāy>!}f;.;fHQ qJ~nł}QKuEK22ܨfuFOTȕ]''_igQrB~9sov [Upc]B}v]:2'&HiUz*M8)3; 5{}C;X5Z?!2%{*-st Uaʷ5bZrD"ëU+9i[I'۷g3~Ʒ'n$Rm91A )njTUcn>tU΅5A0FZ7ٟVuo c9FQ9=rV)MVҖs!"t:2':HmCc\w"3+7+;$tDϓ[S]xMWyO߰{)ŵ|J (,&h؅o?x‘EiuD roHR4˗3/,'"Hq65FGr$'!#!IϩҲJÞڟWVRZVrla7I{<7Gԯtj+R$vN؛.jj<†NGTwXiskޮjOJ1䋷b(D{ᭁ1QQTuuQ[WKeX=Լq|qE}9rP|bivRN6-`yuɍ hFC?9˘kS*\O F> N_?  uxܷ{z"$!7¥nZEt5ͽK7tu+::C\ Ii0b O= kAAa~ͻy,{ k o&7KHo $[0 E}z0"aM4>l dD~+2meFWz{YB3?=5_v9#Nq݁d:c].$Tf3Ԉe{_zr7ץ웹ukNsR2.B)uqiXN[uzZ5%:MWלUij /i9) n|I;<4]yy#-oog+w>ر@$U ?%"48ӈykj\)ūVMfzv1=-;\2qoz=G%N_`Ӛ#tDV7RnqmG*佗\4QbcC=)S+7?zzlBhXt;YIy~bjhrBQȥNOoz{_k5*N~:kXbLdxD}_~N6;'׈d:Xww7,Sxx0ܫ(t =9YeiIz]+:I1wnۻ!Kot\o tj =A_?{fyoކa( c'w{6m~*U?__=Zo=w7b9-IVI8nm e?1Hݰhw{mP(gan\nB\P:A 3>3sP(JG`P( ŠnBP(wQ( = BAPGP(;( rABP ۣP(u{ BnBP(wQ( = BAPGP(;( rABP ۣP(u{ BnBP(wXuEϚ:|KliTf+rB{E-[wpa>Uɮ94/3VBIXs "(I}8{%zxܞԢ}$]+(,9!C>cwnIQviUg\w9=}l |kg֎Zثxko ʢ R!uw”~s7i="reDϩo$MΚg<{1?}{[K">{v= =zݽ>SryԨ8W ~sOz0GVudNtS25g>wA=̕n+{vQA^_АҚ9:/&d2v~ݟS?lDh#*W'Zl.XN3WX;tWi:Rݝ>s=g|{2Fq!-r$09ͅzGO^\l)ۊY&b=#PsG<%fnOr麬"#Y7B]b=t퍯FExUz]i=CGX:᩻csKǕ'*!^}R_yIۊYb!ՈX ?2nߕX{CJבU"fʝŒŋ9֠SkkjTUee%%EE7rsr23n,YqGZ ?Y7x ňW`ْ0sJ,Ήo_H=!^UU}v1K^.' o7O RQPѺ*ҍ&^~ՇODrz [Pk!oϺ؅Ozaɭ5|م16mקb^<}úK/O+]_{gzG?_Xu zz<4sR7#UT#:2FCNTbХؒmhīZ@8Eg(=C].P&T񖒫:wKӮqsmv\'ס/=X9b芮xEjL!v߂jD}vPf!ǟc34X{]7zatP 5" |#ecaVMV$0 zkNgUc d6`^&[Z7P嘓\MRwD7OrһГU nwʝ_tuzXmmMm8 ]g>9=mzX]Η Jzt띏eUKmxDŽ ŲΏΈչ?ym+3u C3dֶ"Ӿ^Uf~4e7ok1"!;I!~LMqz8my~_v??)&[t39k=>G:3.bslωΤIII냾TLe$IQ/lU7Uf' YC]*MMJ ey`^g~(tϐ!N 6^=f/? B6-; c0wEv]b#$vxhVF*-~7?zzlBhXt'F1Mt=lZp +PiZ9&wȄO/K!$ '$F~n={ xr:g~ /Bǽ cڏ]5^>j}\ᑖql#z=y;VH|"e W)xW5M:>5[snˊ}~11q=&=[{dk`'U ?%"48ӈykjO'>oo[5=Ep1K,\~ ڐqhu'wEbiDιЂ3xPA31eTC6Z>c}Vvߑ 's͝AM^Ol:i?{&DOЗ|:f#Y[֬R(.@gJ tɒzSqq˲,ifك*U?__==7b9-IVIf{R(M(7[~Oosn ?b;:&T!ew IDATIacѴ (:3PZ/nGKFan\nrgŒP\ =KP(;( r=)=n#E4F4I4Fxk!֌4m:h9` ό W*O%5q>"5'fo V{틆|;cw':RzXkA=zRf&-EJj#\K @NTw>hd>JdGgkLݼb_ɻN4.VMͅқ>Z IO بhPj#\$^|ӭB#Q'@4lե+9uCs/1p)1nqTĈ59p'̅vC9R(8G#W$g'AIVgΌybȭ ղ"GCVR=/=rB3|NXwR9%qDE'.|wxtK( lչ=E ֱHӊHňTGO|c 77>l{!܎g&8uD4.#dRĻS/w)5]8Yn~?*I_q?,wGO/|dQԕ$ /9Ԉ8< w'ᤍ9[𧩵-MB{ׇOiMMxDvI< Rtl/|tUn57# @(>t*61ǣe BR^}aY h-Xu)Obg(-Ιg|Ң2-Vw"BPnoH=$γ_Ow!s]d=/V!W*:'v#G"3sgbµ;KjB-όx{\ߟldzF&zEs~|IsԝG&_06RTP(Vpc+Z|`G@f?u0ږi*5|Gm/)~>{ìiKӇqFnֿ;޹;AiKL/ K_gҮm}iŢMf֑⬽ω~nWOR*ot\+b{v=Sl̤>f&]u5zqHaaU'uN hSsBesʂ_ξm%$Wkd+BPZ Nrmgwm_ڛy1b'LJ`oY$UHᤘ^.3o)kxy!]-eםH L c#Ո.P( GZ^]5B%L^Ȍ7o졞~mh3h8F<΀;_Nd~,ܼ2ܗbc!wLV`Llu lpDoK3BeH5-ޗB[!nc8=Yk.B XVfp!|8=s\w0wF=iN?R @-ENDAxRӑS$jL%όBsWv`G?~"jY/_?اý؉k_zq^-Tw" )5NQ(w{':g;˼,-xܐGxe1K?P~ϝ2Ppxfg7~;>_8ӺbM Ban<͆7hbu'" ͑X#4Bi-46ޞ>Cv={vm|ʕx{5_?HVp<#) j%+1jϓ~ob BS =!A^vr8@C6Q(J[C,7jhZ# PHNFP(G0GP(+PGP(;gyz7 z!\y` Ҷz]sP(;W{7/vzfg2~Z=,+;/u#-ԞQ R!_޷8Tۤa3^tGZ8T-h.kSO, 7~nł}QK=Bi=]KuԕˢXk47Vx鞵Yi;']_p+QZA;y?Y@q>>smzI/{䗖%={⚓Bre_ܵ{XgD^-{co? n[?;^B֕TP^pB@UZter*%L[#-C) :d=ut|wk}XMv=߿<,J0zB.;JR45QU+guzpjXڟUs'(ޞ5V?ĭ=uP0kE`:Bp;DKY14q#Y6GuNM(JKrS`}ӷ[Q&^zR5ɖhz"W'@g%P(m^"nd;]([*/7(pw{4gÛ~nv$m34i:@[V eO*"2DER_" Y6:ҤI4I׽?.$${{}99 . [~<"P#%w5Nr՘` R:NY{pkz卉[H Ibˉ}?z Eb[kWGBHIZ~ ru(~ƯY D7u+˝̆/u ikE-SBEIOݔqQY-X:}PBc~jU'tk*4ZW/Fg%+\p= h@ nH$GmW8z)6(෍;CBej Y:: @,z%F^ᏙѝfKZy`F9k=0[By}V0Cj!ѱsQ}kaW[ʒ3<>&?vj4i6[v:[:,O<6GDRK *%7dw#t5e`*1< mnsP{0ga1Aˎg.0 <]%7sbaFk)h5i܅oƴ*SZ~OÑ1ة$͑Syf /e;}rPnVѢŸz^./2yIJY9Uqban\9 ЖjyUMH{\M#wMP_ɲx^(j3~pj^r}ee'E?f˞В{d4jf<}3P6,| 4;} qmVl6U z}NhjURQR"/..*,,w7?o'<)q@fϴn=앱~|3sB܂{e8ݮ{jg]s._{qU,ZhUa^AecG$؋`앁zPS^SuMchKu Iu(H{\M!4Zݝ/ShuވQ#Fm0)3dO!c'|E'O}quMKFT3Z5@3rڽaF΋ ADc{ @iɑzhS=PCD>5,H#g,e%Ò1lQneue=CD s),;9A&%^~m&TZ*cE[kHÏ#lD&pE,p.ڰ|X4[TůHsv]2 کת 7]<}h|Χ<@\ttC3~YjXh ȽӅrXM|eRd{9r0ݑgoEskrϽz R\^qQQdULG(v_jpx;V o mtc|!/ձVP3_k Dͦ2ɳ p ڧzN.l=pc@!I r ʩU=%A y1u:t uig3c%e brݬ>wEw:WW_{RgY= s,r_MO윏qV%GAdxsK6'`zT٢+Y%NsV~/+04D={+|jdBVr6R"qu kcO!;ro֢냾i7C~>.SV;-u GTc?!]҂au/ „Ӿ4#(nwˬ!iPhXŅFuIKKon\EFtSD*HSW ҹ]T< U Y'-EE7l^߂Q-5ydا?V;]S_p~K+ʂ3Vq" K ߗ!^R}H#,Dm[E7l^Q+'rMQcUΦ?^Q2injDFQ҉=eovkoz$Iu.6^JQ <. 붭Tm! 8Nə?ܼtLZ˘(Iڋ๪QM6Ѐ9Nدm-Fܞ郞HTvg>93G;>=1&2g:9GBmo,G$oz᧔jռ! pW1}*?<$VcbH 9VyX q^5hQd3Ծ"[x)k8@ ;}dփto? 5bĆSrhNcGF H r'|tG^Fx% wj# po8Rm4MZl {[)U!~\ӹkCӻE]uKqoUK@]vZO݆;?Kե?&{s_~|9 M"*邼bHwRjQ 5/d' PQQ(@="L6]#uvy!Ux- @QG֏\d-O) आ B1S#*oN$T+B.yV= }˗ GVG*Rm4ԞG_^1am#\W u5.35MGC[@uUߞw=C D;G)Q '1А$ҸU.R# PH~"dVGdw-Ț:j ^ IX]PoޤRk=;ɊIk''4*vw,,?^ʂ\/n/ee]{egWL~R G)Q Y!Ix{ ,_kUQy/W;Qgٙ C_OUG>y\^ J^GRpk۫8W~Z +4k5ώ_tF+IIIJ*p2ZM[6Eg`@$l1lwe{PJő~6߶kƣew $8#$I`߆eKD'ǩ>Y {Lܵ0+-esvmC#mU<i-Ms3)) 9+.MgׯH9 E<(Ck& k¡:XyBdoŚ1ljoܾmr,H0~4,A6z—N@|jۏ8}eץ@H3NA @PA @PA @PA @PA Aq4@  O@ HsBn l=4#ڃ@ H3=4#ڃ@ H3=4#ڃ@ H3fRY KXt^C~%w}Q*ϧ7A?j:6A" DBSj84I=J w=[`K 7Ąl#%3"<Єs%iH8 &~[@ F#Zؒ/>ܪ|;_HBN(nMLZFy`КZ i2Uf*FP@(BE0ox{gZ~Ʀh*uyfH (^oMX5 9ŋǬf'gJy <6'g&wp1i-c$i/nw^,N<w DqvB/~(P78ݿT$$/NK#SeBH(5; (~s9[v۬>gY'g&uhgS'DFwog*$qKw z%t_}$WfSҘr g~4ẘQO?D04V;۞ͪ6xTG7;s#Z_;߭/?<]DNY//ΔKP*>u}םɓ^;(*;a]7Ѵeg PxؒR:-f힗\߿w`A[27#?MM?TH~8g{&Is1ڣ'|4P2(Sxo0P{md+ٙ5b^N :2==MZ&#Iu _9NϭYo,|>E Ms{{iFIDAecG$؋fԯ OeR._G1-c9~ ev;cK}b.8Q#6ږCu;2:U#x]BMT) ~"'?ǀ=w[O[~Bo\ԃU7YkF@aa,2 B1SCR.]>I61H<Ɖws֓-L6TF'{@FxerNA( G c͒KB+:y{Vo)}Ԧn(S*OB5[BǩGxd j~aÄB!@$3o6}ݎ;oܸ0@ffΝ;vwСCGmZ3gߞyz~)))ӧOlZxbx7ĐzO?oLٳgdddjR>}z۶ms믿q`֭{ڵk /n߾=--{O޽{РA6o޼]L<=s̼y233,X8zСCSRRzO?%33ݵɓ2|D~+W\.CΘ1ڵkm۶ݹs'a_~I0 [n݀ :d61 J={~ {wu P4hѣܹS\\wJRO>)+++**:ra999999NǏWT.]j׮ݎ;0 ;uNl͛7 ToVT~"ð={&%%>}://{1ajV+Q0l۷M6NٳG}D r֯_ߡC\ æM}СŒSbf۴i|rB)*gϞ}}^x~ >S?M0m۶NQQڵkQ0~Z_"""J%aR0l֭r|}0l6 _`?^V8q"::A;Ic'fZfSѠחtZLV*%%‚{w} neB￟7o[tܹs?X,`ѢE;w4i)BPLH$z/^ܾ}{@ 8qw}iFp2Tx166Oђ:u2ׯ_w8}￧LOPa۷]v*wٱc>m4oѳgO̙38@Zdd#-oF^/RLfL</_}ÇX,|,H^|={UJ(33.//''O̚5+##>Rs}w5Mdd?_\\|MӢE'O7Ë/x FӧOիWlЩSn޼y͚5rݾ};VYYYvvNOOtR}YA "P5 c ,l۷oWu\.Wjj*EѶmzWo޼٪U+L6eʔ PTH$DFFz+),,d#eoʕ+;whѢg}R}  &%%%ZlժUqq1~844_:gΜةSΟ?B; ͣݻ^Ոȑ#>+['F]M!!!`0;w?ӻwoN7`Pغuk<f+[|֭[Gk׮pwBZ͛ڵ y&F^5vr\ϟ0aB˖-ǎxZ"HVnwٳF~4M˗/?|4|򬬬j׮]Tո-@guܹǷjjԨQg-XBavþBuVV tR{쉏jo理߿˖-ޕK,µ)S[x.ii06mtU$55SN2lɒ%n\B|zND@:DҤRe<a999dff\}z<z}aa!nW\.߲e TXX}f#n>|'prر˗/:ԟl.]2 xքBaY# F.Ȱap$8//)VJuJp .T @KKKY|9^{7|3,, 5\SB j0Lcǎ x۲eKjj*J=tЕ+WbL&KOO5IgΜi۶mllL&С>ԋa0xBJ۹s猌 `!RX\)={6lpR0@ 8rȆ 2dHYYYxX,!,,L 7LT*Rbxɒ%iiiqqq۶m8q">-'FDDĸ8#P}W &ŽӧOܰ H g.|a`iLL޽ZBQv4xmS #q: t?8by_0 `Zxx&/B"Z8NAtzv񱲿NWxAAA EQFget:QEQ?=Ϯ]mhoU?êP(6]ͳ%@Pk`(jj[j޽{̙SiHwc fT{H#%11qŊ~/^mوV;w0 (&FaXVVVVVV7fΞ={ڵ8ydvvvCKQ?~<77@ ȣ՞nOOOOHHȈ^hP3gNrrԩSN=>>?gٲeÆ _|A7n荐bݺusΝ;w7,ݻwرcCKQ'%%ߦMa@ l:?A'tjp\2lܸqG_>''G*:u/** G >|Μ9W^MKKDe˖M6-44->kժU]BCqو-Z4 ɓ-ZmhAj۷oЂ4, eD¡RXPT X,֘1c디֭[￧~F1a„3gۗ4Ç(:tP={JR@޽CCC믱cǦ޽;""K.ҥ\~… u> IDAT 9}SnŒ(JnѲk6kwmZ|9A}51`?&u{0$$ДCBBߍtQJ&_D"5ŏdt有yWT+)eC|a (әQ~i8%4 s 'X/'T}bMU''d $d~EBjz݄1y?P(l׮k״zd{WS>;'g@(ʥ}'P< &J@=/7nufs?u֩S2 Aw\f 6p9r#2V\O;ۙ] xn*V*(VbL*+t2@{6E|} %)XX jD WdR8BK%TE6R)әs͑|J51Q2$2LYf+iTBmDj4*L6DdR,:ҭ(>bqRAB'+c%be9'Qib!G54z("DeёҲ%舒RL)/G8 Q*S1Ⱂ|yA./5I}cDteXqDq>6ND*ٜ*L^6Jg xD')$BULTX( 12.U%jcl4<6X4*Hk_, Sa%ZҙQZgp4[Wes+_sZK܌ײ$2H *I,䖖qm N6j]Eܻ 5?rBheH~hYEǬҚE<5M*-ˆP"Gu(W3)7nܸ=zlݺ5--?U*աCK,Yn]7mdټfdddddTzs\z~Q7n5&l)Zrl+T ^d-. z);Wn؋K4fb˕Z^\ڜJjs*Jm6JSa9e]3kwmvVoڜevKYVa9evDe؜%*PS6Zmbsv'*/՛2` &H3mrQ,*fwfASnކ7--)՛mR搗-VTo2ye 9\R搗v\7[v](7[ErlqbluT[aUH>G5ѨT9`9\VGB5YEr~Dk9JF͡PWxT fC^Z^le*TVKTc\2撫6K3W.RmRj*lvlg0h:fsj+8&ͥ,3Zl.ʀ׎*՚lJg9rRiM(J[yi9ʽ[b-r*)M\om e]Vnq0dTZRiM6S5yljw6[a֭[ϟOٵk}z'|W_U/@6mƎl2D"єYb]q9ץ f,:ݧ:{ "3˽N |Bx_DhS ` #O|X,5E_!PV0#4_1̧n_P l. rTAa ! $J(?%Zj$^;>Dӵ}K+rWk0/bz XSw':a@Fjv25K63Qa2njjBy"x6hРgff8qB.\.ׁ.\0~9sT7k,gΜ)));v,1^vmÇd%Kv܉olP*F7~BQfT*o|%=xJ㑗tˣ5zJ5&)hm@FA uãG{fYb~g2 iӦl3y{_A#k׮-Xܵk׼yX,VFFG}/XFd~_hDF)>~2&7,:N}^".:'ªV`%'abOB< Elvހ{MrͤwzT`}8x0UtDQ 9;.Gk2Aф <<>}`DDӍbn{/P}o'~7 p{PEV[p`04CvD|yl4Š2sUM>$L &0 5H#zvn6ob/qI#zDZ(hXPkL}0 bA YF< 𕜌t].V`n39sL2իs֭x)fԨQ_ZZa&MZ`M sS*:cحxI{2PݮN‹ ՂD#J rz2/<4CИ@2DNػ~7+/DsC*ŧSҩ9FkD2qMb>Ɉ_~{p6nثW/͛צM6aÆ9shڝ;wׯ@ɮl=/)V[qzČO+MHj(I쐢mܼaݛEfoGH e&EE}ʠ}"NiQ fAO=i~m۶%''o?~\@$%II>k~#EaYq,Kъcw)L4i_DxrC>\Ĉ*mn:3cTjta j a\EQ}CWv8 &. _^e9L:S'PQtBI :լ39VbT6ᇳ+͈Vh{J

aY'FTb r1CX*N(Բ6.&KZGw&|릛LQ'tH( Jr<x|O=ɼ*0[ b!Dl@ ڣPCpړH$r( :(SѨtæR J8%0vAZ<\/HJy 9^ Fr#z *C q?rrne<(pXjETAN^}d3!nC9lJ{3? 8q\v-AjAT쎊ԅ!h5;Yb~Z)жS !y2pn$U ,JDײD Y :qGum\ 0M6@ ӺTNvQPPD<輛T)ྮKi0 ƀDB"h.r0@GjߕHcL@=_tN{ p @۔d2W}Io0l "os˼Q9nr{(TJ ^;AQ&ٔ0Wj:C[^ag n&;L%bW &h 8 i[.^BKJJړH$y7vw(Cyl acFx)!>?px"S5yn'Lt-v>9 yω6&i}=_aE(Ev;(A]NpV8JeS)DuZt .owNv t:z0J,H 'fS<ũ-NTK?;Ԡ,5T`G1䄄GB9yYrIl9QDB\S(v8}9S{l|I (b\,=ޓf20PTv8{Eر0>`w8zJC JCTJCĶ")ChJw)i|"*Ͷ76rVmSkjUm,&̓l۫4ڻe`ʥ&b2`2rWcJo0vÅP*6NU[pYՌ(vCs= QPj, Wz ⻶ FY|KqS<9A%y7GX1y069=nsP߶t#kٹ&atYB6u|#nxV;?s>}`!FJ f9*c8s;\nEJ'83I),/nHA0L'Z,oOwsiLz! b^|/r?u:wn]#`Br624Ⲏ nؽ~f8\Ym:AhƲV@-3Grz>{u۴c s';ޕxp=!`PD5QmEr Ex5Ujy:S@_ P+zx>#/C<o;HNA8iz}>_yyhiy$ŞNLaizVn[ ɰݾNCC2 ֝6KAw2C9^<4dK޳x'd3봚 089tжmpx۶m_?~WG p^o4[!ɱu'֝< Y}RR'5Bo8s̑xvݎ`Aw_5պf[J/%*_y}f71-HoX]uIX?c!JRBgX uk,KK@$ R/N^M+\m٪D2<QzCD2`V$'aGZKF O3GI*)2 , 0 baLgq'qojjڰaí޺q <WooZm6b@l<@wxp_01{3 㪾P#:$sVroɢ}T-%IXX Icƿv˿HX ku=6#[ fUq'ȦSGk մ\RGN`Xf@Bxp—9j=a3}+_yV\9u`ؽ{_fȋ<.p^k$^P8tPcc9gG>p\T\=DmC ~㙭n>簹)05/$X:)r=U8Ps֟xx:.k-k2H#eu(00֎fo2$l^v֨3֓էcH zZN']r?d? "P\յvOѕ.l?l10/}Kw_ <߿j644Lrxxx׮]7x<}{lYK4 pqcQlj7$Nרimζ |B% d8E`TYw4tf?E)9P _Lz=-H e3Hx $ zۖg:@xM.i\5wܽ}֑IDZCu%,qW 9yn޼x?p'뮻??/x1y@\᾽ގEq\&()[ $0 Grg?u:?.'0'?DvY]]vڷ~].XxyKRDb~C#~ͦ!0썘ZnQ")`?Y*ۧ׼_'?E",s݉GLZSpHqq+A @;HrR,>luaŶ* @U V">ISlP8VmV[<"ѱ W\+d$h DSX,gg'rɑ}A֍/w[BfDK;Q(ZfzUԷ@hy{پŬE )Q3i12r*)@pjh&tRt= ƢrZ$)&aTPdѨ(/i6́F Ch)EcS@SH*Kr$J0K |uxsiU gVgdJᘮR @ͮ嵽<<LjgϞ~{7ߟ_ow9{g󭭭d?}裏p$=SG'9|>Օc=NN{h{&㒯1 .j.a0U$zv1vz]PM9T0-i" '/"0L&}я[n}֯_9z7mڳg͛;;; >ϴ<۷o?~xee% | _{Ç8 / ?'?rʢG?hkk{饗 _s4,[# @R\h>o/HQx8ٴBlH#q y W,wQ NY8BP3=f8a~A|iјӨ-,Tzş'MVl4ȻZ(4 i!24LA8)j"@ *$5MDxShZ9) B1mqLRsȂDqS+&08F);Aǂ +@.8v z*Ǥ6K#9y$Bbn(\4oh% @|A O_]=<{Z8G>򑚚{>G5LO<tM>lSS5\b8o{˗@ssO?ܹs{|f/..>xu]7<<|W|"ضmہN3Z@aEh4>e.s:}%0v"6ޢ;LGq/ϸֹ M-gyaF㷾GyvQX+A _y틅cӸ3dzwD" KL܁a@3@>3+ {'wS\\|7@ee^l>lAA:rfzkttl6ňdvwݻwo~/~]fpt:$:ڔk?ޒk*m@=z+oϵ'rӽ詮\{?מiɵ|k4׶uڳm#dsD Y1 r;0ʵgZsѓ\\{iO ڦ6O=qv0kSZr-z;ٝk\f?^hjQsùu$מj̵\{=}6\{ӗk;z5|3ku@*͙6/ɳC86 zF#t< bKzFsmg v m;vu{rmKoÝkۆ\ fg#%~^xW䄡|+;_D]_gokjZ$IP\a 3!V~[%?P$[Ƃ1ޒH J$[X8Hh bSi~$܎lF_(K?˭?P$-.嶨4#Rr[]S JnS˸O>#=Xss+"}_/g}㯾?|Sp~җ4?я8kqlٲ'xojjկ~u>p< dUJ;QW]V%0!T2'$>#a*hlJEMw [ a#b:K3C` O`QVW%Sq۸~Az!8cqCJѲ)HXߧy5) R/l*J @ ZlZ P#.H|h"k@aL#4ZPSEE s"K@ju& sjilm:>RmVBi8$L&x ~928YK!әz r AC]0lrkorV6L0-'Ͳ$9#K{};Ɉ( "WA敷?r2L& Ad2a]7<#*;1<|oe/J2(ș~'zu"LSvшw!(8IRU%gCd1,ZT0Mj_RZ D+1Rr`y1z+**x~'رc0Ls⋿ovGG^뭷;ǿ˿|w}o}aew}o&+vNgqo28KN./-``u2Y#VXSdXϜ)+Z}U->'<鮏U&۲כ/MnrE/R^bM$ӤfYtm}[(`mF24`  +ےC:;;EQܼyMJ^}[ne#KŞl>tzD Μ9vp ;wML{o._#W\p&|[ڵkWSSw߽m۶N}r-rW^yeppv'T>ozM}%UV1%oh'>)vBA1~be##ϾGcB*́Ay!HY{?]IY4Ejޛ 4  ФS`8 ;ݽn 9$iT).>ʸOwHHEAD%$|HxN`"ӣ!&%7}1ɩo=l1+&J#L x'":%YE iz\r6FCy :Y&;֮wD48qGuqX?-!ћ>XZq>R.8%M/H|+W,?Ï?k)//_zkZwܹUba$Z&,..... 8ׯ r\ܯt:1"<Y IDAT.IYdƫ'Sdg: Ol|voDYV wo]l,Bʊ-EiY@p\VVUkr'יHeF3BkmL*W͢eYn/w`$6WޞҰ{ݻw[oTܹŅ]iy,K30%*?E3F| Y 8[u.sː$)u:zI)~5о3{vuV67lLI}>"M&5z`$h1 B$FBWZtf#ㅰ'ȅb4HHRc 4j~Mo,'T@iQk)(7&$ ·3 ^vsb_Ou8 pk|ɋ<'"a.@03r AeW=_]}P7EB`xQ[ED LrdD)x&ÓћYqHH?Tt3;/8H{,̌j b%=šL<>;+a;Tx;Ӏ%|{*,+EEE555d}}}2|y\Z\ ڞ ׅ뒉:4Zխ&y܎2FNSTIL򂆜O9"2ܡ8K ;V  Ɔ슱*yl&ZOkibJ< V%M,^#nš\T @.j X5"m* %%YbaNB=<䓣uuu2_RR7_|E þk_X쩧z/cXbvwx\32誫tbKoWժF}Y?J A )k2)5:%W/L`0\1JH:Z]B]ݞJ΁Yťjmɦ5ƬG@ Q^{F $')Ш'){e$H}-F/0exgiβKq' BJHϕʫґG1U]yh L&9Q "<[B#G#.0BGhx^.Djh( pJP"Qޯ.3D$֨)tkPEi9(*5aPCє;tuXH<q dN!w/766T'xpx<=}gZ_~eۇ#K%)N"ĉc;vL6Z/ # Td8 9םE%,`{<̽wR[ ){kx !NaT55/˦Ii҉I~_ tVjzk #C(J82d< ,b#1(%Wip~mt8s \0Aү믿B1<%)nw,7YA lAHU?~%&+hԪMW~ح8u7d-G+}>H4V{qi&'$["fIfH]G.mHj[Ɲl$uwӒSPeҪL:3#WyoEes:\97Fiig$I1[yq%`Ij{24\=v筮|3#him^>zr}2ܤgՎI'ٱAݚe+*&Fk򑾜S°ќ(>CfR4I,3u~1aRf9TVik U1INx /Rc4o^*V sg7f,7azEYåtGbI=΁w_GH.8KFYig0^8XAyM6EZ5'z{`4|T eژ-$u(vmŗhn\WU[3$Zjϵm\@ɐI#0؛FXz#U=Kkc.I HY7a)`+]*L 98f x,1-3񔫼8381IcoxE:T֕>HIӼߗ,S?2 a &פ%)$aG43= M|;Gg,$lMH97-wVm{$U6$Uɗ#Jp*|_TX^9uc)wBm~QFfRLP0/"']Svy˱-xڿв Z "Wn^~'6`a Nj͊4s{m wٶ,q`I=۝L&"U e2 :r}Qt1ﭷ~gk2>,+ϨTX?cyH=2IVT>gxXM  'G8h˹Ҳ5Wȿ\FUp߳톆sR)%J} DbR0!#4u4їRVfYMj❒& u(d 4&hY8Esu 1^bG TrP-UIuwo^1I#_F!t++Lg4j/w R n$Ű*չEC]nw4y8."z (;D9 ?WCjLy䱤$ŞDp.wyyykT8h4T#1L2s"98 b4b91Vbf fٺ0~p,̆bE͕efD %t̩UTumq2uFS\e,ZBMSc[~w̶=;T۵jUX^ZdxZ(}A€)?+occI=_h/WM ve27uc֏4+;=>Rls8˕T:=bm{o<^߳Яh)-~C IJMI#q,Iwa`ơ?<Γq^>ܹCjz( Ǔ5^ ]_ř lp7;S..c` b/c NSx򿷿'ўוZؕk"jxZ}~2"}e{eDj\#qFW[.$0rk![lkMc>YC9`8>uHzwBMT-4rj&N1q-C&b:S3H~"$"FBzRA)hAAtH%LJU޺5eXWYWG3eeڞ &Q,$Ş l>~XtQ27ۧۛG:i7) LjdToqU $E /~Um#UNJ`ʟ((\F̙J0_hL&&#%%)N'qI[bF!q=rvtj<@$Er" ˨(>cbTiPbNP[& K$ٺio:up1Hzy@ңR{ߕ~)͛?OexQHsϽ[f_beeEXuy, K25n[/oDܺx<,ʩ6$N}:\ᩃc045Ŝs =R@ ,T 0ly |n, lxfEoo۷l3|ӟ:SO=cxf˖-#8.XHxMFQ ͱLEIaԆl=OQ鉫<{08 ܡIAD!6뤢mݺu֭ro4T'xg?jll#\±$ŞyA%l Gӈ{ ׏ù~*"`?4C^ʼnrq((H2Lh`M>XP  J"yoa|UcH2.eaB%Sb81L@^BQHky #Q37bjF FcXUk6 bBMi$/#*mZ]Y'FҤF!q/,A K Gn^\9pJ}]L:}fTLdeM> V`P@')ܱH Ţ顜&HoBs&"Ġ7  R,XafdEq7ntرCk}|;a>FUkv$b-E78rDhnb/;&ox=1"wYdGr|'P$l J4 $Qj1- +LV?eƔ5cx88AmbT=veԹc_Si5~rX6MxQoa3-'mYe4h(iAD|BS5*ʴ-Х/ɍ1#( FCa0RT~|s5SUdZͩ?]XWic Z/{5 DJr{՚׿ug'(O 0 )R=yf`<9U:s 6 z{{Ymhh(--}W|AA^}Շ~xK# %)N'yI#9oT4G'GcB7Xq晟DJRt IDATq_{,Y,0 톛n|qg==nHaAXLEKݔ7Y[՝1*J΢BI%ɯVWSQQ-#MMMuuu`_ 駟~1 {ꩧ>O>}UR}\"cXbv:ooZd2xOwFׇB{@$`${߼[}풤(KL١sȚE;p5qmE^X>N FٿbVѦH+Y4桛qͱksŞDܻ{7M㜈j oVZY]gGz4lRæ`@&$qMkmy8b@wo?<+ 9yM=tZW]]->Pw}ر\{zJ`GKR]yj5Y,|{ _B3gEsCͷ6)*%4e6~_qTX0w%ZO-YCQf:{ַ硙MxPQh2/hY[S ZcS*hB&QTV{{"l 9++ܽZPߞ^߽{1uuu555555\Xy\@,IvEq[NeJوQhR īA ],,&جӧv@r$"plz)W+QzMZ5R醆v" $n ) jJ4\] Qw}U <~\!챪\(7lN,*;nժfY"%F/\~5xcI( JhxJ( J.j }e=$8 :v[:d5R#P//)^dq12*G@BL FJHjq!B䮀+7MXm-^*Q c箷XXnN"=gνۋVͫbKnnl)/ $Z@r18SBr%cZ64c-WV/+c#Ym%[{ygΜ=y{+ g;Kcxh@3Aj+hZ\xC8qߖ &O6@mOjI: SC!r ;B/L8 /,g-GV |l($_^ yx<ᜰ֣ӕ~QzefLnƫcϰX ޽aeY>yaPtp{UD8;<︋f*ReCidfe2$:NV4bߑ_oFꦮq3=OlS<9S80 ^JSr9[Fδ9K* VF1νx/&C-1O*54LJˌI]|އ(s3M= /M^E3gO4ݒk~%?ۤ=\z$]Khސ.:Ka.Cui՗SHA+$I˗/o y䑱cǪm߾o[]]y +EywWZ0e7lذk׮;sҤIAQ/^k׮noO?{Ǎ{x 5KTݨAKLPHAEĩdA HZ{o/Ԇbn%)Z>1a q )UDjhdq&kW_fn hC_'JJ2҈3VNA۽;a v >t@cȐ[.Xe5+ 4- ehL$Hz 7N9M"i*rlt=9ׯӲWwϚ 8j{de}hH%YiFha"YurKֶDG8_hf$hv2F:7c!3AÞ]_yuuјxfFfUg D $ D)H.] l{)@;D"r/gu\ӧO/xʔ)ڰafϞZ|3K. '0tJL֜i1ZVU!-*C3fJ . %]Bd_Dss`Ȩaï#s-"sX /wUWLJ{YC`wF~D֠7\n,޿ъ /*}ɶi&I ľF@ w'g f/袯ᄏ[ݛC׿:thΜ9蹯O?}ChrJP,CPaal*Jϙ3X=//oA?qѢEDJkd~q+a/,hᮆќ =LD8lAOaYY'F:t$u[FWn׍c3dZj?d4UdZrgΠg2E/;n=ZY75(~#j R)Hy(C3f޼yꫯN>l6_y˖-s:UZ%y/m//B;xbii2(J<ǪSѬJʛQPi}e3*m S`}bUnfJ#MdPi' Jf <7Ǎ FmhPT)IᾦJeްJ#HUMS$Xe58+T&HΫRV{|*޵zϧn߳㛷),W_;اRMUTE8^*}7~gAt$ͨt㦝'UZ^%IFOH's|C@1At' Cj N|Ϟ-ۯҵn5|fǾC U4#˭ҭ2J?[d~G3C{U7iEtQ{Uflm*ViUwojk7 p=&n| nxEOԝ,\+hټJ GnVifTz͚};|7q5{;ҷ>NQ=zٝKKʓ3$SO=m۶O?TڵM5kn喒ZRL 4M/Y\Knwi; )F"Ql6&n8G{,a6+? 7x|yأb 2C盌(0% =x74q:˒ ^Pحf/!\ }[HREQz.3町 `Hz) ~qcW_2.'7wƬY][dɪU6mڔْmdGԷsYpYm6_=c x7y睝;wj{뮻K7n\v,'N|o<6~YPeQ M8{#_40&2I@V]Wi t<U,I{DaKW|E]uh0k?YrJŬ9<=qyA0'`Fp։p8ȉj>eUⱖߒ)`{iZ-mh鯹^}A;!|VH^AYЉ4/n ^ Av$VdhhF`8Ch M^40DccG͸ݹZ{Zl(QJRx럟OpÒn}d9YEC ɾ-RHܳ><˲,˲$I2$I|GN?ʕ+QYYY~)q b'Ofgg+**.r,X̘1CիWg?]8 /wen7NaB+7Oq)+.p  G($*!U[P&Xk)Mz4cHQˊBHtAAL`:6N+,+81 >(N8^w?rΒ2u; " àh(jyyKZZט8056ykZ8P}^CCx#Ɓ}뺓'q`df)姨|GDe-))yG nx=I$_5z+)@l,^8++K|Go[neǏ?o>rk֭[{]~/^tE`P7pCqqqyy`4}***^~zg>cǓb{) effY}q^0KIROq"G$bsYRzr>"DEuv~l)p=#Irذa~Dą^@9fK/m'//zӉzjoK{7?G sP*/o3&AwlvA_8% V:^kkD"FP "5N2O7?5p7p\9ˬʺi IF hxdg, 8YS@}\QV^ *kkkոvy;\lꉝB gI| Ah<24 z0T%F!5BaG|f#7|S[[;b ;СCL!h#10\bʎ||qrhn J لPP$B3&2+d؞@`x^FY&=Ӵ22C Nn>^Zh]?k0J +IYG[dvp+m۷NNv I>`8҇EY` @] vܹ4mH$ӦM{~__z饒e˖nb)ЏlO ]eFb~at4 2HoMx!Ķ0Zl-(БJ IDATV11 uj!Xn<@ HECAD(fQ⑘zŪGs OR맪RkTކ ]qyn[Tdfgef+oIݔ'N:4-3̞EZ#e%hmbZ}Z[ zU ]PO%KTUU[oL&U{~'flnM}` M!3^4SvP:&D#wjkd˲NEN+FcSfcSfB[6;vxl5eg^uctHNR8̣?crhG/-Zc}!l4C-q(P x_e7dQeԙ}V<@TF@POi47ozS0F}A9g%& diF;Vko߾MkWUWljq67>]SlC@2/`FS4ѭi.ݭfZ{Ez^ii)2˙0zޱCzFh|\0g(!Dn̦€ǀd{ 8Q$ 3NA٤q!.C]M9|^!e.{'nGp8'%4h$;EYqB+Gq ~7hKos8G3uwq^/0;OR80 nfO>=A@n८P"!rZ ð ɐmJUJOX(qY͐XoW?Ν>U(˰m_;8腤@:MxFC}dDLr&-o}a ^^ y<^3=>[ie8)tCsqOT7_$)AGE#0##_^;tƆz(d2¬cպI!͂()^ r`f=ÿVǯ>/^z~>-(v߯ UBrMS>$vi+X=ʠ1T@BxBHQb b#($9FM/hn"bPL_4N(8)Hx JN$(2( HAE#%4IN\f9TVqo6^ @)s ȟi DQҕ $Jߞciu~Y6qod0h1CRuP:`$ELVQp,لiBTB=lZdcc 5{lֶ <_aM·dY(  Nwt#mlcb<(i8~jda±̤~$S͍xwl9 @Qd!.4oQ姷Ex !MX/JV\/kO# IRra0H,%p#%0J\7w(\8umX#_na6kq,mhNVp"_?䒡*SA9s ?w"i\>tcGd]u rsSXz~}up;8aΏ>vt?)(2R/HI{)IZM4f‘<Nb zEʀh !8fp0]KplL#l⸱'-}$L1oFЎS//ךxL%RzjjjZdɞ={6oa-[u]Gk1P)p:0 _瀴Bd IqgWr*v+z+IA'SDY~0,>Jvu힀MH)I?-VKo5^by uS6[W { DE9s:w_YY$uT妛n曛ʶmB }@&jA~b#)Bd%>n+ u\J8F`1?IN)؀ZXy jFMLOi $l$HTFcH9E(ii$*r@vh ۯ}ŵƦ:h)3ݺ%}SА} ,y-UfG Qn PYVY$$J%KwV+ð)SxUhNH2_0'`XR+\|͛KJJX]hQ?3*AEDhdT<r.&ϐt PZYlzG )RVҔ(ɢV-2C\5& ϟQ}Ôѹc+b20@jvA\ff YH +[&DR $ EA1P(&! OрEsyʲF Fق|HgsPW,j[~!3gțVSA‘uZ[{@32b~6qjcvxiiN{"ldۤc!D~Srr7c q|ٟ|ISJ!@\(xXo\s?^onCa:a`2fbkFɼ̝Z1r'6]6 Q޶{W0 )Zu0b/_(J8裏&Nx )c@=۝ 6*pS|-gͳ. M }(4a?BEs`9OƇ|4" )0FYS`:D G~eA@/9s+r_̙3Uu‰'<c>lzzznnnVV3<빥Ba@*9=O[6. o?C"d?BqY;@}T(q0ehb>?hT,R,*tV$`BCM'\Yt/w.!SLBs~qz.ϒ=qz1/>P{UuOYsvnO>^p… Y( իĀd{n{A2nxZ@("T0L0D&1) 1!mM_/vf889-I ?s&سT#rz0㟯n=&K8l"!,mv8n(h" ƇӢGƈ6(F`6)x?d3 zm]M޽D;U*)nMBjp8txdYv:;~{t>a3 0S͆_l9ƌq>tL|8 O'MӠeZ(ƍHp?& E.b}a ϾXU5Jg(fΑ*<9T -AU;nMgg8c!(MQW4#6fr )( Nw1H'#%0r`d8IxT8b HKS<?'c}riH{) Ҟ@<ی$H] gg|]Z2cHI6CeD$'IP[=&+ĨVk+ÌaԚ\G utò( HHQe?k~{}7ۡ}sFpŢ?L40 -1Qdc&(IXm2@p.!R?3Hf˙|0 D8&FMa$H kSt!t(/QjE"*_q1ۑJ _e,="s-9E1ȯC~i"5#LP0% zr@Ād{e-AP$R҄,IKX8P̖#M L8w1 ͘_{ӆ#n;IL`GteM x~4P^`2ݟ+"H,=ɓ%">1GK0c̝-II( tZHbbt@QH2,#Cs~<{՛L} ަB%';EqB&(`pB!5&JRFK"\4 AAn}kh:Go5fZ ab.!Wٍ>5A~-dn`10VЭhr()# M0@%e5r9!?K[o⋔o`0P0S q2]Xʓ3sՓ3HZs򤩰sD\,KU3; 9h7g14S?004uN\eЊ.GET*\ܹ(6ޕK'EC©C: " IDATٞ)sg7J^)pN{'I;sM7͝;n;tz|ΝwEIIIs~x@MMaÆ /w߽o^-ZE͟?oV{nјbxbVcҟd%AI}]1A}gFChcNy 6sY~MAi3P4A8ΘɂA6 dR⧁QG_'*{`{T@:<9S87=H$+W˞z)1}p\[a4{ャv\rڵ/>q'|R^^\bMӟ[oU]] o棏>Yti 蝴g2ɉٕ/ҞJ1C[Fm"*}']U[,-v=\i2 s,F$2`Qq&,@݊Vm#.X?9@AL@=߫.諯naÆ R{kNgsդ=ܞ={۶m"`(6ll֪͙3xƍsznՠ%CC@lr8R"@< H")]v=lJrlk!4te]iШ$LIHJ#ɵ^ w0|ea@hh(e}qBc* Oi_OE<1ݗ驪Os D~יJ+Y I:Wh <4 j$t h8! "b86NA/ HI*%pҒE`48I}ΐ.qQe r8NEںK $B~oߺfW; b1q(ʀr"fDZK E4)hO)] ).,655|gIo_׿/̚5kA?qѢE۶mfxⱤ32;.`sfH% s%jT  stͿj+\[.]{F]a3 `:׿NzտxFq8${cYtˊVhN)/rZt桅u*xk9 'p td99SZ>*C3f޼y>,--mܹ}/n/_hGnw(e}[Fw}ݽ[kwNw"ZowF豃M:=G#v 7Jhg:y,ҒEu51!JKV i4ꊈFkmc5Z]$Qxr \ NުFc:NڄFU1G?Zkl&*x&e6{`.pa??>ƃ h>ɢd)g `{O?-[>v*e˖z뭝,YkCǃSӨQàё#'LNR#BMR:6AV;SQ2X8 4hZ4BIR5[h,BItj0:5Uͤ5G8ϙ- - (c>W[yåJ^EU@}UjDf]F -0<{I4 L목̤lBbBZT噴P&r)Vb($&42J$TJp|ذaa@P Qꭔ(m$k1*U Fa5M,/zHQ:ĩ4#_~?O> $нP:A\ZhUUHO!3 ?v%rrsg̚E%K|6mjRVV6bĈUm{yEEE=u]ى/ƍ׮]+IĉynK.ݰ_gXކ&*u~ AALhmȹhMt F @ s"=F7HJI ;BH!z#$ekP}h!d'r_} k'"7$AZ3"y|ZFյ`t\?oM/(A jp("+FறFʾcHM!ϝcHcy5j-IY{K[TUDJH<4*gdl2 <^BZ:-P &Xn{={JKKsss×_~yqq BO>|>)C?~' '|RMx$68<fl9 XD`"h=}}"#f}Y".0#qc\f²\\ ҝrv||])YC~`޷8էTGX/5/ Bw8_q~g(<@sssmm{w4gpxܹEEEEEE(ߎ_nc=?â}]QQѢE7pCQQQyy9FTI<$\D.:5?S=M:wlDņ7һ^~S)JCxЦ~7 7 y{" 3h/3S疛H$Ο_;ONرcGp߾}\pAK.-D"˻+vzOn7![\ ;#>P$r\u9۽3p;?}fpǶ,5iȈhR論vD3,&n'_7)섔GSSam"uQ8Z 4)JD-F#@w8M?p$@Qq#aŃB"62VseA`pL2M_Q/RNֿ #H '[o0RشԠ HL (Zb=gECpL$l\#m*)VML~ t{Lנ5= (t JN#XѶ)S a0~ z!>z5IԗH!Ӂ<|(puuw|MQZ| C4$˰2E' F( ʣ}j0PyTٞ@O#3GDyEqݽ(G;vcS-EDMLkLFEŮX(Gx ! F=̳.یQJv\oq~ Xfg bzٜ)`*ʼn0R= J,(yqN*[W+0ڳY"=xBҍOrJg yPή]zÈZjSOשptTϑ?T)INZ Ԍ:#CɒP|1ӘTQ`Ѵbí> [3?Ǫm;dfpR؞J0RjoWJẅwUudPHF ])9/}O _=D{$FRБ"MW+1(z,*f:s8T務U( PKNC.ԺeSRPZLr +UcRdyytMZo6D+iW~ #c5&\,8F# EUG5Xy|U[pPp_*pGZR+>;Fx|!E\9\$YuMs-2AݿãHQ] >h95:AOxlC,?Q'EAwA4{ AKG'%e.$d IH0sP"*QqC&+Wn pr4=T.),B:&D,iNG<.ֵ6+bLQ6@ !| TP *B|~cY\$ޔ6|* {MK4p _ \ycֆ-91>//dWjuIW AA:?On+sK|:G/QYBhxDQ&b4HOY 1˜xo&ُVy -b= y=Ub0|go^ٳ<5kJl( / a^ jhUp-ufo4Ԩ>JyFӲ[.J%2 =+3$(%5Q{L]klO]Hhg~zɌ=,@n0\ awI~{X'{Nqz/u%{ :R<3@*ƣ=+@{hϮ{gPtߒ <#6Դ"qP'ㄍ"?`PB&'+,Xm6rSDǏ@|(6$!'  26$\b1c, ClXUR&'Bq#5nœ=w k9; aIYna%bH]yy&-/hI(H?QA"JT>3h{&$Yr)8>E -ɭ'@ (k0+[U JPRc߂O aJ]?ye?1Gج6dwRi7+ZXy%=<~qO`+DsS <4ڊ$Dq:vCH$N""Iȁ’!I$<4# f ౶)ԣP6D;˥Ouq$F3ꨀ*&90K 2 FI|$$4_dtB4X$zIb9yy<EUid zx[S +UwIy3av?g7I IDAT_H=*zXղ8)F{4IpU3QO`*lF2Vj]&ӋnȳRɝ>c(71qQڊ[ #[燧\~"ug7D^91Z53޸P*Ԧy3/($3"nkk5fU5G{Vxy%b/K߹~sT3z)hZ]^^5"[} 񅅑E"CB/05˸#['t]Aìp׎ 2骘S}T*y%Gf/FJ:xKLyF#Onv4G*/nTZ2sk2#;E<4(UZx|a-H&0TшEyBDE YWr#P)*X.W'w*$wfA+Ri&Tx\$b1갈<?~Mdl!HOg²XJ^8&y:s{=%Eu! 5#渁?~ڵxXܫWwygȑ9fj֬͛Yb1/R(d(pNh1;@x[0Tj%AWr^.oV#2ZK~ɶk;RE'^ d?|jJnY @n'.)ɨƎ w)_jx\fڍʧW@˅˒͌Wn>}~)sצrp 6@gkgGOݻX,^z [nk^hϞ=ׯ_rjm۶mo P+ŞF)*2W-i^Va;۱JWiZyߞسۻig[7kf͎9ƐMEӘ YyF#77[ZgYi4ו.FNrSBrS%3uG,1HOg ZX),z1ZBPZMNImT& ݽL}ݱWnZMQ`f29m%Iǭ7M36[)4Y ?TX0 hZ777;ӧϲe˪$ Ʃ jZ`>Q]6nn9X=O !n;v{gH14j2Su"zu `܈k7ı LV הc>`4*OTQM׮a*n S'{ S,JTIH:\p}.\`߿[n/-8NJJJjh|u=PHR6I[U x*^ I$1Oi׵KֱY񐙡ʅiLJ9Kb˒AjQZ4\ < Rױx{_W8Tչ# *' P aL֕C"NGb{cSW Crǖѣ/^>s6ͥG䡝[X%̼Y]#ftgFrWu{QɄ'.hJ~b.Q粤Qz=/)20%')* BD_<<#,rrghDV z\HDbഘ9~xtmoKhL$7S'oܦA;jf⧰N6p(XrZ駟TӧA 5z@{m{碍/ -OKEz4w +e7H |ϣO;wʭ0h) 8T5.G) Sir*(Vs0´>y :@@N.V8)&7E)imܮf0r^ANM˪^}`j RUcZLo+e Erxc?kSңZGf`wӤObR sBUxGШ4Ĵ}ϫ42)ӗzJd|eh*[*m[E܏lI2zelDTI^6Yo3eȔ:/g|Cݗb)r52[ٌd U]yPfS{PE58 O6e]> cd-J_9s:_D؝ػ9 J+p0N]dE*?riMˬN5b ZFq7RE…8rlAp1 %G,+9\;՚+UHܝc|h0pԲ-baLImNSrmu[,EE( ^u5}D]*IH[,%Y9d,uɫ.' ۙ5{dz?fz>A4S|ɦK0P+ŞF),4۵P!0EQ|7|TuC39>BJ[h4=Ag "$-]ϗY CYyH.Cm"踨Txt~MF3C399AH/!ä[/,4zM@j.qh Pl6mOJruyyܙ: K}h\F =B.4iP*,rH@f߹+ڃop-?_uYܽd4SA#7KIul$KT6VxJP+ŞZyThnwiLzzJ%: J$ aa4*F.[ubIբz"a9svTLs;VuZb!ތV0xD D>̘ZZYѶ @ 0Rj) nzx_J,QZ^*ɁaAhl.εQPG*W3@AtEabbש.#aG\b9 VyȤ??&R:2c4ļJVVss'n\zN/RɧRJyXZ\$򢴘n6T 侣N|ӽEzfjqu>W.eHeyHP<<쒝ab$mSOTNwZ637`?0;VLZYA<ˉylg --91r= ّ#LikՋrGzG{4Op'v,eLG{v;Wy_ |2OwLP'i1ocd奓DBy9=eNrJ,fe$WU92UER>ڳZgnڴJj۶C%F?[gw-[TT-Zغu+1..nܸqu8p`TT4caa|ͅ [vѩSPc6[ɂ@Qtb+YE}•w% `bdb,hchYZ]nnaYiھ d,$܅$HЌ}B0M%`_1<߾B>_k5l4X=C~.#a_hl4M} 88$P6}YjX-LMAAn!eVb1[,fK{DQTd-Ͱ CYڽsJ S6 0;LB] EQ̗r>s81XݘaA} ?Ѹll)2W"py2&'Sأi~?bĈ?M4'Nu=:JAA5k=zW_M<9<<RSS۴is7oT}f6m۷ŋ]xW_}zhEaw)'wU}*hϨRfdy~T~uA^nSr ܆mp~W~=ɩrm<hlɉy%سZo۶m W\ٴiS{S54=u6mݞo\.|o;Ն ۷\~=>1?DhJ~W"s 97Д"/ҢT$?MⲎSGHwM&ОIVdksj©H9=!04-915Zlo6Y~=E$ 7jhǎ;wn裏BBBPŀFѣǜ9sƍW Xd;o[{s'O3ϯ}5[^ytTd6m$$$gg|NZٳ'_^tQ;TP3?'>RήH!+ļ&ܽwΝUl&L>r=޽{?CV9hР-[.ZݑaO>ڵk!!! ¾]tmĉϯq?sXXѣG{{wwرc뗅7:;;:E7YYvڵ~iӦݻw yx2/\nA .C$Ԙ$S#,gXXkpPpOoEQ&Mh4Gr6$I${MtHHHϞ=ϝ;7sLtRDDӻ IDAT={ N>-Hl6A'33j۶-MK.۷oӦMс`~~'Zj6~ׯMɑ vdڷo_ll#\]]ϟ?!CXb\\_&sT9uׅB4h^1ߟF~z\\\^^^aaΝ; 2LwIHHСC:uCCC\2| @TTԭ[ɓ7o5j{DFFG_AѣGo߾-Ɇh^z)Jtˮ]1QQQZN::tHJJ|2Z-889r޽qƱ)22hQFdA>|`0nݺo߾?0'NqX,4hPz1%%e޽&o߾-ZoB1c<<<؍۶mkܸijpO@cKN+^zzΝ;Xt駟~*v1aٳgׯ_߲e)Zz5 /~ᇣGDEE$0̼y~>~9sl߾&h׮]׮]8pm 233/_ھ}{wNĐ!CΞ=hѢ۷o_zuɒ%3f̘6mھ}<<<Ξ=;wܞ={յkg{sݽ{￯hZn[ֱc6@nݞ{6lH$<{[.gffnذ.V\O4رcJ?[oe?`>}E͜9^RRR-BBB:t;wnj#W\ڵhlݺu^^\.5jWd7n;?>44T*>|xʕ]tbcʔ).]zwcbb6mz…-Z$$$>OK,ԩ>wpa&N0^S裏ƍǮ%\.?u>iҤiӦq̘1v `#ѣGϙ3a1c0 sڵ3f0 5 ?o)h0uyy99ZmVffF&-55%99)11!>.6o}o7>XOff&0 DN={u*KqիW322IN777@`08p+f͚uvU{U:-y۷;766_~P '''k2￟9s[Zn]=(=zuܺukHH _3av1eʔ}ڵMIR=N7k֬2rĈ|͌37n\rjMәŲe˖ɓ';:ٳg`9 07n|?G駟N>=''' ;뛙0 ddd{:L 11/M%%%Y֋/ۗާO6򑗗WQQG}4s &x/0rC=tPAAW_}n7oԩS˼qpuuy֬Y{=z4k3gڵ+,[>oWDǏܹsС{j}~G-,,>>((8۷o;T*,ٽ{ȑ#7mڔ޼yӧ@LL^իWÕJS Nj⋆ 4ly&O%((ĉIIIAAAB/_wÃQA|yfXP:+W?e ѣ͛7׭[/{v\^ ڤnذAѰ6;K.6l:׳gO]F~f :cǎ>> [j$:v٩S'/_M4 *,J:yNJ~.RSSO:U0qqƕ1t{ 쮮5ՑKΚ5x|7\]];$7oެjYCk֬٣G;0 J_T$ɆCȰ[rKv*j^سr?M&O?+"""X=d2 OfffڿY v~#Oyhh2ʌuyMmo?k]v;wN4MƲ|b899$٥K{thooop|+1QIشiSǎQ `Zyyyg#Ǣڵח5mp\=Ctnn;uwww`ҥ;wfXvN>ݬY38}tpp =''#F=?K.ob_n]֭o;w.^866zݾ}{''nݺy>>|Anݺuԩ2V(g}׀.]t?weچ ֭[w=޽{N ř3g^ʚO?_}'M4)hG' *n 0uԬe˖ЬYC8pĉN#FڵkF$_p!;x?VKdd$ڑ V{ѴivwMIIYr;؟h޸q=˲eO>} ֘RhG2gڴi~!;WуݻAMj-[fUTw \fMÆ ߿3gΰ\`رc9zh2|=z7ye4mթ\.K :Vu޼ycƌ)SATlْD 6djuJJFYbE@(N0 **JR^ڑH$@hڴ1B"ܿoruu-((HJJ"b}ō5bryZZVmݺu|||jZTڣGƍ{xxm2i֬DRͨA=Z >> .,vJ'H$e:Ҽyj; 1vX$.DFos|Iq&8T..q~_20kJ< M PCE Yӎ\EvQ}F j0 r ynpS5 96^>P!yV&?uWn 9ƈRvGgs| [Irφn1e)V FPN(*%-%7*pmbQ7􎣛lM?ph WKo On-K'tvl9vMd!6l٘6~*U6ceS@]_AF*W~gЇU\]T.g.>%~ݚx\]TA_X?4<"!o_#ķ03t^/cp-"EdՔs|71 !%52ۻ|Z6OAJHr <ߺ xѐOeeFˣ%n]D/ED&[<[UL%|ɉ-q;oUNo?/q>' 2jk^۪;4_;# S>UUqGrdv$N2>qnO̗ E5[wӥB \'W,xI T 6UT)'44WS#1|7_@o a9`˅D#A}GV Exx.|LRE)|e!sX4 ^*MoTOq{u_c&}fu#= ɗG"aZx{xٖX6K֣lafP}ZN|7s>^>u;/6W+w^kTEo;%WEaFSەZ0LMQc;+x_>ύo<,J[ p9l2X s<ָ w$S/<)o>#SZq\3ݒy(r~_ne$<$ &Ĝ7SOJKIMY-3|hE]ݼ9~^UݯNrjyco9d7yATU蟕"d\_l-cmM4{>nmŇ^N$vMΤvg>z{OO4-O]juׄ$gceO zuikJ +~mܢۙ"sk|@VL3\ZՀB1wvoܠǝO {%B 4/^֍Y`6< > _wycܭ)ƭ̝vQ)kY'g:]:tCy X=۟m|5I_T}&~.8qX^`~))2dIHpoMgxg|=.JykpNrrB] o8H5ܜ@o$iKkH` |⇤gr4Dԗ>5yu3}GAu :ÕPGPeukaie{@ jDFF!,@ ~`yzeLN'2F c;mQm+FVpgAHёe;޷CrSNHRON|]ٰ?{}T̫qUWVf!0bm8o<ͧn[F~.aiN8wJԩF{}K੎@}A{Vw6B dS[\WaK$yQy/ ;v"<~|#S"̓ |F|j'J('oi9hDqMYP/=Mv[qhͰ֎V'`h#1Hu N8s|.E~at:ٔGݏ(GQ9'kg6rJ*~km4~0R1.Szr[X12!*ԳoQ·L 6]='8Mtlζ‘72̪C؝ش-LCIHDGdܽ2eųj<;TUV31inJ$GZ]F5`Ӿ5O>I8?*(37*m@ ?ݞ=`،ܳNǹcf;&#YbV#>7OQq? ڽeȼl'JeQF`ISo(d_=9:z\%xwyRK{=z\$IΣ[OPD1ux Ҳ*͗]@ UC[߲ͬ;lHqb^[].fs_߶6!T Zټfƾs9>_fR 4I2n\>8:(PyV8NsK1YAҲ2 BO!Gfr{ѳݑCfc_sPȇz|I ,y}י'jzIUU㥼JAnh6qӽ/Bb1#7_u(N:tYsgL^c6<*W(DRS''aڛ=2_^pOgSba<窢Oקyt{}^^nnn^yLXV\\\\&q /G^`ofyf5z->$FV>V׳ˊLcF_t$!rg ٸrЌ{@ D A=Bj7!h=(5  [x.ta9a?ɦxϟvχY뢗I^<]@q #ZxI ÅEy\Wh<1켂l ߿5=VscT==flnBxt%̟Ό0Uh).=6C hv9d#:J(K+igڵu)?jHq0K03p.f @w&mȤ ԑ KE-=7aqP: L.u_M$ף^u34釚}kǡeԌ܎:nL[O%@7^m= ڟ-Ԏva_04dSn PUs% 㾽oU1EO5{E't٦ړ I[5R).+}*e~ qnрBkI٧x \iԯ{ًi -:)$D-PSnGa Qha|{ա|)bTiZD)돲.:^Jޮx-.jj'4tMzr. Yy>EW=znu a?/[̈́@(%<.P)F/z/:$-g=E|3& V[Zp߁,-Ru#p/(P‱7eI e:zTqv -HRʸwdN aQ( C^up^c[kS\s3]nr?YB(6*^Ӊӯn=X0d$o5v⸝=;}]sN6LN@SܷgȷG p_~s)oiW…x6 r r:< "jtohz笅֦;ʹ%MmMtuФ|lTb?qlӢ4>ˮr /Ϧ0u >a֠RlunD%6С~a3h*:2jG s񧳧>\hX᳇;ldxAJ޽XQ@'G#Pw}B1"^\Pd.͍/b77Qp=2!h<wرx!6} v+")n _Z۳Z`ڿ@/x7hć@ goet[ENm%S)$!^Fc#mh!/lk'7s21 N‹clT9xk7!N>rT vu5w~@f>hHhO ^}@39g%8H2n`?zӷj۹O>1Nbvvfⓣ Lؿ]QƯs"ȆU_P_/ Dj=e D0&v4Aұ9 G˨"3@4$g&z<eڱf;e`;&QJYޝu+.fU:3fD]$MC&x**c+S+GO =1?r3t%T둥T=t;fmfѸk *}[aC D Nm7#aZ<650!ͤ窿z*6Y\/׭L.@h"LkB#O<4~fl!ljymuf>h8i}|{uNCGi},HIP-n6fC~3ZۣŹ sk,\ۛ|˼ZO!8C:sPX$Vm _߾Ҕ3@4j4N1viM^A=0x)Q5L>8DEYEb̸1_=ux́7 *I|UBGVqٛMI^MwXs7!5!DCF39o.p1! oJ:`϶fkea:7{\U޿S&t;ZSY.D(q~䦉Ai}vne1+NU̽!0!DCLN5spD%I{6[epNrrB9%[UT,:֒ $U 3;ɾtvrsr?""=.l>Emuw<Xɻ gG DZRj=YA>Cf?=ٞxҎb@Dj4\z4&xr֜EvԺzqR?='4q}}Tf ̇@ 'ȷ@ &! NԻ`bƠ\# +Ǩ,ACD [ԾLP#_5xJtxaTƲb+jq}w7-%&>XC^XƓg&*EV-1 s.1>-M$QʓX|ْ%w>.Ta?D4Yh^B٬X_:dF׆)*5ja _rۑEs7F&K10`ц 茪b+v NjBMQs{yG.m|,kLLט7hAښ8IeP$p . T6ZK<SLΩeΔmzb)FP@& xťR<ҕ:M3N=U4x'}@KK#אW?၎&s^ ߯lca16tڗ"S䱫zON؈cl1pō,BogldAd6ΘclM\NYHg/roBj7.vGqg³"i9dkh:x WRYO7{['H*DdiGqYNp U>'`hҡSCO'ib^h /pq\Y"k[F8. Xzԇ܊t+>+LWN{Œ'zμZ.d;R8OBJsqpݑkϏbQa N[k 캿@VQm _pr Y>%8$%ә8ܸGt҂JQ=j21O8Dzy1<<$C#RҼk-{'?݅5ҢѼSDN 5|Ç'ތ"|]u2w>:'ϭ/* 1lF9vŘq6;XcWr/cOq(P,msi< }^ܥfI7 3n[Mۈ_"Twg*='2nIaA,Cj|좏EyKf_M>&ݐjݸ".ne88k+: M5YEvGy}AQ#FI|Oto\WѝeWWlZ*(x}b՞vS{[VvWI1ɕ>+9WE*>'GVfy{}5P ]R,frLT^JU>wO0PI "o/Ǧ@M+O0Qif|b1s5IUUO/!x񇲤ivmKIJgC_,JvۥGq13 f5xE{lµCy~R<=zn닑{$wyMv{Maoo7 V&Iwjgmj|+(u+{ĿszjKgT+ɿgJˢdJ##m;Ln,8_^ѿDwN p SJ'\;=%CþW'썲RէHh4ߍ)G=hjS?$}V?}qmu+φ?D4MLFH]jȼlSG0niѲ!jt6;;EW͕f,YTεQ0Κ !Ҩfr6:PGVZYqVӊj!њ{o*zQJBޖks3WV^8 ?D4Yo@ MC D C4i  8R?,XgNaQՅ$ڤqחZ:n:^d7q t9ܞhOj" aPu{?~8 CH,F!zX;N>}>,z4^@+XMct}䨖j8F50.-?4TN-σ:~zõ/ٺc[WO(!kK߷ j" !h<=|{~8R*?gD҇SDH$g$ιj=i62r dSu?^R<0ĥd”vsH`W;3Ka5qǫ|Q{.[Iwtzc 零=Olzg"ҿ *1L2mYkfA1XKn4VPij]}*WЭٯ=5FLw_'$'lOkDReǻ9.K2)LڥE cGeq]Ԫ2e!98窫(LSqqQnV ii(~*c1}(tnaZi1r_SUX]%{}xDhXoEKb/`nvҚ=YJ(6g2HrYaJ̓8T&% 'ZOOL+ .fg 4*.hD)ˑJk+"jO{ke+xaeoHgi&NzY:WIHIw'Qօ%Kݹ㵊3+{׷׫t9$"dy51g|EVJ,lfS,psBuOI$]L[2D:F_lLdQ2FRYޝu+.V>33'B/zrO1ܵ͌dS,)zK+aI)5)YTHv|VK|kJ) fh@)I_e|bmJ MypQJ–gęRM\(,R"5Z=?Ǯna9jڛ=2_^pOg7xCEPx:Wˉ}:_Jj)wy38ֻv|տ;uaVLΆ ma}f6vfg 9]mc^k*@0P/TcoE]jI/&IQĶNu7g-\?ػ~uaE!sD*(=e&惎6EĸQv)C0P?Ϻ=e~0ɟ=l͌8.&8:Ʀnfȅy%q7~?< @ C DфPu{M dC k7_@ 5Z m0 3@|7LNu;xF{J?K;cm_6=(R&8fXkG+SƋ@ Ԓ3SʷG4=xʄӆޚM;POhOy T)͸q$nl5in#~zv.'7aܜ.,j&QJ{P; WcZi̷bh36Vr@ Qu [SR/zv ʥL'_;ޱ믊|I/Ě/wАd5ђ JL@ n!|{lג1Δ4a\y"3@'jn|{0mz ٴԫ\ f'K |) Df>PO^-z"헳}`sK3 S}RCAf>PKoOy@ %M˷<̇@ ƳnnAf>h @ ȷ@ &:kRT5\H@ 8jn6}Ke4 ۓ'dwB-Z}w%GFH@ jnQ>yVд,jao{B=SVTo|BHj14}rT vu5w~@F.-8WȒ??Dx,bߞHtpkZLU Xꉗn? NHQAJ<+@b?Ѵi<=e|{L 6]='8Mtlζ‘72"^-Ga[,!󣸇g^}fyțG߉d ,2B] աu\S4l$n3ӿ'XMӉ %a"h4ўr==;Ǯ3nq0Խ>Kw`{aZ:n=r6w?cYͨBB]Y |{j|좏EyKf_M@g Cԙ[ZѧT9!Kɪ5@Ye3,߇Vd fomim.^TћдM4=s6pJs+$|VVNr)Y߃ JLn$Ak 8NV9dDG6[(J$C Mua eWpEZ?%i-hojk8FiGWߥe0PI "o/Gi "^eeI)ۖ|'Xφ "9un~ Vz|IJE hʨh}{8?rĠ>;72s`ٕYӸ>Y.cܭlJm?[aޡMD`K+Sn]YB(#^@3݃ﶾG2xdG<-M642֝F̂)^$$C Mkp|{DHGolԣʧDb?Ao [wTbu)@4yTiߎܷ'/c&cj[Tʦm@b?tAfƝ ^*͸] ^*@@ &?G`bƠ\w +ǨBDkh=W},XgNT%j=BH|{`[Ck/"!xG(ģS@SQ-}n]n,c}sXAgVcְlX6,o䧖wp ERs=m6?YfIS6}oz $bY[B+Ry*zNUQRӍG7uoM>5yv^]%:AD`ͭ TQ]dDR :]\"W'm{klq ]*XLW 01*>lN1pX:S9biH@nOcVo D M_ˆcl/EB,%1$jM_80߄Aoxtbb%fpqҤ.Braw=E%=3Tɿ ~˜pdB0,'dq_7{[)%Oy)\ʋ=6vĥ\̻M~94;N>HK HF$/,ʳuK>K/MP.Ɲyjs O8-ct 90Gb cqż]]WIolq\>-x:{߼4!+ʫEԃ2N68nXƾ(͗8r3==ZU "듵w%<㯽KD D#=+*uTƯeK?+"9tt}v:R$s7?ӽK +T0E<%B"{J%F 8;SηWH|{j=*c6ωt.n8tYQ\ '7ґgImc7KFsb߇U|9FTM]R35T$+ Cz$'w9G5#9Z t}-kl4(kIyz.N:ɚݩ=&">+Zz^];J "n"BUXk5@YZ6$%~ VBȏ+.$dOD TD ߎRFRH#VFT PyE oVǰ/ҾҢ(6=w{#烸ǵWwk4ݽv?;ZAi*ΒW A+0$,;CfQ1t4HeD՝ U (+tםH)O]HȞ@:I/07Kg6kH"FmW R)ils6kIDjtz_:PifZf|t)zҬjA\UPvmS5Ѣ8fHr@}'.?{i:ҴRv՞kPiRQB>pz1HnƚuoE\Uh3qFˆWz$4 MTZW%5Nq)t/2yHD {""jnA|{Tm+;Vիo$RAAN4r)d=zXsE1?*`u԰fj\iD~ 0 Z0tK^eaҒ 'k:5-۶L/(jkh߼YV,ERiC9 7 w$'l"UE9O~`${KZS{CQiXj{(K )EE {"*j4}{r.8BO<Oɔ=PVG8{L{[>P:dOTm&{#ڹur}oNz=3zkp.m.|\T~=%`lCjovI8?јQZ VhrףK̝|($<փyO<=CzO0º&$:Q?Wn˷_7owqF{b-?.#%W,5lA=P3SGheyGߓoɧ~qnj+ }*V~iʇ$+\U~qDwiiHT!J w#[M+Tk-.$ zQJBޖMց%\LսED~Qeukaie{@ jDFF)Qu{@3iݞ/V]oOJlP+pahSZF~*U&;/xt;(|)nmܧ,)F?J'fvqqݧպ~FE39X[:SgdvIlհ97bOz,}}qO\c1YxU+֧B j7k_ u3CO]_ T1"#*ҏ΁Ω8@eYZ[Tmh:iΝuC,8v䃿|lӭ2{XrR֑Zf'uR+ l IDATfn1m׳ BJbrkByhOβGJZ9"5s.&Yo;r ^4՚hHXxȼ+qo~ૼ)J1O*S+ U3Wt̬͘b322bU[A4^~y;$)GO;qɑ{n۽";qllؘ̻w1ѷog'߲Tb{L7M*CX>^^o{!sC _9legEl`߈>Kȼπ < JT.Wz0R,_t+)lffݫ淟qǥѭYwŤ3P߱m2DZX?SG*Z0I^X8?/rݏ|> K^ydcb<Ĝ;+:[r$A(8ֻ}Y}^82<\tbjgw #y˯gOg9q +ߛ,>wʼnc9Zy'9j2#LnqC#&$ɓ|9}8c?v5RzeH nȦR\tŀ#)bDžb:ퟫIDf>PlO) f=ᓄ8.+zzwW#so ^j(}ڪopq\rrkҍJ:+^C/Tqduy|H$ަ~C#1ǰlAeEwgؚ\X4ghVAoExIUr)Ĥwwn>x,_?v*X'Lw[D.8yNn/E,m[VEon3[@_>Ckv{ҘcG(/niɍ,1Ҍc]MV.n,J( Xzԇ\'qN54r{<+) _,`O"aq.jaa鍤bnNN&vĉ$:tA.t5,X(N;5zT~6-ɈOW@n "nZhc3yGRV݃n% ʲ>}'QJ6D*תB\}7BV#>?s3.S5jgF=,[:T".xˬlAhLnKHxYHO*ԁlF% ql=i4G 8J_lmսj'QuKL'|~_B1fRy /K:~`G5_6x>_IsCVw> c-gOjiߨf:t)_1uoi6>^#`Ç'ތ*%z2u2w>:'ϭ:xE^.g 7K")Y fcW7asѸky(=kD{aہŽY~*BV嶩&(Zе:L~;p}Qi9Do w l L/ NkO(/(2#8=ڌMgM(@xToJ!˥;?xL\\{yɝ!ZHT:VN @e{*d|) \unJJ>ybeXջݧb QBc:E划.yh:0Uջ9WnPYz,f2{\<̖<^ A$ v6UZ!mF9 'ϸh{(LSS3+Vof~;4C@P:UAic.7vyf TNfrjupcnp?JxB6L  ^}Ew&B\V 6}LJx:)ǸRŹj4Fw壢exӎ`SKLOy%||5 1Uá'Wy$yq8ljC5O%2,l*k?5e2rT/H. [vi!@v~ނVa,IPPuꦠ*(hOQ&$0=IkMEG/3s} Gkc$k%'x`. / jDhJ9!RAP#$ ;|/Oz=@Яfpah J'oJ#w$oҥM] >婓M|{;s?f^:fTC]SC2Yq֦ O}f: r %?WL"i+y֥x zp᣽xLeN[rSbugјX䘲}H‡Ę8T_=w̙u3߉NK*o>LP]I^ae]]Mߥ٭(%o@ݷGM&2PR O!Zm?%N1pyG!^%%,tsw7o)fc]߁'N@خl5'ϖ3+9S>&JNDIxO׭x%;[TEgwݻ deF?lUme#'#-+1NVc u?-. h Ӑ4]x*zu-6PYZVFZh[~tJC!MB6m넾*2Ҳ2Ҋ#UTE7˫|\싷P󕜭oϖOKZظTC7$-+#73G?6-|0kS'/#-mfB5һ(%AM>WrQ}Zj:yz}:c Dvũ飨2MbR]iYi~x~e7Kv1W;̾foB1q;mGQ/f>,MCPkq7]nNC<[nICR>8 (g֋ȣp7itA< 3L_0a:DQʴO\» _ _FcLNT}wPqP.x&;BV_ ) BkZiSۇx0 C"}{ != ?J`0 = a0 Dh`0$G{ !8`0 `H0 C"p`0= a0 Dh`0$G{ !8`0 `H0 C"pG:ؖ,_mnTTԦM/_=9}Ǐw+|wm ә5^g?իիW۬K.UVV6JOO733+..VUU>|ʔ))|;`s 6ׯj71OVv61O^AAMUOrvlmmt޽˖-n_uv5 +YSg\Ϟ=7o޼vڤ$///}Dܹ̙3߇aÆyxx$%%1N Ȉmܸ… aݻ:::֭b~֭[RRR زe |!dɒN˗/+++ׯ_/++%77o߾|g;qķo uV eUUŋCCC WZU\\ٳm۶ĉ޽4~xF)oo;w{ݺu;vۇyzzvuŊϟ??ׯݺu300ذatKP(?~6mZ׮]9p`۶muWK k{!%((hddHqvvqH!!!EEERgϞ8qɓ#"" :~7olmmmmm|C-//6mUzz:HKTUUҒכNo8W`X5{m Bx~"..b>tK"lbeeդjaP(?~d^~]FFѣgkjjBv풗/**jR TTTXS7n܈;x S˖-c< 3^h4--{"=z$))Iӛw%|}}%%%8h4/_\\\;vggիW>}G ;B$gg@KK1GR>}aÆhiiyFzyy1>ʌo߾Λ7o8qAu]t^nCf"ٌz444/A[[АȈFL:UFFxUQQQճgf]BBFEDDJJJ5ĄYOrr{~~~= bnn.$$TVV3q簤M<7i$S@BB·oƎ0bI}||tI4,,RWWwRRRgƾ3gv҅C3336`QQWUUyǏ***ӦMcu=RǫWLLL͛7uԧO9uꔺ:nnnw֭ی3DDDVVVAAAc?YO#0RVV6k,OOٳg?5ёȤӧO ) !>É= ((ح[7!!!//QF*УGׯ_sJzzz:88NO1N0>bHIIm~… 8Tr#Gp`= rrrC"x:OKKKpBRAR3i8-sݽ{޽{̟ )NiQWWWSSxb{;=zq !mSRR>}4mڴv4hP޽  )Ni_~siҥK͏! {MMM^^9?𵵵T`0!E'((.D꬐¤=LPPbpiT@AAC> 8߹c{nrrr]*##7u`8CKJJJJJۋ]h7H}(..fi)=2z'}!!!㪛C;\_```YYYYe^ۅv=$$dɖH{beeUSSd޽;0!Et2,--I>)((v5`mCZ} w҂= WHyң[\!sI `mӉ-k{uIN  `f`uȟQ˗/E@澷_|}6TL))m۶d4uTUiiiرcӦM222+H;Af}}o/ >e˖Ĵ8TEP6lذc 7CRܮ.))IZyގP(ߏ7ÅNNN/ZhBBֱ )= f䤤pS*!!຺:h}}=A&MsosCF!Eھ74@k\ْA@@˗/KJJzxx+Wx{{3^4hО={= WHam;֪,--9||ȑ#̙kii),,JIIot q:R߯_)Spvv.((Yd C] ַHw۳ 8wccc ֭[XyްJG:eϞ=EEElR33 Z[[ڵ+77 ₂^CC B0aXDDu$cyT*5**̬yM̯#yڵ nܸy;;K. pvɄYӿ~^;q %%;vݿÃ9SPPЭ[7ڵkaa!,..ٳ 68;;_vmРAcya.=7o_7oޤRxph=w/$(k׮upp`emm}q棬Bh}=zdf=٘"=z%֬ÇqF544UPPx=B.###444##WDEE"?z[h#uuu5B BёJEO iit|y뷷 F'ş?UVvٲe-gL^RB^^^DSSϟ24EEEq 5550j(OOzpwwWWWӃڞ(L áCQ;fY"s/_~Ν&K4tk.;LGSxÕoƍ7nHMMh˖-+//勾3\]]>|ʐYii7ڂꐐvξqF9ϟϡ.}{t}{#Gf>2z޽{pppϞ=@HH7??Ƭ O„  FFm;CFzѣGtzϞ=+Çg> ?Meoffw/[^A Ks)}'-x+j^KݷjdggxvY"sI pSk ɓi44Sm-2`mRD{-1~-~ͱ7n rʪUYLMM= W:ҙEA((yvi60T4aV]< ¢Laaa&LغuKvرqwviNG:o\E-- Skm[xv\J}uK/ỸX~,XAqq1신a !!azzz%%%̈B4r0\!EGpc*:6oAQAf}}WTk޵kT*ΎF@@2dhWWWh>g81\5ARBBޖTUU1nYYY˃6WTTdkeeell<xxx0si4(qg )Vrgl] LLL矃2=553g6766NNNf>:מ4ÅA\11?w{vyޖhjj:::}w9[YY}xԩS.\ʕ+GfN83Xpk{? VVգm̢E맦jkk=#Ȩ7Wg81\^SfTdַ?]v\F%!!Afa̘O&`ZIUUU```IIɻwh?+g~YYY111˖-nbH jj[=j;ȬoݻwHaÆ5YY-[P(Z+Xcc7ӦY"s9tڕܖ3`m 3fӧdNZ )=^ [v`0RD{d{?55~1#pk{-". E澷""" ,`1ݜ^km~~~+ssb0m Jlɠ255UKKHmcǎe\`*Hqձ1Y"sۑƭ-85%&&̙3yyyW^|2Ngd^aB|Oz耸8֣y~DE_|dxH4g? 66qԄM6[&LPVV N Wx]!?R|{Lw#E޾854Zx@@-c|LJJb999FҊey @R= WHca~ؽ`Ӳa{fѣG/_444P(߾}KLLd1BN Wx]!?R޼;m y-r-YreBBӧOm6FׅB}a[[[߻w8qB##/,^>zh`mq,io'0?]v566 vɫW_^edQQ,?tۋ//k{8rr_QAf}}o{5{o߾ڵ̙3\LHHٳq7 #(/ E DFF^v-66vΝKZ@WW7((C7nq5pw&'+66pN2[d@uu^ΝʡxiiDEEyjk{`mp8 / #GlXN΃"7X#>46Bj E澓a=BP(`co[{0 e?emŵŭ@f}}'-x{AdN;c}+cĎnξ;+{\۱Zw)dַw҂= WGkkذt6kΧv4(y2g̝1f<*ӎb0 %GUU}gz4!kY<"a1PieV6 E澓aBhWMzhq[Aqo]+/&Km dַw҂g81\!EKP^41YɒK<[@GV6zȬo>ٜRRR}UU'N*0Abbݻ ̨TcfΜɡ*x$LYɉ2eR@@٘1c߹DPV{k{>*)7f]^H@޴{NNiY"s }}JKKmٲ%3'' rm…gϞI+&{]bbcǔm& L]]}ܷfW'] z^=/N8*n=dַvF[YYd!CpJ@@˗[3th/&&Ȩk׮@PիW#G3gN JmoƊ@|)S=q[-263kQ ۷/kJQQ͛7_zUWWw=???fͳgψ= W:LWVVƜݻ6:t޽{wٻwѣG0nܸf:ׯ_zcl@@PYPRrp))$h_D^C*J@@A$$$Z2x>󱤤֭[s}=py0r>|RUNNN~++gffy 5ڣ;lj6bD[:zϟ0UTTN8ђ#k׮>ڵaty}1uu-1ZGASp _|}t[ZZ۷Ғmk^^lSddƶ39qDCCcǎdhee5e۷gΜ)**tR###/_3oٳ'LQQ166VQQ5qذa[nLSRRB/Czz{555D'=BB33gfP(sv28kvZZ55p.CP0~*(>Эxi˽j*cmmmii _3^8::JHH̝;NĘ]!p۳؏?N0/^ȰIKKc(..qS][;txB?}x@ڡkw FF@@@Ct4lw!E澷%s%$$@LL S؋w^zzqVmݫTWTTdddMbaa=11ׯϟ?gΜQQQC1l&O1p@uuu--;wŘ1c[$A]mɴqb\^%  P( cƀnB'ַB澷%jjjk֬QWW?sLff+#}֬YT*wJ,VVV;v`>'&&KIIݹs'//9G QQQ\ep)!!ѿaaaaaakMMH,J nwyS#:} #Gv]9}h,R=#G@B,,[PS&Vr-ҥϟLf֬bAAƆW^յH~~~555bbbUUUMxbʕĝ3tm59;2Y%."6Ui틌a얍l S=뇮l)dS(pz̔)KHIWU$+E/W)+ؐY"s[)9:::|4g81\^S r*sFkl6byKs~LLeʸ'Jשׁ}g˳@ETqqq,vɬok{`m)Ѯs8 { O]L(@YotinXUqذzz%Kn47663lv+`k{M6R|bׯFX~{zbQbZikjEGOuGID쁬,p# R(.KTU-2`mRD{{RM)6 , Chh gg-w-!喱{54hhP@,[ԫOkkɬopbґNiK,%h̘qD $&=ۜ=zSeejr ;qm|Ic۷҉˗:iO޾= WH 99QQG]Xi4Xv'Űb(g*7V{,kzTedžVRRl99A`[V(+'LW0pܽ{7%%hgFz!C^WrvafoΖ:>};Hiqb{_jj&]c?^ 7Ã=<٫T^/pgrq ''1~$k{`m<4p&j"# ;W=Mm4UګϤRGxLKˎܭ[!4z(H<8AA" j{bN<N WHȾĢD[-[j6M`* hz|4ڤϟW,zJ׮gOCKMͭ#^sЉ=CWWh )="%M֟,"(S)Y-mOe_r- &&ܳ'b BRoh ee$^`0 )="xd0|8|h@׀(+#DD`cNu†+[LlT^`m`mRD{\yh,~JۗiE2ϓi4:*oDŋ!>*RlTUM*.湹Xgֶwb0ogm/$5ooaxneڢ#IaaDEEQCC[L,JGh`Wc0?^^Kڞ{$Iյg,]\⼪ecj^׮VRTrȗI/ysQP[ l39[KRee߱45᯿q1: .,Yn3܈yLO}Yз2Ν ppׯX. 660p xymUiNgg2657G&&#F|:nok{揆h=w/$(u6lذaÆ&tDWsVK(LࡢO<*-mSWQYB ""8}ӝ & +N!{{9gP>[6EOhTa\YᄂGJ{%BKODZZ(11 ĚVT_?GލF:QAFx.0F>ТEH]ݼQmmk?%%%IIIƈ#?^`hG۷o߾&1y1jr=ie<=&~ yWQrjQZ6c*wJJ\#n°VSԐ?5)iT(Ӻs5\yZ{wt ef\ŷoHS/)Z;"bmJJ=Їav|wqUU<7PNڼuEӦ%K9="_(5ZQ#8272*:&LG$4EEEum=}$ ü 3?- ?N]]DBkyuI0}:<PvkhmL7 LL`ha,ټ64ŭg⯥_-4Y+B y o߂ ͞=3!ᐦR%% z^G{&XmǏa|HLuC1)[ޤy QARj) V<1cLLy7kW^`|ȫ'u6lggnssYuڔ("g%u +VŋW*rݻ6SN!— N|Ӝ fuw:^6/7m1,QRrg/~Ď Bdpqa3 8iiCӧ*!ڹd9rB/T.ZeYu:'-'ԫFx MccdeAL `T8z'Tee m}k{T˯ʗ:&UO'ZũShX&HL UW0?_3iNVy R޿oE]HN,o/C},/ :KpY\Hoq1:?Г'<RWG6zvh&?|nW*;)}27" ޽HYsJEqqC4>-\z^ovMlzxh` w  ҋ+11]g I[oM}8hB$+/~6 }Bղ!!Q$&L~0[[И1|z3S"bIhtDi}0 54dsEHu5]zZIjRjjCC_/*/oB(L6$-N!:9:"sEEhX4bbpadn LWUnۤxR1HI -X>}TKa!:rIKBlMW&'0??\Z]Yy<3S/2 2TVV_Z!ھM25Eӧ[N;j{d؛x_YCp6w/nl4zbN~"=gm-oE?ht ^4.k72i44dǺ:-ZC?n8d%!_W+"@v! K\ބ%%򡡩55ݺ ~/h4t)1⿁ְ}EcN[ xxc_RG(8h-eehB""X#߾ILOϡR9T@G(lǏ/h$'F@"77|AAA=q# != oa RD{Fz?2YY de .BV#܄y vsfe k>r54bhhLe%BF>]hBE]rhf(kwHANܺ Ij#c5gF:}Dt4F4z4ڸGo~\vAddcȨ?h4QAi{moٖSf#ZCPJ RWG+W">֣>yё#UU&QQֱw`r+(P ]TVU֬AhFrtM$'h{YFѐcJHdZkɷ?pRGuu"㙙tdfl}u !HN}$9:/Z<۩N}ul,^"yq'4ӧ5!-HK ͝K0\!Edsݛ6*y*O2{Qg!&yrvBQA&$~@/ eax͞6W[vD)+S e? [h#!<<~8鞝3g5ϳDB N7ƍJJJSfrXXd3%m܈ы 7;PӒ-RT\8|8A+N=-}뢢`\xb{^K\8,bZ/wvܠ#7;V޽u+v`24 )a?gOUU'8|ҭܼ WCAGZ!xWwdggޝ];ӧJo//ƍ}:*^-)yۯ ;]ۚK IHx<9NZ  ۰3! WHj{ϾuKJEpw1F]I QT"PImQ:da TR-Rjq-ޭ\գz[ɡ-#veICRhh}73Y6]ieǠ i%ir"#_x\YQG4I/w\.s婇g7 JKQ>a^|bër!8m 0\!EDŽh~i~܅ap:@hAbbw!::s-,R+=6+8xuΰ~=mk^M =4 غ_O~'V僖o{賭iiSL ^oDE7!crsa:w%'##Jf=}!*Cd-ZÆB"烱1$RՁI'Q?xGzo߾*M1))K-n5͔mcCe%`0!E"#zih`\Oώ[8"}u{k{99G6E5bOwL\(H#}p)3sDt4MtDpo®ӑmu*?ѐ5ڱ`Ͼ#z:tiC]Q ePY qzakUU:K7ʳv&bLkp DG^PPlzXd; Zܼ ^q} 1,GlPQ\|I"h!'$oo2pgHNq #t57W6$i!nJJ8;`}ӺUoVRl-20`h yY&O}4uu? ЂlADD@ VXbX:ZwuբV½GNr ( 2ef 7s=[ S,0G|+AY ߽{egb L99hi [XhT~Ŀ4r_5yHfͰt6;;ٳhj{\nU%,88XIG磣##w>Qd~~J+tݡa4ȧ=CCCC+/%ew\ _j!Z 21QFFwV&j͇9G|HLs_Px=.wZٿ٧gkתݽqw"ΝIXF^;#{]ǟ;Qˬo={Y`"0ڵ׆s4B&i?y b9RC#}ƍʾ:Z:v8!٠AE#puCX~D<:tɓ56;8 ?]ɴKCBa=PNQ|A,ҥKϟnZpww՚aĝ<߄YX?SEFa*a$f(3D4699ϯwN*N+7;9L BCUazze'%탻&JNĖ.- MLm EKKVJD"ǻw?'u@Jٿ ď?gСʎ`vWˮ*@apd;]\UKs^葜{[mmWGCZB5Î;ׯoff_^_n׮M$g̘eX g'?A HFÜhqls2zx({=8s\ӝJh_UPgڳg ]I/5 ޙښzd<]آO|tiěc_l ]K|xM1?j Y*&MrGQI xDD.mpRp5g=GGX1#9j׮0… ;vX>}L6a/^ԭ[7C9s4j\heWl'W ؙ{;t@i|T0D?8(\oϽʽ*!:ZPϤUʣ/qQ|ظ8D~ 75,WV+y3zz*ס4ka=1t53g?桭mSbnV2$IR2sr+\*grBt瞾IB5;x`YYY~KƍT]_t0x$:8`׮Pii =~|1+:uw|mes/*ĩSbb%%?^=6Zzu߾y $Gz1ܾ66'}$kiгO:9țZNf&=MIlGᦆ9JOb[b22nX.2®.q=$ )S[8..Yf⯍̞ϫ][vdd<|~q׏~_7 n^a͚ݻi<ګ['@ِ-[% Sـur2 tkںcΟ3޽{weѢEo֯_wٵk׆ &֭[:fD%Ch8nZ.#!._3  u{T#tQ\_<--m֖lG5H1"ne߷k|o Baa u)}\OjVS |1K}p88 QS"dA>po1ǽͭյVecC^kurut@tS !S #. /b^2\;(R_Me;[h +y$[SS+W,3|H^gϞ̞=k6ۂPI ~~2U"g\m#Wޕ5 ?^K hi)Ʌ>|a=""vJvľnPqjƫoh(!$(ia!"\o)uaNoߢy UVL O 7]k)77d;]h_BZӊ[j̼͛?~~񅌌oJy9::xKFGGaaݺъBǎpze۩W]qU8ȑ#pxcI ۧ`Ti=u w-z{$Ȱl̟͹Bΰy ~lyz[*XJʙ5݃gKFC=_lm<{gF#=v\ aM nF.<7Kg=3V?,k7J.n}aNBN;t(nݺ4oȧ=FyPH|ݻ[[[]lFFF ?BGΞ=;zhuE+ѫ\ q9x'yN_> и1S9ml&%Uܵ!͆1Tl½{0eJm˖ ˪ $$גfC~RAdNo]7S"} 6K?5hP  omh=a rsadص .]`OT؇]_hIX*n=USRFF{KL/H7^c,ڥOG>}+PXճ!3(g8M66x26 BwIݛ(>Bϟ)VVxjM!GrK}]j̼= R* ^"aaCGK5qB7)>,I~ OCss[?ڕQ":*=Fi3324N( ~Pa2׎:hu#U3<1;15ML06Vd=wW/^{vvӧO^Ekk埞=#''S!\Reg\Z$_dZZS"QV@=PWz{j1in)MxOeW3S4zi{<%?zz֢n=9{'NTl.w^Æ+>,l}&ysj  11 rJ7jnqY9J9xB??@zaw][[qn||:B;?C@wk[QQ L&<}HH6>N3|'a k/XVV0kD(93iK6Zhl'OTE4OML=D:u/K3RZ:W7FUD dw;Z;u;v?d?^IO}Br׎ 2RYqv6ZX>wMMEr℄QQtE''V===L>~&&(cM42Lȥ6R(mZ)R(&ML0<\n|+ Ѵx5e춒'-.3UKv }@f9 7p_Krr8NNBaLa!Iӈޞzs{()[@ʻz䷊Kx- 6TWTA`x03? 3ݻR1'q :X)Y8jvxq͎^AzUiûwD~tnO#41y?yN0FD,'m[?=c?LegСb2(q{[X6+vq2k =˅^wVTce`չa'X/o< ZL OCZ 槞=kk˗*PPÔXo_;-%^cc9TIx9xifmy˅$%WX@X<)7o_~`ڵWFqtܛvI pa`o/ҹ=-`<23a~|;᯲64Ҳ>=[z{X( _lmKg }gZi,ݪ^=R浉шޞs{x$vi:lRשSٕYMSb-R~y楙<644(WձQ(įU^H43tM+7:6 l ݻU ӝg:5 z&mp i$ݼy<#qhYzlKgDzKBZZ *w|I&SCB۫zw!'4$ Sbb >w9oܩl@{"0W]ڨ/Uɣ9зl cœ90!ܼyR똯zܿ")cw|gvmm 0t(zokFaf:խn`ttԥ&7nׯazv1Ioa^ _'xƪ~G ONnTjKG}}8]V#5v.~OL^fgggvTOO84r ˁ-?0aԪUj-[*[m4F}c|7XJѣC,MZިƒ.ǸV]_TnW/w/(h| xro函֒z^9S:խ;&.)W+UXgS{7###""!ObNr:&4}>ݙ2})MZpMTeee +W|jLP{nOl`Iʚ6alViժUQ:RVfY ۲Ϊƍ *fc88@.JRnm_Lk6O?ܞ8>s&L I8س.]c$22??q0v`&M2 h5 NғK,)F{gg0{6|:m ^8kpKʎWN,Li9.iKhE>VN9}>YYh`ggq*#o_ Ml,a艡0##bpn.cAmNN1rΛr&3Oy~Mؾ؄czBF@WL&ċ-92|ǻw,/-[ɪ,J^ړ'ب]AIA&kMߦ좝wXAC,UG%j^T"v~rvltw=|$<4ZaI *)(0 `s\vs{G 8k_v"dhj*v"Ρ)FG/i7wOU+JI%%NwPn{^zyan(wRwz|%=CT88:zђaÆXj})+G:TٳX޽fFXSfdd8?ϏQC59 CB%%ImB(9sA;D__в5ų/϶`u2$nߝ;.r꭮'7X$K?-CȆG9`wj{xPN$ 항&RRbtoe>;t`=NES76 #K/5L+

H!^7iq R8[Y3gV},{$ʵr8֯7J60RR $&q )aaأ`޸q#^x8'cFhi׳6noYgy{~AS"pœLƕG=FOn?GOY3ӵRWz-UnE S> x]RbPB˗c*"ߙ}ŵkhb";Yܞ?Z[\bᛇR0ȬZm*8l(hk'cFVrrp(tpP1`XLypDkD{DF߆7_N^To(loFΉVSy4MҥkءMLi=ܮSY305U0w iSқsKr 3dBuu7ۏ oZ[j~gL ÇOTP0!fV:\sFu`@8R8oŋ hW >68*=jߠ}: )M<7EZZ <f̀ٳ@Z))g   mfum,D`ڵ 2wܾK}BHehrT~I:9X۫V9qN(/S[n5kRq|vܠĭ[mr>__<) ИK G8>z7 =L<mhbI[",pv&ۣG{#6`왱^#PO?=\{zpTWR4HtݻSR~|lqB_rD>~!'Qr{juz̷{pwaahgk=k/^`aހ#~al,\fp=7:Xl{Ĥ$D~:ԩI sﰻCmV3sO@|ʋT4BSq`\5ӴֶA۶ ny~ pGd w\z}tQ14wr˖}L)ceb[k)tvXaXh%|\m[X a&8={ϲ [BLɩ!;nȏC(rr "\]?M+货DƫW̙GE9rDMSOO[z}gdʕ>giSw\] #F~4DχdˡW/1>Fp,w; ######uGu֥;O y4\;d= Zcu/_%"۳GYn^UNXbҕ+cƴT[zI`A||@NέVLk#=,\ BJ z``PMBjdPocaK)q%]z׍ S@z7*9Wzøl{ 11q,lO/idQÆ#"ds^?8:fn-+sϓpvvn:v-NyiGz{jco..pTܙLVVS\~>Vf͂޽!50i\ :H>h:dz |ZZ5II]\R*`9k~)6m(ԃr{D!S|>ϽG,sOEL`lwwػww&"Xv[Xn_}#mjik|~#)dB!F䬆=1|}aJ_XONv|NO wvvcszBN&qH A؈)bIDATΝaX?]])g޹A^mJHOor;/W( 9Uwc_O5\FQr{j6`Ím'$`<-FhD(bJKqo{ Р̜ A~><~ A߾0v,L ryz\~kFF֫W=n114R^Icr}.;ΟN1/Cq1@L \ӦB9oOB[.ٳƯ6_yD_[3;8q VeJ'HEV=,~hޞ ՚F=&La1CaTŭէsnv6uvfk/b%bJD_1iNN#FrVĞ:4iĉ{Gp9 :{ի nnI,۫6ŋayBѰ`ܾ-w:Z:#]FtY8 !F593A [Y\XÆ5м46|SwZӈ^My补к5w;2?77mjl~ 4)G۫qt7`$عS1й34i{(P-ky{ꎂTk۫Yma7` 9έ!zш^MI4m !!`k fWCС3{@r{ꎂTk۫=!""#eX aXtQnQo_;U_\.,^z# v(H=B!Dr{_8o_;4=46|#D}46|SwZӈ!"Vr{B0,,,44T T0Lr)999L{SwZIlOOO===.MSSXYYq85jիAkl~ 4)G5)b kkWx==[(4)(HloO9ϟ??zhpƌs9edd$%%1 #RNNfy337VmիW7mooX!>ܹ۷)4)(Hto ?p ~G^>_V-:::-OKKKKLL|I@[[̙3JݪU+Ǡ5ŹOU͛e˖yzz.[,33KG uΝ?y4aaaO<)QGGO:)>ӳ#@ ]vHHH6m?K,B~M:o߾T_yGr i׮]vduuu{7Ⱥ@NNP(lBIf̘iӦ7o]v̙ݺuorʮ]޽ݾ}{qvr{Icr{ꎂTk5iހ޽{tRD\r!C]\\ׯFFFwtuu=<<ϟ/jf|4o9jҼ==zѣm?_?~Y4oO3iӼ=uGA#B*&TF=ͤN=uGA5;IMMUwjמ@Ԡn{DSH$Rwj3w\u6{z=(=B!DL#z{yyy"(//O݁dkOLL|f~5KX҈^zzzxxxF{N3/4aF2GxBQjMޞ#VAppC R-(ʰ*$B8B4"wՀcc &;gʕmll999:uj޼d{ffݻ߾}ۣG^z1*0۷Óg͚%h޾}̙3̙ӰaCX51<<իG[xWqq/\]]}||jb~T/ݻw7aÆٿ?05jԨérmڴ3g΢Eܹ#XPPо}ϟM4ijM>=::z֭iiiwi􁮮 YYYȰ=uT6m  pY'''??~IݑjDLXx@ D"ӧaΝ;[NA}>wQwCĎ;n۶Mq K̄BBbu |iӦ#G1&N졩MTTԏ?/Ĩ;OO[[[r@OOn߾ݡCq%GGGM$հ^ZZ𐅅ś7oԧOoiӦ;wiiii􌌌43`ccӯ_-[}u7nPwDUhݺu&&&=z4SSS. ~{gfnncm]ÆƗ@r[l6m1qƍ_pa πkCC+VtU!Ul޼Ο&ޞeIIINNT+++uiڴfV$\xQQQnnf~Zj(Ç͛wU++;'R)۫a=[[[[[˗/"^xKkbjϟ?wqqQoHjya ʪiӦ3kHtĉ-[7pљ3g^|]W%}xNb$>oY槟~JHHgѣ#Ǎ֚5k"""/IOO/333<<<;;ҲcǎjRcU!??… {ĉϞ=sssׯ'^B!bJ=B_xnB)_B=Ç_uvvvfffi~Bȗ͛7QQQC1bDݺu}}}! 4hkVwPBHu!!!;v/!259ڷo_pp+W:tPwDj @4ЫW5kٳ~KBB=YdC T_4B! toW@>B!5!hB #AG!DPoB! B=B!z{B4ҽ=ݯuL!z{B4!hB #AG!DPoB! B=B!z{B4!hB #AmBH fcm }ZH%s{揽: ekxJa^RҾP_גs(S\+HQB|\{#ޞt#|kȪhGBte*ўfC-{6x.JOzBբdP2# r`Dʜ1j#t|gp]q[l}aB4 wޞ~v KG޼|%;A>OTGWB= >~~W[_Gr1aޛK jiD S[K<^읶ol=$"[o{o70oo&C񼽞O*us<pjBL:&㶧og?o;5ZNV&&-k`+k5is]e^B&53]*ZBH ֺG\E'l,kkr^1w8`!|T+}p8uF,@Fm a&!)'bJzI!c @;/Bٔ2+BH5mBķ=qɡrMoCNB!5‹/mE \BB!J{i2ܺu!'!-66bccWW]BH WnHKTTA.E#R)1JB锸QnBHMW]B!U}=JBZ^b%#>ֺ5!`ߴn lJngai_B!## ܻWqB!.p ѐF ֪U7SƈB"P #hL%a|gT_^ux?m$SUXy79 j27Wj)]~z>JKm,((DE%B~H(6?;Ge)âlW9Jʽh|T[_-:`aqP( 9Zq*KV-Aw2C̜|4G/ſ+*~H~T{w/R]vz㗳c, 8?Wǁ kUj(N{$~˵&CK n{r^Gz % 8@ZmҝIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1714924254.0 qtconsole-5.5.2/docs/source/changelog.rst0000664000175000017500000002773700000000000021256 0ustar00carloscarlos00000000000000.. _changelog: Changes in Jupyter Qt console ============================= .. _5.5: 5.5 ~~~ 5.5.2 ----- `5.5.2 on GitHub `__ * Check if kernel manager is available in case the console is connected to an externally launched kernel. * Use string representation for a QKeySequence construction to avoid an error in PySide6. 5.5.1 ----- `5.5.1 on GitHub `__ * Fix error when getting code completions. 5.5.0 ----- `5.5.0 on GitHub `__ Changes +++++++ * Drop support for Python 3.7. * Remove ``ipython_genutils`` as a dependency. .. _5.4: 5.4 ~~~ 5.4.4 ----- `5.4.4 on GitHub `__ * Improve compatibility with PyQt6/PySide6. 5.4.3 ----- `5.4.3 on GitHub `__ * Add missing closed method to QtInProcessChannel. 5.4.2 ----- `5.4.2 on GitHub `__ * Check if the iopub channel is not closed before flushing it. * Fix kernel autorestart after it's killed for Jupyter-client 8+. 5.4.1 ----- `5.4.1 on GitHub `__ * Fix crash at startup with PySide6. * Cast images width and height to int when trying to insert them. 5.4.0 ----- `5.4.0 on GitHub `__ Additions +++++++++ * Add ConsoleWidget.gui_completion_height option to configure the maximum number of rows or height in pixels of completions when the ConsoleWidget.gui_completion option has values 'ncurses' or 'droplist', respectively. Changes +++++++ * Fix some errors with PySide6 6.4.0. * Fix mixed input and print statements on macOS. * Drop usage of disutils. .. _5.3: 5.3 ~~~ 5.3.2 ----- `5.3.2 on GitHub `__ * Fix syntax highlighting with multiline inputs. * Don't call processEvents when showing input prompts on Mac because it's not necessary. 5.3.1 ----- `5.3.1 on GitHub `__ * Fix segfault when performing code completion on Qt6. * Fix mixed input and print statements. * Fix switching syntax highlighting styles on PySide2 and PySide6. 5.3.0 ----- `5.3.0 on GitHub `__ Additions +++++++++ * Add support for PyQt6. Changes +++++++ * Don't show spurious blank lines when running input statements. * Fix showing Latex images with dark background colors. * Drop support for Python 3.6 .. _5.2: 5.2 ~~~ 5.2.2 ----- `5.2.2 on GitHub `__ * Fix implicit int to float conversion for Python 3.10 compatibility. * Fix building documentation in ReadTheDocs. 5.2.1 ----- `5.2.1 on GitHub `__ * Fix error when deleting CallTipWidget. * Another fix for the 'Erase in Line' ANSI code. 5.2.0 ----- `5.2.0 on GitHub `__ Changes +++++++ - Fix hidden execution requests. - Fix ANSI code for erase line. .. _5.1: 5.1 ~~~ 5.1.1 ----- `5.1.1 on GitHub `__ * Improve handling of different keyboard combinations. * Move cursor to the beginning of buffer if on the same line. 5.1.0 ----- `5.1.0 on GitHub `__ Additions +++++++++ - Two new keyboard shortcuts: Ctrl + Up/Down to go to the beginning/end of the buffer. Changes +++++++ - Monkeypatch RegexLexer only while in use by qtconsole. - Import Empty from queue module. .. _5.0: 5.0 ~~~ 5.0.3 ----- `5.0.3 on GitHub `__ * Emit kernel_restarted signal only after a kernel crash. 5.0.2 ----- `5.0.2 on GitHub `__ * Fix launching issue with Big Sur * Remove partial prompt on copy 5.0.1 ----- `5.0.1 on GitHub `__ * Add python_requires to setup.py for Python 3.6+ compatibility 5.0.0 ----- `5.0.0 on GitHub `__ Additions +++++++++ - Add option to set completion type while running. Changes +++++++ - Emit kernel_restarted after restarting kernel. - Drop support for Python 2.7 and 3.5. .. _4.7: 4.7 ~~~ .. _4.7.7: 4.7.7 ----- `4.7.7 on GitHub `__ * Change font width calculation to use horizontalAdvance .. _4.7.6: 4.7.6 ----- `4.7.6 on GitHub `__ * Replace qApp with QApplication.instance(). * Fix QFontMetrics.width deprecation. .. _4.7.5: 4.7.5 ----- `4.7.5 on GitHub `__ * Print input if there is no prompt. .. _4.7.4: 4.7.4 ----- `4.7.4 on GitHub `__ * Fix completion widget text for paths and files. * Make Qtconsole work on Python 3.8 and Windows. .. _4.7.3: 4.7.3 ----- `4.7.3 on GitHub `__ * Fix all misuses of QtGui. .. _4.7.2: 4.7.2 ----- `4.7.2 on GitHub `__ * Set updated prompt as previous prompt object in JupyterWidget. * Fix some Qt incorrect imports. .. _4.7.1: 4.7.1 ----- `4.7.1 on GitHub `__ * Remove common prefix from path completions. * Use QtWidgets instead of QtGui to create QMenu instances. 4.7.0 ----- `4.7.0 on GitHub `__ Additions +++++++++ - Use qtpy as the shim layer for Python Qt bindings and remove our own shim. Changes +++++++ - Remove code to expand tabs to spaces. - Skip history if it is the same as the input buffer. .. _4.6: 4.6 ~~~ 4.6.0 ----- `4.6.0 on GitHub `__ Additions +++++++++ - Add an option to configure scrollbar visibility. Changes +++++++ - Avoid introducing a new line when executing code. .. _4.5: 4.5 ~~~ .. _4.5.5: 4.5.5 ----- `4.5.5 on GitHub `__ * Set console to read only after input. * Allow text to be added before the prompt while autocompleting. * Scroll when adding text even when not executing. .. _4.5.4: 4.5.4 ----- `4.5.4 on GitHub `__ - Fix emoji highlighting. .. _4.5.3: 4.5.3 ----- `4.5.3 on GitHub `__ - Fix error when closing comms. - Fix prompt automatically scrolling down on execution. .. _4.5.2: 4.5.2 ----- `4.5.2 on GitHub `__ - Remove deprecation warnings in Python 3.8 - Improve positioning and content of completion widget. - Scroll down for output from remote commands. .. _4.5.1: 4.5.1 ----- `4.5.1 on GitHub `__ - Only use setuptools in setup.py to fix uploading tarballs to PyPI. 4.5.0 ----- `4.5.0 on GitHub `__ Additions +++++++++ - Add Comms to qtconsole. - Add kernel language name as an attribute of JupyterWidget. Changes +++++++ - Use new traitlets API with decorators. .. _4.4: 4.4 ~~~ .. _4.4.4: 4.4.4 ----- `4.4.4 on GitHub `__ - Prevent cursor from moving to the end of the line while debugging. .. _4.4.3: 4.4.3 ----- `4.4.3 on GitHub `__ - Fix complete statements check inside indented block for Python after the IPython 7 release. - Improve auto-scrolling during execution. .. _4.4.2: 4.4.2 ----- `4.4.2 on GitHub `__ - Fix incompatibility with PyQt5 5.11. .. _4.4.1: 4.4.1 ----- `4.4.1 on GitHub `__ - Fix setting width and height when displaying images with IPython's Image. - Avoid displaying errors when using Matplotlib to generate pngs from Latex. .. _4.4.0: 4.4.0 ----- `4.4.0 on GitHub `__ Additions +++++++++ - :kbd:`Control-D` enters an EOT character if kernel is executing and input is empty. - Implement block indent on multiline selection with :kbd:`Tab`. - Change the syntax highlighting style used in the console at any time. It can be done in the menu ``View > Syntax Style``. Changes +++++++ - Change :kbd:`Control-Shift-A` to select cell contents first. - Change default tab width to 4 spaces. - Enhance handling of input from other clients. - Don't block the console when the kernel is asked for completions. Fixes +++++ - Fix bug that make PySide2 a forbidden binding. - Fix IndexError when copying prompts. - Fix behavior of right arrow key. - Fix behavior of :kbd:`Control-Backspace` and :kbd:`Control-Del` .. _4.3: 4.3 ~~~ .. _4.3.1: 4.3.1 ----- `4.3.1 on GitHub `__ - Make %clear to delete previous output on Windows. - Fix SVG rendering. .. _4.3.0: 4.3.0 ----- `4.3 on GitHub `__ Additions +++++++++ - Add :kbd:`Shift-Tab` shortcut to unindent text - Add :kbd:`Control-R` shortcut to rename the current tab - Add :kbd:`Alt-R` shortcut to set the main window title - Add :kbd:`Command-Alt-Left` and :kbd:`Command-Alt-Right` shortcut to switch tabs on macOS - Add support for PySide2 - Add support for Python 3.5 - Add support for 24 bit ANSI color codes - Add option to create new tab connected to the existing kernel Changes +++++++ - Rename `ConsoleWidget.width/height` traits to `console_width/console_height` to avoid a name clash with the `QWidget` properties. Note: the name change could be, in rare cases if a name collision exists, a code-breaking change. - Change :kbd:`Tab` key behavior to always indent to the next increment of 4 spaces - Change :kbd:`Home` key behavior to alternate cursor between the beginning of text (ignoring leading spaces) and beginning of the line - Improve documentation of various options and clarified the docs in some places - Move documentation to ReadTheDocs Fixes +++++ - Fix automatic indentation of new lines that are inserted in the middle of a cell - Fix regression where prompt would never be shown for `--existing` consoles - Fix `python.exe -m qtconsole` on Windows - Fix showing error messages when running a script using `%run` - Fix `invalid cursor position` error and subsequent freezing of user input - Fix syntax coloring when attaching to non-IPython kernels - Fix printing when using QT5 - Fix :kbd:`Control-K` shortcut (delete until end of line) on macOS - Fix history browsing (:kbd:`Up`/:kbd:`Down` keys) when lines are longer than the terminal width - Fix saving HTML with inline PNG for Python 3 - Various internal bugfixes .. _4.2: 4.2 ~~~ `4.2 on GitHub `__ - various latex display fixes - improvements for embedding in Qt applications (use existing Qt API if one is already loaded) .. _4.1: 4.1 ~~~ .. _4.1.1: 4.1.1 ----- `4.1.1 on GitHub `__ - Set AppUserModelID for taskbar icon on Windows 7 and later .. _4.1.0: 4.1.0 ----- `4.1 on GitHub `__ - fix regressions in copy/paste, completion - fix issues with inprocess IPython kernel - fix ``jupyter qtconsole --generate-config`` .. _4.0: 4.0 ~~~ .. _4.0.1: 4.0.1 ----- - fix installation issues, including setuptools entrypoints for Windows - Qt5 fixes .. _4.0.0: 4.0.0 ----- `4.0 on GitHub `__ First release of the Qt console as a standalone package. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1714872940.0 qtconsole-5.5.2/docs/source/conf.py0000664000175000017500000002407000000000000020057 0ustar00carloscarlos00000000000000# Jupyter Qt Console documentation build configuration file, created by # sphinx-quickstart on Mon Apr 13 10:20:17 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # Check the version information release_info = {} exec(compile(open('../../qtconsole/_version.py').read(), '../../qtconsole/_version.py', 'exec'), release_info) import os if os.environ.get('READTHEDOCS', None) == 'True': print('On RTD, regen API') ns = {'__file__':'../autogen_config.py'} exec(compile(open('../autogen_config.py').read(), '../autogen_config.py', 'exec'), ns ) ns['main']() # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'IPython.sphinxext.ipython_console_highlighting', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Jupyter Qt Console' copyright = 'The Jupyter Development Team' author = 'The Jupyter Development Team' # numpydoc config TODO remove this block if really not being used #numpydoc_show_class_members = True #numpydoc_class_members_toctree = True # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version_info = release_info['version_info'] version = ".".join([ str(version_info[0]), str(version_info[1]) ]) # The full version, including alpha/beta/rc tags. release = ".".join([ str(version_info[0]), str(version_info[1]), str(version_info[2]) ]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. default_role = 'literal' # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. #todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". #html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'JupyterQtConsoledoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'JupyterQtConsole.tex', 'Jupyter Qt Console Documentation', 'Jupyter Development Team', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'jupyterqtconsole', 'Jupyter Qt Console Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'JupyterQtConsole', 'Jupyter Qt Console Documentation', author, 'JupyterQtConsole', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1714924509.6696565 qtconsole-5.5.2/docs/source/figs/0000775000175000017500000000000000000000000017505 5ustar00carloscarlos00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1450622414.0 qtconsole-5.5.2/docs/source/figs/besselj.png0000664000175000017500000015073600000000000021656 0ustar00carloscarlos00000000000000PNG  IHDR} xiCCPICC ProfilexՖgTɉ40drF2HC a 0D1++QdBVE\ k@D1(ŀ {sOU=_WWU;y"Q:*!zIJI2ME!!m@&oMǰ> +H !p _$@1Ƹ"1i><> yP!gh8%)9펝S6W77e[YXNnOߦg5!kY? r@{@z@~8o:~#AAt̰մ'pV!b`!!2@ VA1fUA8ǠNyנnQxDMD1Ax#H!IE"kR B"ȯ)PNO8IHE"',ўG!w> qD")LHΤ`C*&UΑICdYlE!ǒr!Yr?y"Cѣ8R)b&Jr2DR j*uHH}H}KѴiPZ m%vv6@D=v=[pc2r njRL)s)@jTTT+iBBrץ_Pde+Ӕ((?RRRU^IZzLfDmZژHRK F6#LMm4tv=姕WGk\@;R{v#G'QgNΨnRz=^.k[   Y7F4Fƨqqu$dI)ThZkznjnng`6`24_mbjY[fufaknQgRre+c+UMk V6&6 6l2mlv~5ڍsB89+N;|rsqG/-t/6^nB_t,Zj2e{#wYQbhʃVbuwk״,bbqNkw9u*}+\-(-/ &6&ndiff[\,-+,y{[ɶwmRnS{uGIE`Ekn/UU=jj|)ٿmWnݥ?IswZ}}yEu~_H쬷?vhSڐ0rx#^GZ6Jܣc9O蝨9'{z_oumwߵ nro^5Vw̿#+;|/y|HxXHQcǵ$~ ş~*zxZLYY/ռ2|u/Gxlu=~~CG?q>u}l< KWm=Șļ)/Z41F >퇧"i񤗟Ӟy*`_;@J@z}JL7@WL,ىVSŘ581VU<11sbku0oӞ5'e0G&0<#j?4y]2^ pHYs   IDATx`SUot.Ҳ({/ Ke*d@TD@Pe*2* d2 н&Gj&%I3;wKyq"@ D"@ D"@ D"@ D"@ D胀L׻w5jhKD"`:gϞ7ojFРAM4J2LKt"@H$~y(;իWsrr4 P< DԫWͻw^///a* DB !!$5r9M^^^JZzOʲW\%ʵۏb:׫yR$G鏞IUwk@Dņm)J-*#h*O+\UD1ՂV1a4B5 7/tyUi`fwNř)iYy7mP- X9F?je-qrqvB%R22_ k\7D8sZ̕$+%saKf Tj}(h'hDiFS5~KPwx١f_M7k$/^pGda!jXX,V͡(So8;VڲEA>4ƅK?%,֫%?œjmӬyӆyy.\z.\/*]}rPy8y򤇇FȲ>ݽqvvvEo03 PjYſKn^ޓ'ᠹz>666ХU ,'B I c !9J|=׳JDZH dU?M|UC1M`ƍ-Z[nZZ… ;uꔟ ۷U 2߿LJ^5$7=I@V(,,TK^G'ZիXDcCJȄ.A,<{ a|!1bĔ)S5jl4iԩS.]Yf믿BGhhURSSx㍁B-ڵ իWx-ecСJe9%!/@BNn~`5'{{[qQ|`L&b9.v}ߒ똋c!j0BE6yvz5''+$zWO_ us͋OLV B_ & hOv911q߾}Xbjժx=z̙3?>v$E742O7#RS3F[ο2Wc9.YNn^pM[tTr}?(=sJh 0~iOU%?/W)-VNheP+r Ц|W{ܻw] H =^~e6cGUvl۷oxMR3Rk@_$'Go*ܐoQ,a2PJzUuPyUrX'#~S\2tjZmwܴi o|r4HH>_-^{>ёcJWWWt)4+Ja"`|Ν;+eIU}tظ*6 ?3VʄO,535# Ȑy8&A{P"Q+h+(BD"`4Z<"@``Tc_;w(@ AĉZnDVВ."@8vfa.4}L `ncM()XjڤDc';`O_Lzy)boBH"`[c547ٿ"X❐V( "@k (fH+hCW Df-@?pNH+Bd K6'~m&8M {A% V"@LC-܆'$$`: -`&<4n6%^yh U"@#?gãbrdvNvnRYZ̳"gkŎDZ؟7Տ"z09t͇/WWg'Wg;xBIM{h Ki W~D"`^?p1YV#|YFlkyUtՌoBH"`0p.NNvֶ6PN48׭WA-C$-p D`ѝU✞ ?6V6\p^o%JDDD&gioѠn:5-,g(0;9:V~Uo L("`j,,jV*,Eb%V{ФT+ڳ"@KB$89b 1Zv &q%bnTl ʆV>#LΉ &G@l)bfN+NpSL􂈉%b++K,VХ@${Ta"@2+l* L^enU @OJt+VPKDS$hg 1 mmrKCk ӧ:"@ j3;i9C3(IRt񂴂2Y:'D"nؠhlA&c_$h/ՂB_XMV"@TCAȤrzt0!Tٔ R z$PP4 ;^2T&> Ir)tntDRD#' dP*㆚e0&)  q")"@q梊7  $ngے1k/TzuQQ)0E6pkK-ZT} u/dT6eĠIXH{5VA1Hvg;yT;t0֫)Ar#:v7v4O\n@i`8{asק9HD˗0'_~yӕ+q7ӧvZZ^LL#.VlSȔ,ilt/L`PV8p͙23Y~>[ n}_84/B*+_lr)>o5Z W#tG&IZJBBS2| NmoooUf@|]O:7m6Cqsl tP4Lvv(V8t萕U5BD@v;a Da<)A%jڃO N͚n6x{; aLN΁v=E[qUBL5ole%sΖZ[K,4? PEBBCwO%:-q9\Dr7vrc緞s,-ǯ|iGf66Zva\~^ښۧD&eob3q#;~1GG.~Ӧk0oo6䄫Ve1ajpٖ͂-Br6r@eٴim\YҒ3Y0/UI >͆Ć ᢇj%Q:PDF-R=|t-sXEMa4Zhɹ-t r_螜>𱧽A[y?n5?<.5DP["~ldk޼Xzf=Yݺ MpPֱ#bO<$eY10x0в +\ݽڵ{P)\>5Պr4Dh(҅+u@ cFfs׽;9]H:'2;;v@'4Hp:(ի Z}1#~Oҟyp啎֎ uMMA};{ U#ݧw'b>dC ]dcShg'f'v%97yL1<;G{/OKCUH <޽{=zt~F;dvF(+VrG,0n$Þ$Xu̸臫fȔ}.ቚe]@k7܂M.1cvgd80Hm |[^ qkt͒ w&SD2ٸ_5A3, s`v-^4n<^!:v]=:|"W_-23f0L-C3 C ?.2u*eN X1@U.M~4_̓Gpo(`9B}2דKNEA޽lZn0`>vkݚsxPѯE& BѨ9pa*VFN owqcz~:_70QⷒnظlŧN=2uͼ<"{yݵu94׶ %}VOڰaرĉ+_AHs`!cK%vLշEE*R1a|5?Wu\7.nJVm-:{͛à |AzUrrCvج2^ZIWו}WҚWyN^AwP/i6.i `)*}=zp#hkޯ> 2fN%hܘ(xF$Ї%~9D@}7zv>S 8ڴ5 !9q*#)=}:tT ͪ}{vU͞],|*%N]?4zϙpP-:+0I񇴌o{!,I# 0@ ]nAÞ{!LC >Q{?"PVP 㻎/fv-CiӦ<@04`h”? DMU%D'ۿ D"`+`TT L@'Dpx E[B.H"`}Ra@*3R F}Dbyy+P_Ԟ<՗"bm%/_ R, DW+;- D`7In3%s`~"`Pf_(s"@yx1̛) Dt ˿A2՜" np17U_XP /,J u<i `("`Z_i=v- D@=}W /"@LW0F%DW0$]ʛ"`jȿ=1/ Dȿ!RDS#@LQ} $@ I&D`jOK0,Sp}OObQ`pUm׮ ZK©Ο?Ԯ]GbWmmm3,Tinnn:&?}` ҥDBB$^->&)3glLLy,̞}\"veի`i9.&+;n %teS}g~WQf]vu(g7nl׮]֭/Vwޝ6mܹs? .BBPܸq6lnݺvZ_|j*E (L* }W(VH 4| NmoooUf|]O:7m6Փ'Rl̎VVb;;[nvI^$%e)cT۷>m۶U!--AĠ7vhѧѸ^zҤI5k֌\]]׹ӧO9% K(kkСCVV;v,**/ԩSG{t++XsYA!&y:xvafM7[[KWWaLN΁v=Etsk MsvZbgg)\6Ì'!^m 44. `ɾ}^UD׫WLDJl۶MQ@a4a8zˌP!Æ KNN>|'ZI$^[jeoo0h > vyȑ6m޽[ӧOoܸqÆ QÇKx7޽3⑜˛2e re?2㏳g8P0ѠWTв{xxqh ڵzꊖ r%@P4Ԍ3p#]vݴiR)LHHaJ))0{F_c&[Xg֛۶,Yru_ll1"7G7g8[DttzxxR@'@r޼y<@Tb |1FwgW/bxŊ[nTg}>ׯ_ߡCh &V#FHII%ٷoϜ9rrr#^ɯJn:C" +DE_XczY0)`֭Ӓ%K`PBn @ɓ'BeSNT0<_t$M|##S ZBBn'j< V46-\~ 7o&̛wj׮;xk܆ rŭqc/MB<#;uTZoŖ;(ܹsJM4Q, &}ҥ_4r4x)ޗ<M3gS;:W^vǺyYflo3{B[n-[T^@zz^Ύ?dOCƍVGG1zhTx7aJ1p*Uѩ*ќDl palW7%+kz۶~ݽ==o]2kpu+iݺ%߬2(zwj% Z@ +hδ$)%SZp! vA;FS"q %&.yEڛRSS~O̜+觯fa8"VK$4iVWM7 HxE lcƌK2A(SY+aeBL瘹[-FkgT48ZG+v$՞гg5jċ!DM-uL0ÞH &\U @DŽ!%Xp`2?OjѡJM)ɨ=Em1K%$fO@_Ɔr0mi) &xQf~_~;vL0 2h4aN j ;ǀ0ob&_? bA-ZbSR3S ˨ LgV@by.^Xi 4Kj7V@1:Sf1pIa"P͛Ξ#/< FqMcg~{&/gOn>}B0E+ȑ#QEaXp%֐T:0kR);03Fk1;ombPRRO?Js y)Kep`8 hֱ7BP|ha1 10);wb1!B#aAS )EaJH PmB$A@_t [Z~uC"k[K,^|}dk;/е+{jZumAA˳[ jݚt)fch/}0agVTevC'NDAW*xRWCm:D4z.^Q#ɓ(]G55VvH g<92fӴSQ]w_oW{ T㕶i`/p>ZjfNaR{R&#(T TR <x rf&ba?fbZșDl!eKtЭ[ L(*(u -đ[:&4'19wuj8Ѝ@IJ#ŏ?8Pb%V0 JH+Mǧ2BKmTA}0 D ˿i#T="@N˿NU&!"@0=WL8@ZD0 `@X5 D ˿̝' D `g+Ȋe@$P < $(@76r:fGbD"BS_ץbtD2Dc'`.Տ"`jȿ=1/ DȿaRDS"@LiQ] &@ M'D`JOJ04`h”? DSzZTW"@ ˿‹),,]˟FE%Ţ@͕|vkK-ZT} u/dT6eDSӧO{yyծ][b|K.ɋ*VDɿB~ۯMJuOΜoِOdǿLch%'Lh I*.;62--/&& j%ͲW+|-[\x1_asXT͛wȑ#FZ… ;wģb֭aÇ0`ٳcDh'/ !!z zMJ{-\Ez* tn >>[~ذFVrOhٲޙ?[нHJŖ d,hJ)eH^bCYY֛ +1K#2?\re*UZtԩS;v,**/l۶ԩcwE"&AWafM7\]mvÇ 399po?w1|xc++svZbggQ},-Ů{׆JО}hh\@}i܂ܰCQ>>HS]hk17,hQq#6^XK)3f@׵kM6i.v4da?Xive+;9Qx{#v\yS xYKo쎻hеvd'|iKK"dɯБ ^6mƩ"<ȇoժUsTBNNo@%@Gȿf͚!!!: طo I"@З=iŋ1nvhnm[kdɹ֭gLjDϤRYNs瞬WoEVk뮦B<'ÓMѧnrq91ݺv|+4)}cYmG>B&Rjcoޟ1ǚ2Gg@ޤ{Y ZO6W[Uͩ˫1"ga g'RJVvEj*:cm0\ʲJ2om6dۤV|||+7[op:TE5jB;nSgvS .zC#Zf).%gc||ߠFM6)))ٳg30¼~z%t,\ʕ+SĠ'B"`N˿…yְʕ}deMooѢw߱`9..snׯOxq\a5\4 \  cΝ;͚o+v~ۚ2cȊUy䲴rfbVSBLn B[Ѝ clO|K/ql%!|nջaa𨔨'Ӣg$b,+GanRrrX\1YfpCa"@0. nnોhʗphr@xРz[4n.4TB%]B(4PHUv𖈸ħp]d0 P ̂ KȶcU^ qWvZa<| r|L`e*M#FZ^Ǐa!NUժUcbb>HIDN+L7_tTZɍrVγ;wީ[[H?j?tؒzBVhY0'f_9Pg*kݺe˔"a5/6a!;;AQ #wErH "P*f_a氳g9[D~~5 84lXBMO'cJqd`!IIzs+bfm]Y@&Wbؿ%ekuA2~ۆl6FvF4/i@8Y? a4wQ`lAA@cE@X8N8ƍ~>}.^(PC(7`BǠ3sIɋ}t[ />קOݻ1mbxkk1&A/:`K{_zZ9.]ٿ?ckt_g~n0=)%p;5 ?k֬9s|7:f>`k2]Ify|՚VUln%Ғ01o_c{ڋɬ nH|&M44hСC+XҥK 322&L8`4n8,d<#K.߿cǎRCD@@_ aÔlcʋX&ѣQ ڲFJJ㱜-,,^VYȑ;L9X#JO>88qmxL9սzFfѩBGS>Ej5ŠHK{LIwԡCfPD JRx:%D@^{:rR뷣ܸG^p#'ϲRs yn?Dr4-y @^ IDAT Bѣw`rdnbL׮5Q57Kv}̙:g>M Jq%{9<Ǩ2X7Pؑn&Ī[s[ك.Q{믿 EiJ1>P'*) a6#lTS^YњB_NH1bhnj8:Z أ%.,FnE.@N^]wtɧZj%AP]ߠK$C*ɟؔ3 9a0bt|t-}pC)[^9)I{ZR U"P*_TH"`_A׹Jfn"`_D+V D, fA):0__)Ө \ߔ>J»nإ }VxƍڵnX#V@?kuV[(V/厎9={o߾WXh#h(@`{ _Lp msxw<(88O7oތ}7=gA>=zp)R9"UͿEX ?^}xI H/P`l{L~ۤXæ 4&$pL>ho޼$?χ0 {!jɓ'H!Ok;65jT׮]{8`ZJl+nݺm6B_*Gu.[ZZڵkנQm.رcQp_TN> bTX,XS%ӂ~kgw^9`2񢅭^:ނ^mnݺvj޼9>ctX$b+E=x`;w g%ؾ}G-ADBz#\:1!!!j Φ8w=QXh#A i36[U1^=/H84  W㳄1# f:zo0Ǡqǫ4J±R 1zN 9FK!BۼPKVDXSJHM?>B&OUl\7cAKFƎ Nٔ&gРmyy'TOŽܰ 3m05VS:u `85k0fC/K(XH+ׯ_~ӦM3g΄/_/ ‹iP-Ɍ Nȹ 'BoQ0q )ETS$FEX9+Vʇ;|'q1e#?cǝ'OҕI&ңGYCTϯ<7. yx\n]ڵ٨|<2/A s0 I,@7ܸqC`LOD &#!R5Z ⎰LOPJhh(E!>r޽ԄqJ6m®]pⅲ(@*3}Wo\g䤤,b5)>##/<:X[.㜳i'БiA D ˿i~T9"@_I"`ȿ1? D  GN"@W0gC5#DT<P̩D"@ lfD'@*9H0^_x Ռ"PȿB3 L+ӡ"@*WXT D ~>T;"@@ ˛J#D7`χjGX_bySiD&@P L+T0p*"`ȿ1? D  GN"@W0gC5#DT<P̩D"@ lfD'@*9H0^_x Ռ"PȿB3 L+ӡ"@*WXT D ~>T;"@@ ˛J#D7`χjGX_bySiD&@P L@O$\m?pE*|KKK* ish!&˥RTPPoX,Buӧ=ZL%/ <<<۰wN:9::I}loIeP gٳ+WSLE^ SNE{S@%' }Ç=zpww718`BňA_xD"qqqiݺ:W%gg,$*PPLn䅅2 ʓן2hxlTI"@˿hkkk.[x*[ZCzW mhA*UhKÂTj)?vEr^*db*0R$(.%b]QUS|Ȑ!Ͱ5iҤ979G ORkZ*R۶m+DPVU޿Y2f)HD"Lv.3Bt{ DȿBi< D s~toD gF) J+듥"DW( 5JC0W_\, D,ȿBYQ"@ d龈 e!@B"@̖+fЍ"P˿iJ%D| ɿiН"PWȿBeztD _ S4 DWrW0U2̤U"`(S@x]6:lٲ͛،?^ ̙3F{ s&1'DT`tZ!00p޼yC8}ҥKn߾=bĈcVNwJD /--1 h!NB)RiW^yW^...JNEb&Nx̙X* ~+V(qFDDpċ-ǽ{p JLMD$P__sf8gd1]Ga%&'''Bmmm cGػ5hWg_}5K N .."::oח NNN}Ȑ!CӁYDkՊ!8"@0[ hMbjj*S}Ev[lc))F\ZDF We΀"@W ;ŋPA f_oeK{Vl* "`* [\Y1Q)D PY+-6ҶbG/<)"@^8}W\s[ݺid8 "@L+T^'HاASP +J_A@P-[+,,f DE0[  O z&`Ig"IR$BЗJ=?_xjFǎӦ"@J$@JDvH.A0]_An0v&zUyR^D %@ښ} D%/ mt F ^]D$I0Wq#_O"/.g6"bsif)G"@ Z VΔ# DȿA)|"@i y^vغuɜ2%DW0 ݩS~yK"`0_PhW`G*ʗ"`_T7]k)k"@ #}oÇӧc(D0r_+^}[@ DT>1cHraK܉ "@ER}>;Ϋ3D @ 0?z491eN> }TרQYvڋI02_חmPD=З۬akD޿`A \coHCO"@E+苤|Y6Dt#!@*A`† b e#@ƭԩ?;x ) D" mWq$"@H+\aGɟsQ"@*W8,J %@JK\C[˕%&DW0(^{d7o'O"`$ȿB>kkֿ?۾B ˆ  ҏ$Ñr!D0ȿajεK!_Djn]g9 Q, Dp_AbY <sߙL.;е{Ey3ͼxʡgF^ο"D+V(BLF_[NwZZŮ ɣӢNGmfcgue),EI$J(-PZboߞ%'pv1b'O~kPP (łY8 O~%K^J*("@&_AƋD ~oBO3-Ez{{kCfS&-t"P_<ʞַ6ꘋBq8Pb}T ,:5sё;WB;رmʘ%#D!`ݻWN]vDD꽯]cƌzIK{os㳅*ݓt9JY"P ˿qbFxhS>0bĈO>~pwwLLVܹ__w:$֩)!(^'OnݔaJkئl$urr.[6dbJ@Gu㍰qoVlu2h39bXA!/,1Gr#ָqpLEqrcΈR4$1E<j>l_EMךNO))l26aܙ0__l*+͍!>NӿF+0ZjUfff߾bǏ-X~;wv?k( {&l<{cD|NVA LIZw\)iilV6) pLw"W@IǏFYVr̝8*"@:B̽)~.e\w^u,^I v9qD???Xggg )ϟSSSa޼ycƌQD/#e|hؙ3Cy{KU}W}y-\Π/\`zz^>8%oW-ZF`UۤD+VZj>|X _駟;N94#E%sl ,?׎oi降r/z5=M֮e+WS J@Ok\[w'&>0"A+-SYb"7`}@zv׵<֭٥Klnb42e$0k@ZAkyGqqLyrU.q:kՊk͏jN_= X&Mbaa ֧ˤMJK䉀Y xf|H'oD*KMу|/-7ms4u0@IryhFƸwϟvEG/%.n߳gʔ= z<7b+u^y5#pC8b'{@a$lo\)vHCf)T _~7nT9sfۉRisGGgJ"I*}.=Y\SRNDX˿NorT&vG:Q4WrMN#Gx9L=ZTWt>Ld~*so{ Ets{{]:VV23)'OFݹ}R|)M,.W IDATx"@JOؿ{ʼn-EbLX"us뮮w]<*)F7"0V ;zY4+6~mJ*\u)cL׽{G7>0Pc'6/ϧO'޻lo2(_C=Έxx rf,w((.gBkbui2S3t5"bl&M*Eظ|X$_& AЅ '%mnP||{ >ŕ*iժS˗֤-,ɕ'D<,/xY$tЗ+?V€Ӳj>-%UFFp2p k8Lc*dduzc3ٳTz'EEXD3xx{7$!aM: yɂ)R'<+(([H$bgqN>]xWdF[q+/RJ Bq/- qwL$FJ}ٙgsg3+縩TU,[[xoF(V$-4N`Jc`IHjV xbأj%˂-Cëe͟8N('IJz9 V ~4c;%GnnG> X\bbjC4:(+Xr;:<8$$BV̗122 f걔~)iNH@s X`7CJV׃h^Y'F+j/&PN_,!QAm<=}uw#"P筧 VPY["Ν#WX$ʻmSȌIkU4Ra5J(x^/9ac}%$, {~()zee:$YF+٥ٰ.u5z (enE҆öp_M2uT ԍ33^alؘUtNf`HQWUq.#iG*1EGUrUA,OIzuOyBy)5L֙^ž>ipuXATNn}9ؤ j44qn 9H&5 7 aaM[QF쪪!!%TI/H$G-K'Ufۑökzsu#hNx^wQ2Wz5܊M5Xf fqP[[0;o2N1e | mDz4yJ92 ;@<(L$n26>YF[Tii7??Xe.98U  - 4bfDp,\pv#lE?B$^YYm38\L`$<"--9`)G`tz>>~}uuZ>d H_Weyu!C˺.k݁!0"jP S9=cwO z|cl‚LP t'7 iG||dduQܼ积76ɹ880^.ag44HI,2xy>w =VSWQuRZ2.)Z+ >μ>bqWPA2u^N^qqg7d\)ƆeݼO)M#JJ+! w@4{0Thŏfax%)5vu{[7uuuhf ~c:֊OuC0+%01ٝts{XP!50P#ie1$+MݗDW'li=b#auvZRƌ!zl8O{ w䝉1H? ԃ((hHL2 -^^zz3`92Lgj:hHP|uTVvtrx9=x` 6a*0$+>{L[QC{$~"[2YE7lC|jil:E"K[x%j|v{ss ͉h TTDD wp@޳XmCLJf_6)?뙛{[`=#U B ,6 _v><޽gF. Fm]Aeݪ|+6}YKhOo'H<}<SD]FPB¦o]SIO.]FLߊtÅ+kk+*0 |`Zp##}Ԣ^80P52JAdjsО`Ĕ˶m]u]ll2ޞ\r,V`+R ]X: " "*R+<|< e F/Ԕŭc!$ 3wvFW>*;*|3+t ϶ʍ`Gسned rPѷ nEߒcɍgȞ=JJU qGWH`1CILeFHWEZdګn)F3Dm69J}xp{o8Oŕ C9&g 1vvģG ߺrr즇xyi!`xQlW,rg$8&Lߴq׮#"#B 9WNWH}o|ݫʩGފښ/~55Dj*篿2KNpIMc[B!Wɯ"Xb`Ski ^_7=2О'_QGf0Á 8G}koVJ`9^GH 1Exp0EVxqРlftz K̓& G5n$.]m9*V3R€_r;C4A>!C@B!VEs jm\[08bD~3#)"j!o'?5 L$K)4U+`DIN=>7{1&L )'w;Pt&>T``ڍ0L 8 T>i:PgV^݃'iJz[z*zM-iAs ?oSNbةs|=*KKP>:ԩAV YU_ AX~JWv?˪V_ * @N]xET9-+(v[TN祦c$p@*6f T h.]%KH_%XtЈBѴ F/_v󻜕Ί~^^kMLji׫"ߢ_oMLLF>**Fv-CYl#9#-#D` "` [?C)ӧJZϟ' _ y|hkf hyաBCF]'aÈ-[HkR}iAt\\; 3JJԕW񖃽pSCHIKٝM?7CxbEݪBCZYAO='0_ml~|YX( CaV"S Ά?[~]yQc8|/YDgNŽ^%j♄|ߤ1Ɣop/N _sJ Lde;/=VVzRHS?혝⨨i`[W}$Zwn/=7Qc\jۏ H>wcl&N34ܛgKVW QR:fk7/ $RMvL'j,M+q0ᖧ?X#FD9H'ȈyIgN;ɷb}q I['aM:U.lV[QN~M%X@qU.>Y+zz"L Few<2!zܘJPLW `$AoQef͢QWp,9hv:Wu rTV>lc#2&G_!gs5dY^wh+tfPn~x_hQ`݁eo;߿ZRiLUE0Ϟ= [\--;9mn̋@%~]c`l P̱$$+З"Ӓ2=[|X9n΅Qў{JQxkVU億|#Ǥ5cxwo,)0N 4#h.:{-ܲ܁5w^ u)%!!c$̆Ͻzuxy"ML!%%zzֽz_B{nmlEM퉼݂i2^ۙ7Is6`D3yWU$b5@}`-&cF%5-%u3M!-eeQxO᯹ ʶ*`JDfQ"D:>="E0J^ۤ;ipr`2i&TVJzlƅqjj Yp%+룀-T(bt*<6[ӌH8~ +|!df ' "ڄ0SV~<[==ig|קbQ7D٢:lQQN[ZlVvs#\BT뜫}β`(0#d_Qz_udA1Ո7@=d7[%R}v(; NBA m.pƩW|5!X0p̀jE| TRÑEjIc]UrDŻ>p՝70CZB=Sxϯ MtGZ"+dy`ɰkt۱9s炝H9`m }`bBFUt2_X7 `ZSS_M#K;o=5hՆ=/4ՠ}{YZZ;\@{ Q}Iϸö ]/;cR- ZkM֘ k/iW3MӬFHX+򯳟21 o5rrjNOPwUҫgcy BJs^DG?˃cٓs%}KK#Z*+FZQ=Kw+"#yw~ Z^`hI|y Ъ[ x9yY-o5gڣ`BqqB__69>u4./7͗&1ӀAKO}cc|n45E-U$^bEB;wDD\?`O_$Th+J|g peGޫ4@|cb(/VȤA>Ļw nmz?uj6nni#:Ou:rL~ZA+J@R ^B^&Ԅ<}R BV^I]xx. [)˽,9c?۲/FF X^^P&$kH ɕ$T\olZXLF$0)䬃{_:`͛WU  ))*#NX2|ozb@h$E@VՎꄄov$KNfqv||cӨ efpS᭰hbEj/xVI{avni]EZPvi 4Ϝ!XXvI䏔4[i%䖏*sY+85D*G2׉UrfVZ `8"`TTF-8ǫߛSc q$d__ϼ/63@7ЁI );j"eYo+B9C=_5ظB2{zq8:<2Kˉ̸hƪ TD!ߘ pDC1 kJ1!g4W Ί EVKʉ6溨_" V@٠"33mb`GEE妳s>Ut6-: Ћ5 d13,lw!zUU##g^BhW Xq6v}v0,ThHtDpA%GIr&?!?yAtzyyd->\QᓐrQLU\S V5DeMShV+V˖ kZD"h+rrl05ei jiI"D 5 8C6H>v < 7M^;\Gg"' ׈B> O 8M4|}}54tmB"_1^i d+w}ݍڗ o:sf/mUproAu-r%I#:bd'lg/ p71 "𕆂_:>#]s4T@2Re!맡`tS9&N6[g 3zl,# EE}JQ]6"!=-h^m]"tٹyB>u3ffG؅}oVUe{y/ܫ,z[;NZ0c4Aq\#p.iOj_}SJUureHoxP^[8`ߊPz|+:ٻ7:r$мN uz+?a e"cg b nY)Iaa|\~*Yfd>Jx4q!`cј#'"QQ!&TmUVs|k9 #R'lz Mg#4\݀LˍGvmS # "iK`/rt3T0}bgtTKW1XGۍVSy <E7|Q]WW$py `m>"TeUE- ?O;*XSSzDdN(+; m%Z6  Mq `‚ 6/1^9z7]OF+N-,^}%6@V!2ah3VRcgv͌{k$J E4pX{t󫑗`{w3 xa0u715Eï HX0(#HK/1$򁁁^yx,64$C( W$RUvU7vo]?W6/"*!aWfyy.=\mm >HdzcG4 + Dȿ@NR^8̫GE56_N`.la1 ׋$ UKwh sؔ@D}-(&m]^6dlF_rZSL*ï P}LxxiMI 34 HĘp)ǭ{cQFؿs $8:s**ݟ89N:2'Rh"WI$j ~<iAEO Y\ov,zv4˧֮%.^1 T&@%ljQBf錌XSZyt9r]*AGs+! ;j6FL6ï H'N" U8H AmPF(l0_ wg3A~ͯ6N7C]Ȩ{{SRjBb}fҤ}3`K@¯uQ=J`Am# np˘e1\`44EJƪ͚mMäONN2^F"wAwҊpU7U|- ?x%Ŋp'sR`i ؙ:ƳH[7ҽLxYA\x ~Iδ@{ι5>1՞~6lmAWyy6u떈"dhP:Nݧ8WZTD-|9ؠpC`om8:AjU☼mgӦ)\JHTtkN\]ι>:},uU"ӎe>"v4dҡ*&P䨠%dBlIL݌gl !_g,.f^^;wHH؜U†V~WgH55%W#"fx"ee.]t<\J0ЕcTzhlЈdpxR7g_͋|DZQD*<@Ӝ@qs7a?UUQppOˢBC[ ?Ώ[|Yv.$S} 0vR\l- | 5_kkk |4}@WT~O`aɷ'ӱ?zd.uu阌ʢ˒& q5'0"70gpK]YY\j RR~QQq|"crr˻/?sT(^e(@v7X5`6/V>E7Bâf^fE΋lܴkp#ʾm񎤤f|?~H<NчzH<h$v𹎐`mDYmGiee negg7l0 w`SxX{~r l0yC .p "F/_qqJ_Q@\؀دcF_Rb̨{rw8M.@oQQ_rO^0 yݻ'mdLAcdt?T)5ũe3r 6?V:z7-(En_r5 !H:/GX/8脺ɣ}TΌ=IjQ*uWkkBF2gϒwSF(PKoџSSq ޾}uV(33۷Ϛ5 gH@2oٲ%==ic_HخceBKo#ڕ[r<Ͽ9zz!T3uDY|(_+(v; 3E&ƍY!22aO(L;uꥥ5 L &T4=F|c UR I pN6zT.208"Ƹ {_9O^<`IaiMB0-i k1w xV~~~ԺuX_ ౙ~θ~oG `o[TqWAvB@@H+:Ȯ4a6ж`+='ͪk B .]1bThѢ5kYEddd<~,8Ǝ&lbR7ohKq.nKQ|yypI pf +*zUW?coﭪJ8 R~b)+ .VV뢦jLSUSL/g]S]JXDHz}dz6X~O;7X&f33Gi@VT+a?SQڝ{Vs&(¦sfuX_<:8- w[MMh\ dS}iCUh#GvӧL}!+y@vxuy>~2hkn->v%X#$¯бEH$m(8Հ ňA ѹx'nM ;_$F Qbe ӽ/4閛Snm#R=E6O G(*@rp*^>+v϶iفFXg54\'UT̉-h6b Xfs0TVH\upt񽹦x^?tQlJ`;8oo\'7bQ_n|+7§Ÿ"̮/hXK "~M$>]qnl4t[?jc[خ`6@DK_o~yM-)t'*;+ǭ"£a(4"@)ѐoo.<|FEER>toӰlc?m߫N(|rX-)4;(k6l< IIٟ{ 6:a>>L~ӧ?}ƍ^zp-@C*TUU'OT>|x֭X 'xoBJVd]7pcIJ,YTE#66H_`_pፅ?c5nRYѝ޽dMg2`d<4t|mrPeV De<-\f!\)RYsKwv YY樽%a+\|/}"TTT= }/lK9s ,F[1;lFpfQ9c);u4zH}}5l."ZX"L," 4`.IAfd &&+[~8mgּO*ECB~ \'3el=+P!i();ddPtJE숈sڱƖU7 @eU'XXtᎡȽ0&ă/PQ{k9`xxB]vBTNT ,rV(¶n8 ;[EhN==|9OIBhxBا,ffď?iꎎcb䜲y)K ,7À{}۠бȨEP䩪d_ 40C6I HH׏~II=Ҭ H[egcU ؀^8Pվ}%9:SyY<'=}%]RR;yCrL yo ~TTܬ~ 6ݜ5  Ȩ#`x>ߦ] ;MWSm]2ICك 膀A/C abo'ïϠ3doE/L$-d7qkiEdTU['x>`rIa|y5Dܭ?.՝u4(<򎧧#7;_~acnxzl%Ag>%4dA lh-!bb֖Y[h8TAeSHWο撒1f (. LXv%z%B'錆W1[[3AUjO>Ӗ8mQ 10<# Ns-,jL (ypát9h"cq<FԼ$wXFKanNs߽\ZP*+ÎsFWi_<gU)CxVC/]J.4ꉻRJ Gks~3}ş0(2I%i'86kXlB#vwв?!YۚlzƇ-72*{+"m;l. r K.''&t<"#C@.B[ICWh+vq`Gˋ> ,^RC~-1oK` *nIVUGWXan.K+J;\p[W^J Vae6{p厷yys\k 'NYmy%LpE[AbEؐ*b|r/ǜ"y$ U}'|.#nnI;;l%R dl%xd6s"lA>d RT䃴m'J ~dmKm/I+< IDAT ŔvOmjm˴mO% x/"t%RXQ3b[L5595ޫ'ZPI   Вe11W;!ArEENqiN랆1V!M*0@!(MP}(nۚl~i)IdX`dáT rM \i0WLDN۠ ۅ\xHqf$}W]A _}lX6 ޝ@pR ҵ F61(sgED` 93VqUPDʌDմ-Qܜ86kI bEܨQQ0Q`7ޚ^k^ηhHET)]P -@F>DkSD"7|[J@NdG@=Bmm53F55F+PI-av>g ] "00 jvhAyL^ ffy?#QYg@%41(Bwn觡-]'/]BT} ˻SF[3G]>:޼;$b -A:/(h@ d;RJA1 7~aØpUAlR 'W֣ͨJ JʨhjU肷m5Lq \ݔ3AI٬"79gJܐ>8w/%v~H }gfde蛿p ydxS9;w3n*Vtd|[[(*foihhF 0a 7ؗFShE>|--pt#}ϞSۅkMu,,xX/֚C4ÎaǠO| ueh&`*AAC:uieUOK݀KpiGKJ8k1m ؅?x.%#*>%ŹyYYq3b:F+QU`mw޼y崉*Kxy9MN!!E,AAXG DՑq~o]n<+['b'좦a tبWP $")|={uS}1:^.dު]@ YYyEŹEEyEyEyyҵ. M+d ;zfwSM'NOjC\+>nԥ lN >({X4:H% ,e]d^LܚHY➤/9:th2@ƀۓ $,uȿt]T#uq6[I;Dv5xx*]/vͼ6KCAXfJJ~33{vVVJMJ--+~SWYhgffB# mhmS~U dLd6  Uvɡ`x~gƜqD ~Y&kMLךRT㼄H`[;%`a<y N]R%X"'C~~fHIrAdj0m%7rfl^]WQY|KH>|QT_E4ղ:=:??A%|ib؟ >'] ZK@:O\~:$hX^M(+3xlKάZUץK]qq>+i2i_EE^Vܩխۼ5߼q,+.F3g*ɚ+묬"#."nlY)*CbDg>>.*rT^]nZn,G;r7nHϟy?((;wܿŋ.\PP;Bm1{ %Hp~}(xzeP iDR|4]BG +=i0ahK㑚tT7 $}#GWۍ[պj"HRM F ڥxݺjj,-jHyͯhrxxw55//-``{dIV NUQ3<.@i΍a?[Pl҆R,>XH߲ BPP0Ayy %`ZޥPLH`B 3fHB-xvsѱa;~B;MQ%i A*#?gF$%V}!V׵J;*{vЌq]x%93Ds# *pUL 0Z\@&DhFj),$ўE,AB N'?9}#B<lEy2ҬIbΝ#6o&cRʴ ԃ2~nT큘u`¿))DSRkV*xNճgC% ߆m~d@74W_G'\cG`' AP|I304~<= X`#7r9|jVG S 0@KWLX9ƈiV@s3;R[oLQvaC1΍~;SƮ` Hـ} r^;pHH.iq|怑YV!Oxp7` ɫݚzZĵYf0k.ܙ;tjn\e M"ƶ${S 0h 0{6} ҷ E($QY9/1vc—[jXp*FUmߚ[;X/ߟ~.(zg.1`$j` &ITP XL|*f`.>_aSI~̥O,5-'u6ՈWïe `6#xF2: Tg[ci@բ"2cp$賵i'<D)j:)tm;zu,S-<#FVV谏jb[1%Hޑ$J sP?~i%Y2{f 0hK 0~ƂC75C1@ܵ+@ʺJ/&#)F+t"|V**$'O.$;H`Bb`lJit2#FY#?!sCM$;PdU?RZJ̛GzEia$H?$fs_$k&EUA!uV/F%PȊH?+f!]`;228%D+ H(+d! 07J ƧFk=O&S#F`͍` 0`$H`%`$H#& 0ZI#F 0Z0`$H@$ 我#F:VHOOOC=S 0`$Hm%Biiýt2dȐ.pHe #F HHN+ܹ0...>>jqyJMMM~~~y}pLa$H# Ii+W̛7ONN ^tJa~VbN2`$HpZ!::֖}666M' La$H# I/nݺ͋իqf!***㪟+ h2lذYYYTxQc763 %}/@-˔V@;h{QUU-|}$&P8{C{xxL~e)+昑#}}o~0oBP9 !TwqqLw46.9H#F"I>}!)11q׮]L 0Z|]1`$H@< |8Z!'hu5Fɛ9(^?j̇B׮]Ɨ6b8TVVRT`.DVC}Qbui;wZ\mP8999KƃR Z ̥H -Ӗ#Fpb?cb نAAA:ud#3G2;ʀ2(ܖwPٶ6 @QE:ɓ'@jt&9pt:`;pp_|(.#ya2&û@1綼l4h 0`$W}:||a,X;֫W>r%@PTo̜9X1d!&xuGG}^ ÿq`` ^@S|FjU۷׏ZݻM;w/Zj :sÇX+ @63lǍ3sӾ󁈠|}}FjNҥK7ni45kC0[6>(++fj[={?Bbxm(0 /niG0`׈X~G.w>> 2// LdD" K1! ^JC+@%%%^^^6<%%̌%,1mᨇ{. ڪUJ]:֠ƷǴ4/98f֮a_lƍl־NV@>Hx؏MCC?vjalǯTHHXO86kG ӦMDKK L[li ;*}fW9YYY|nG!P7r IjU)*!W}L ahlUxA"(a a.G2Ԋt1# >ft:9@)ˆ\fldP MOCIJfe)m\H$Np2@:Q~L&dZE'qaj5CaXplm̥T*ܸllTy Cs l6A1/ЬʃF,bъ06 ,K&jbKpx2|x_]@7Iq /=|>B1ɻx. %;iO_SX)|vyn^DI~pkKG;ư1iNTYv#ND0t7 C^!uPԡB Zz \!@@^!" kB@ P$/iɍWCB@DWd_ _!- E;B B5 ! E@^!کI!- N. EMB@hO>mˠIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1450622414.0 qtconsole-5.5.2/docs/source/figs/colors_dark.png0000664000175000017500000016740000000000000022525 0ustar00carloscarlos00000000000000PNG  IHDRs{aM xiCCPICC ProfilexՖgTɉ40drF2HC a 0D1++QdBVE\ k@D1(ŀ {sOU=_WWU;y"Q:*!zIJI2ME!!m@&oMǰ> +H !p _$@1Ƹ"1i><> yP!gh8%)9펝S6W77e[YXNnOߦg5!kY? r@{@z@~8o:~#AAt̰մ'pV!b`!!2@ VA1fUA8ǠNyנnQxDMD1Ax#H!IE"kR B"ȯ)PNO8IHE"',ўG!w> qD")LHΤ`C*&UΑICdYlE!ǒr!Yr?y"Cѣ8R)b&Jr2DR j*uHH}H}KѴiPZ m%vv6@D=v=[pc2r njRL)s)@jTTT+iBBrץ_Pde+Ӕ((?RRRU^IZzLfDmZژHRK F6#LMm4tv=姕WGk\@;R{v#G'QgNΨnRz=^.k[   Y7F4Fƨqqu$dI)ThZkznjnng`6`24_mbjY[fufaknQgRre+c+UMk V6&6 6l2mlv~5ڍsB89+N;|rsqG/-t/6^nB_t,Zj2e{#wYQbhʃVbuwk״,bbqNkw9u*}+\-(-/ &6&ndiff[\,-+,y{[ɶwmRnS{uGIE`Ekn/UU=jj|)ٿmWnݥ?IswZ}}yEu~_H쬷?vhSڐ0rx#^GZ6Jܣc9O蝨9'{z_oumwߵ nro^5Vw̿#+;|/y|HxXHQcǵ$~ ş~*zxZLYY/ռ2|u/Gxlu=~~CG?q>u}l< KWm=Șļ)/Z41F >퇧"i񤗟Ӟy*`_;@J@z}JL7@WL,ىVSŘ581VU<11sbku0oӞ5'e0G&0<#j?4y]2^ pHYs   IDATxUtwA10֟ݍ.l"!(!)˲ϛ^ލ} gķs瞩=gfΈhPE@PE@PE@PE@PE@PE@PE@H pΝs衇kΛqE@PE@#0y/Ǝ1k.]nv)//IhDPE@P̬Rѣ8rvrHN֬YR4("(_cǎpc"/lڴiD~=("(̟?T{Hgm֬YB]lf̚.ժViӪ;v_Njղ32xUqgϧXjնjٴN)dQfjU۶nVlQE@PJ8^ąC qîǎ<h9Օ%KX6w]~=uپ}ձ5{3ڸQZvUխCKX^;wޮcOUeՍ^ X]V=T<&&u[=RmmѣȓT?'Lr9ڱ}VV}Oԫ[{ZK]c6zʴٜڱCDQvvm[ݻu];t#Ơ@8t=8=5 Ըqcf9^믿y< 6qkԨLk?2wIFXeE(Y.]>IM4jݢqΚ5{,ټYjUΜj;7BAyy-Js֓*,DUHvj>& n٤Qk׮1szp-ZԨ^s֜GOlݼI`h"Px7we~ٲe<@Ϟ=׭[7f? $W)S4oܞE[E8㸬WۣTx5???'Ϫ]v[Tϩ|3f`/_˩^c95n_¸I3ԭӡm,{Vf投f̜؊+332׫S}Ҵ95iȟ}Yj܆kdeeXr9PlժU6WFA,v5x<Hk"*Ep٪ԩ]lRZ6edf0r*XlE` YٺujWffedB&܆#VִIÅf_ty իWjv,fVz^@ P *V-i\*vZ"PR@ W\xb(~5\;Pƚ.nx9;x ^u #Fx.]z's1O?%q뭷K,C؄N:)A(uumԩYza6X)(乍 3iŁ%S*1Uf?3b~~A ͙005y[WNdYћfLbZ5;o?+  -Ƙ _}բEw  0G.YNuG=.dA7޸馛O~}tA|۷/fFܵk~r6R(1+,իB0 HƈzˇV^Wz${͚U쬪aEKׯM,9d(V,{,#/+YHu׬p`i"P= kΚ5_ߵkWfz뭘k֬?S0w>] ac=Go˖-@مtaֽ jV:3{ /kh{r@e7Z6kũYT7L^բE {&T㊀"63km[6=waڵ5mFd IKbQ kBS ׶l\{ykl֬CZdZ4nk״kx朹jhּy ϒܾU'4F2eVmgݺuu| ޵:묧~'x$\ǁȑG9w/vI8ׯj&T㊀"61ӆO>$/d:uVhˇ "f̙QZ9UrF4/]tEnV-7n$dV^h?3gϩ[jz+\hyM,\kAn^~vH2ZvV,_ b#,I,eQָ)ggvܶU+Nf_ 4މcCJ۶k4"P7nw{%fx @1MTMAN"kp){ӴjGS Uy:cLoPvHg 8LV ЦTl^ ٲ]/4;I-wm+"fY+3FwE@PE y Ϧ"("E-)bVNy^!+"(0WcA 6k,4{T("(XerB(2P,qKC&s+eV/WE@P6 BKg~^~-<Kԋ2 +"( jW ՙ+{Ia8b.|5("(@|Y斕ò-&dT5=TE@Pb8D%ų7v֒Y#S"(̼ qŊ`"n)1E@PE 1ԩS iJ`Ņ &LhѢ]TXʬ "("ChfϞ DnYC`+[QuԉS^ȔYhh\PE@؀`&XZkm,+p]v^ԔYhh\PE@؀#*kvI%W./jʬ^44("l@UKP)06aD ݄ʬz)"(`ujX] Bʬi"("UK,z-o^Yhh\PE@؀$ e 6V-2'5eV/H=3dH"(8Ź5M8eopżW"(L~-Z-RM[FWE@PxqGa`FE@PD`MeS~m&kMD6 rҩ"ۉL9!rW<[.Z]-W֬("Pnd֟E{zȅ{\+n<|ICVL("E.yQj۽TcA)ҥʐ?FRK)"TZ"T,Dƈ< jPBO\aY"4N)wDb} zPŋ&S(;E)B,>YϗdTE@Pʉ@ i"Eֈ,1yEi`wQ472/' 㷦u"E$`_E-N̜gz}uڅƍn +y& lq=07,uuWtZ%4Z!KcphPE@PBYs5!Xk8 -.JD'#mrM>S`ӓN:mٲ}\Jz:ۏagR.~RN:֯_o]?slnЬY3okXCZ+scE@PPD,ƝN'}H# e.rcqQj,+N̜u]MϿ`Ύ:E` 2$κ5}vL39b?5i++\FE@P@#tp\g xJQy[Ĉ+f#9Edqb"oѯgϽc矃'd<3*tYv68] \FE@P0bV&l&`Mx~31rs[raРt7?Æܹsb˞3OX`'[ܥYIj.FE@P*+Ȋ%ZSNI 5kW^ӱ .ᆢ˗]VM[FgAPE@(-ǬhQM}"&#|EשSLE=n={3ZLA"I3I-PE@P*0:Kb~D>+Q ḫ̌͒'}q3" 'g4;G8̙S:ף>J{iMPE@PhYѳ"(@j(J+"((FgE@PyڗXvm2}WE@"ʬ]Bdȳ& oV\p˖-Sf{APE!j gٗ Xg3Eqo]sȋ"Gbx|(s8\lv5ٞYf6mjժ"ƍ{S\׭[nڴ/xGkȐ!f:"t?V[y*Ph+E@P2W.rHF;vl~׏!T^?ZݻyNjP9C("C;ys\)rHƍCVg1Oiݺ51)SMdO]r%"(@yD fŻ>^iW)P"c ek Xе'^yLpC=3˜11e~~1MQE@(Ć-ϫm[)XYc7&"ˬY4Z^_Œ֜,u?sUEeefmw͚B>=c2֕E@Pr@tWE7u',"S,2Iϼ⚆hVijL]`f9sع_Iau駟f0{ـЯzE.4i)QE@(Go ?7̢ nE5kBxKm3%2c_f)'EɷYf,o];t7/ɦ_~,\[o6ӝ cf%CE@P@O35qtb93HqB3)~TM ̛7J*Xt[l Y{koc~ZYl< vAPE 5fe.FF8 $b1hE@\ƬDb¥A- ǡ^QE@P* i`HhE@P@@$P2E@P8ʬq$"("P(Z"("G L\o#wYh/RR шrU{IJ1L0a5|Tz)"Q:+"xȠVqiTj L㌯kM~dS' 'Ш5"qРAxqڢE v\l<ҒBKSE|!Ŭh/&"lr^n"Mߛ*_ҮWvے+/~wFq\Aynzc.1cǘ}k&&{ͰmHSNyWfVa&k=3^{5#3V904hpMOGi;*۳7lؐM{lK;࣏>* @peA{] ("PC"CLL㉻?a8RVX; Z D5|VU"rE\B ސw T2z# +VT@Kٳg|PU*VY ؓmQU85CJ8clbf_1LfO>{ 3 _FSE@(5n7 Ft7)n/,!ZV645H_pFPE"7ՙazcRD8~Y5(@F mfMJ?`2/IKƠX-O݄tStV[U}'TE&$CLn~S@bэFb"(^2/%N>c2֒%KWwPeS-[=쳡|[ڦHOZxCp<_>oTE!pxI(И"(=EagVft"7X6jY\lˬ6A:1jA 8WboƊ<6gCɻL5UYn12^]~ggvUcwU& gr'`abd 8׮]k+uѣcxw{Gu衇"'ì4஻bhG{ѢE|AvCE@P;i3A@g"OL:Ϩ,b_/L@%|m~iofW;rUh`[L";ŋ[jD:_2p"Dڟ; +x≨ǏwYvGG5~>3g?lwȢE@ ì@Š@IjUt*#2m7$NgE珟]d .5)Mi gKV6 Fڰ80d<M+~,<KZgk,n7֭[CN0ݎH07O?[|Vn\@?<| =b"vk*;cTX)+"P9(Y%OQCFNk@La[37~_;f!M_A{=QKo w9erb]w|pWƱgcƌabɤ`jfcW=7}U@a@fOufla ۷o2MRE@P*6Q'񪛿D30 rJ Qf143F/hXY)?RLv"{eχ -V0n]H~1v mz-:ۨ6_k rbTGW9u:#T (aT޽;ö^#38طNAPE:9SƙW'̪ҺѤL͝Ƒύ2ƤgʌM8Td0ILoyC "(Nk3Gd"qwz|m.aX8`hm]fz衇:({?UnN&cG.]csY|A-_G2e q% %eͨ, yovfZq]r y3o4(@B LEc*jgHmRĔPB!sbJOɭg +E;\:OTԈkeH[Wea*#.y@T˲ZoRVnAPE`#f(`bȍj蠠_e>`3̘+*lځ`mb@a!ptStV+`g$Du}I1Wߔ+C_B<,S("^'1'rqPulN>d#xUM`2֫0YiK4JTE@(tR0a*('3!^zi`&*"SʖZNAf+"(eVFE@P@@@"E@P2B#"(@ Z nJ,'j6;wSIw Wl0 GFV va'Vldb9ob*(@C Y1lc4x?prĶ0b8 $<w s5!Dz2ewyJv‡r^D^y7D9aS8qģӈ"(PfeQ|?Gw뭷~7 K?l_M i sSe'ql_]gu)K߽)6[ k"(Pf" DaXo;T\q;YHEfgIf˶Eݳf%ƋF'O4z_-|\-2vtfx.\lz5װ*nj 68)ds6!b{O?n SvqgBšݫW!CϹ_Y^{q8f60Giq">9CwÉq@-BOfWtxո"((f}"\%rW2YR-]_D.fxR= B|'ܙ"ḱך"iK̖56RوWT7ZI Ի\r=z`poVGx<܃ɓ'ABDlp|qv_~%𫯾j7w;Igs Li,vW*ٻ'tcѝ`p9GhH&aK\i3 Ml,Y>s6'"auT[ ˶o&hk?nUE#[g*>##Gm T',{ɦ'ƗlZ"(/ ՝BC.n"5#.i2R O;)d[c@~DrDƘ8jp>nb]oE7 tc8;1v5fo('`L gUY8;`IL.e4k;e'WHcƖFV9[Ff&3b(^]l ŦS{zEi.E@P,żiwfr24ՃEV7-.^ވ"n02Ӷ19a"F7n,G-rb)J0ҡX;R"&MbHMUb0dұE]ݘ1ٍ^dY|yڵj"(PYdžaF^6ӃO}lӜ'rj|XNk癅 A8 [3+ĥ6f^1UjWo0z90fqC@sAgIy[nZ 틵L9{ǦPOMo?1vw^E@t$Ŭy-(\'2& o$⼷ ۢ1n"'4Ud0i/r.r,oNLS"("P fY%2XEgOE"|a9CC# AyFDg"Ol"*CA5^ۛ`xaqD~H{m}Mۮ&("(!,,2ŵ5JjpIqo=f3)x,ɰv|f #n Y`NAɯ`CQa7zȵ"}$agF= ˫銀"( : *B /"ZJ&(Y3{j6qSz+ =v'9>:?` IDATaeqw?ƄaÆ^xE[{Eƾ}Ό"@i<0S`nML&8t<2%o=@O6/&|:Ћ_~g |VNG}dyJse"(@:kQśY7hYQ0Z ;Zfs=!3&6vg[p,} ڱ믿ʸ_P"P Q?n ޻w5k8}%f v2^ϥi޼t[n_;LH̊Vj Ǐq sz(@A mfb`xb}Em袋"yc=D bؐMy0bE3ghfl98EAQ,Ɔ1" UT:+z*gVd*v<3$O;Kr'LpiE[KވNZ۸e8ډCD.=(@e@ j Jod /?餓ƌ]#VӉCh( #%# .t)ĝaѦ &BN/f&Al┏L>_D_}ՖMe'?j(̼pݩHP ڜܹ3L%bzWE UJCQ.R|7|k:#" s n1ˆ{キ͈2J`B9Xn '$zI\Qഁ<P)&{m.bAmСC)YՅ{AV/n#ho:VjGᛩ,*(@D`1+j2w~ܘ;t; e2(A5'x"Z$ͼ\+`Ge1^&%d¬hCXbӗО|ACLL 1nMн{w[jՠAn)4aW ,śDڶ7=L."}xRJj"(< cv._,XUvw@D0S}0o^qNE?xf {H@ >Ro话儓. Tk#&]]vFj4ӦYiצM(2 +"(  6͏O_۾Ek5jPNYr_d+Vl 6EC㊀"(3fcWuںU 7WNzukTV%E9.'j^tn +"(`^vVͶj֨Izkܨڸaƍoӱs^AԩS.]EMuV/WE@P6 ~GMЦ9԰^+V]Wv:kլQfuٓ\|5eV/WE@P6 ߩ+W]ujuju)3׬^6kȩݮ65k)zи"("9iƜCdԨS3zիẂaTnZe)5gQZ񌌌O:ZF*ԮY53+b̀_TɪZ5HLTcU2)omنwرc<<`^{mt_y啃>xѢEbeٳ .<6g.e˖IVOI WTa K]vv={2űKjO-pz^{-rvqGJO.A3*5UΊ‚*Y0jW,?J[Fgoƍճ zGOcM4hڴ)Z/{9j 9#Gw1iҤyu֍p$m6o/]|| СC=0I~xOOTDm:1c0]<3@W^yiV^&,PJ:uns=|BM7D~g z ͋/,#F/J )ŢgkȐ!f:#ms /ЬYw}wſN .|'ZkذaVZ|GN8FHisovK;Gqv叫9|}ݗb ,^[˖-bO{}dxڵkg/٥{Nz,i-oAKG֣#{wo.VY~*S7eV[.~x3سk̒Km!oC[nqO዇B` +?Y_χfB 5Rq>&xxS4eϾ<|̄2|K}ip̅k'^g- m hm=覕R0Z5%,0(XC-;DԅIp*2|Pl:Q/O{D&PvTﰤM0yx4b42wG|%`h芅=)yᒋ;;VH3@}KQ* M>X2/JS\l:+1j0JRvVF2lIfׯcThrOqjJ%4>؉E͋vHp~ן[jb6f pNaEs DP% +Dhp~a$4 #/ƞd2 4|+QDw `ry)[Y̳7|3 Q _Ȍ ,*nn?Ɔ] qgt).%"kqnzE.Zc"bDy H v CV(r xA@<-1jg2іbɗ)?IHm|3oŭjSJͅ ae (jWD#௝U(|`Rg[>8y)Xha2h3Ų#[~{z2c/딘|=M;C"b!2p,HQ(C8$!WD2q? fCܴ苀t^NF]JK1c ̧!N2x ?ByUiгӰ6 cDOF蚐:+eV)3x fd.< nTX5]pd>$3| 2Y +RN2 9Tq@/\V8qr3c&F0{Zg NѿMC A ^}ޔ [50L\lu =EfEW( 6d7ƀY 4o9 zO0bfHpw;pc=)EĊ4"oJ0 #B_}UB* b !&.zH{ ̽)+;7Vی| Q |%q.?62!QfMhg&cᨃL:cJyX0_`ZTC )əҩr~mpC F,/^4ZRZ"RMΩV)Ŗ}U-</ȏXum5ECQ&f0)"(@Y@XP(z'0h2kYpE@P@AL7EOLl=$/fO^82kY.E@P2zLiAI_ J[>DE@P@!_XUSHaق Xc eYq{^x,Hc9)לC_+{9sqZ|2l. <&Fle J1tVk+ ֻ,K̀OcBff0Q(.Ukkq>xR}s\^a 5!B"S<-B`s­] )9[[XxRIiy (/)LQ)qsoH# \;Řb)9Lj5<gNt֞={MلȎ;?Ju*8>JdC$[K)ɺ~%nHj hפ~ ~ɴSJ-0 Nysa;\N>7X"Y0j&gXV¯zQ@_4Ræ0(nSX/gGE#a޷Qj@k-n:-ɫOm"~tqʞ>"] y죎[N(-GgV7llP#\mX;OG:.[e?j$wWeU<$㶞q岄E( _κ]Ä 4{w0xm [tG;O:Y9L#vi_=ԅ|^b+p)ε6!SwiXcP;pJƓpGa`kz6EM7݄X'D.>餓 |4C7yrEJDܽp}%0]{kJ9a?2xb4<f¯#zた}=qyK.˛2opQxE A4 'fKxs١2u C. LB!(6-ˬ8dNagTdfW3ąp? IDAT#D60d+E〸G4pXƒa |d`Vg<|imӍTo$@sE1ҧOˬ}Mߛ@1kX0=NǛ-a&;io}W c'͛2w•33kkyst;) M0*w!$`ͳz-ʕvYg.ceC+f$_k^wqp,4=FNVj/_ĵ(* ҄:yr<1ʻHG#IE;ܵ0_MAa=؟ߴ;dһxdۃt#A@! X haR> 2)8Y7bjC-eXN Օ(=dcvƄW#ȻTT:k&$7:ٰ3?7583˛OB865pX;0xI30^kg Gݏ؞3ehv(Yl"-LKk77i]o 7(7feוL * 1X cJ67ɴP>Seln]ratE%DҸƤȤT)-4yֿW R&6bzxQ6(>|jY c>V)SnJ Afjè{c6&vnvvHabfԓw y r!( O9&=!yrw y!7f6ɦ+HJF;|=xU,| t%pƟ3I>*歷by.6ݞ H)rp"nl;0c. 0kp3ɥN,9D^L.c>[BQ˒Mw&TyMVsCzu`9~HL6&G&Jy;1:P_L{Do߿5-S1:q`g*eo X)?DyhRVN%`<=[ 3`̝^|I1A;8pݕӆ~QBO6H~(6_q Wi|D>}თWtyqC3ڌ@I6!w ~7R@ s'D f7rz o24&Jy]`@l#S(GTA`0oZRONsÐ?M9FoOriW0~Z @f,fҥ = rj p!os<$xU]>1hCC ^S,EG-+`IY|?f=^[F1iKM]~Sٹ,LJQvYIZx4#X 1E)|'(q9_<OϪ̚> 90!!QªvaT`R{W]YKU-SPE#PagP("PHcVMQE@(k0:>5?k+e ;m"("GvE@PEIY ήEXhTY0j"("1nYْU%# /YkFfu4("Tzά(~J+("(%@Zk"(ʸ?kE/E@P@yڟ,mPEٻ*&ދ bFML1FM5h4jL=%H8^Q7;;?-C(A|VJ B issp 6!:eP8PҬ BG4)*4/3';;5'+٬BfB B@9ŏJU9TB!#@Y? Ble'I#Bx  Be (#Bx  Be (#Bx  B(A|r|XP;$g36\tVeƚ9yiy 9P KH!@dC>>kVng̘ճ}(T?ٌۖ!})N `Ǝ1j3֬ZsiI:zSkm3Rs[!i~[5{x99).ڦ& B BYUڬblc[XJQ( *2 c)_&fkV*jPtFͷ_;yfZ[GfQ\Y>\lLprm#BPgUYCfc,P҅nM`lcua*J(5`cWQaFflza{%u4a>ӎ,32Ӻr(^1b]>fYy.{yxe1k=L ?"eJXYk?M:/,)3:[4hou؅#3|qijYz2ss{L= M ؒǵB&sw:~HV[i.A:*R"BxCHjnrPyP e0/n f̋A|HFȋUVڲ 3) 34Y1.d=ࢇL{ZPѦag~6g?{so>?D.5l]~))6ZMx4tV3]<pmm K{N~Knz3W |Z5]pTh\;6ƿ/yo%B 7@ ⳪YuՕ1PƧ>` 6 ӆ%͝ mesMgi2$Lvm`gS\N0Eln٤N3Yľ[|]?u?_ߤIt0ؿ6rHTiX 7.ďmlBp#xD`~r_R2GdP"BP/%}r1u`l cGCfg+$5BXeSS׫iA榖Z-zO '퇘- YYatAJ7vj3JB3kõͥD&N]"(51Hwr|JjᨄXP4f~#2!@@i(qS|OenAAE^mN76n͇:Ȧh迀۩M8Xh'@ c(|k+̼jjj$c1)% 7T@syZڪ^T WBC%}!@D}7c$Rm>CI}$ݵ@A$bJ",c^ܟ,?\ܞX&Yc#Iqn^FЗh87080A Ku4hbd WTeB '>+C$AF1 C(X`*Ј%UBBŒz^F?ZW3Eg#ڃ +A{c\J5NiZ,Wmߩ)׎VB974e?{WAV/4aXVV<K&J!PV?>k#^A[Zy}v ʏX,z"ڄ>X3< gA9뭚:7hgn\ nDOOlncQLxQ x]ͭNx.W6cvsҭPKPK- 6D/CҎ[$Nകa@c:ibQh M+KDB  pqn8wѵۏ.oux]{^ǡ1cǎݿX9u&gl$/[yR IFIB2! C. %`ZܴdYZ>aU <ںu붻{N:[˫ĉS23= u!P( 8ﰏ^{FwөKoi:};1v? N`Zn b<4w4.W"&݋8Ҿ={~ *V;vl/߿{a- Ŋ66ְ8ÃWqn?;!|b 8{w7˖-W,Z,!@Ty+>kEXMԛl8aZ9CNpVܳGBuMzl##!ɉW"[Z08ז}#Ov!nչzZ_p^{S8K-Z4ǟ eجYÇϘ1랞 ~eԩ SJ_;q']pܿPmo:2#ӴiWWW++K1!@TWz/c[43N}N>4+]x Y,/EÏ]5q tޤ{2yg#No#~ hE'#4i2*޺u֭ۿUU菲-.]xw`ٳ{]k3OSMċ/~KJ}'65m=u]Ñ_mmS{玿{}CCCL 'O!!pP"BP@ VLE,*ʇƶI .ֳiVjroypCʰ(tUzz| ͭ_/ɓ}BRtpCȩ Ǯ$0׫f:7ߍ{Np!Сb?~"*ڭa]|LzFE⋐'a\<)*dBb -ڨgVVŋFW998K z\y>T}=yqQ%{-CqHHTF! @ V6Un ZZVB69񾝸ў kwH rHGRWW>Q!i޼yRMmݱFkeXN|!Dƍ -cKB y;+kr ! N9y2[yQgLv1,%~m0=6]1=3gWftZYj;XSOmQ\ +rlUg;w3=x &LggW ˃Ҭ oڵK[WՋK(e߾}ønj5;7ym;KCsgoX(>|KB 1Yq`}92(c{jYLbcգU;~(+9Ϭ ,Qw|_(56ꆯ\R,v Ny4@sf_ڸq3V_rX,,xפ cott81|#GQۜS"dozȀQٯ_vU*P/1'B#P}|| .4h>~8c.fJbfݺuOeMHH ݍ=*xق?Y,O(O! EYKJdVT#ڵY_Lf),5o***ZM~b#BP)@d<,`✝UFG *+n_S\Pj*"Z B]G⳾4~B EⳖ-$ B]G94jƝ֭nF]eTicW/6FJ#Jeci:Ou BAg}kncjwlѱۆs߰/2sghr%PO_7 ,qu檰H!_z롰HM2F5Il!N#P㳾56E6K04yH/¹˰GC(5:fdA'٫;n ٔ^)`T8R"@EgElMghߣ̜FNҊ7>O#xCw IDAT3$dgu))Y>;(%xІmZ?^e>]<?vyn:ѻw M'OhfeeEOmۛX[jkk"#2 ^tj~թc;w>炷CŒ/ ж ^tz79?l |F=j%%g~$ Fete0J;@B *ՅyTfŝs ߯"m,8YP,>78~+j2wӺ~rqس}{-<ٶuQCB=vyw΂:8uH;>u&=W-[TGs U3ꋎPl7oGԫkE56=3{4ɭOY6ֆ=mxޫU|:B6lkئ%GABg4ۺ~@XDoOeGc뮠?LPM!(RHWcA#B Pec E䌩-[)ztϲsi cgff/cOYh=I@~dVOjٹ/@ /(MNάY5K7k qjn+?Ҽ#/@[FȹũA E~pzu0mÖ329EUBx{@Y [q窐Z{8{!XItu6?}.P NäDaP*Sgd/] HߤޑJ2P׬bkLedTҕGJJ00`} j//B * %ZTiF_:n{Jkؠ_JGGe v>nLlT8q}geZpv.`dp*U3ONN..AdnRR-J Bxj2i(t~@2D%B *B0oNrnjhhAiq VbR^ucnoal?| NԦ1V ½Q}zֵ43ҳCZ, <8$.5-k_(u۫.*mUvy^y5k̵%20UrsB OO!\"`)&bd\_Euₓ%s>#=#[MxM"B(B|VM -M-MmY_).P9##֥S'7ty6^/!@o3B|Vg-OEV{%JJl˜etI+uy557[FùFFp( tlz +,/ӑ7׆QS2_f7"`66^ I B"pqnO^8Q{{=q5'Q/bR^Ť;vb*L?1Hr""ܓrI]p܄})51ŵ(97*FyNJʧ~;|Q)qłyq6E+ J!@`Q Z+?!2s;CU1{>8B ~ťNʑ[t!B * *(!@@@VD$B<?>+y+ݥ!@;%JTwɥQڗJ\6mZ lo%_뻩5SC1Su)'B(~|VҬ%g_yĮYڽZ{\ᇓ(-i?żⲷ:zsN^xY,õd :mz_diiyо7|?cB߄!@ Q1:_ Euuut CnJi Fg٭IrPYO*Y ۟W)XtԨ3feBA =%3?<7+r33r 33rsj:7;;+C~Je6e9c}d^FuNzruudj+Yo]*~?S>g||9Կ%'_*Oj{6/-E`#kiβhy*̬YÐ!urԉ/%iiiM2ر߿u Ƃdɷȴo>}0|8O6{HÆ QF2,,xժ)?xA>VAOI[}#C6{pAVqĵGdlN gϞR{ٲ%R"2>޽&&tZ䦥є#"YsҳrҲ23R3S3"d"U*ӎ5`Z9!/Ꚛ!S1Oy8ho̪ѡK { иzf6ц\<e wzueO"ʔ_HuxM Lˆ ;ߊ :fm߾;wa!!4ڵk+W'K% ؿ[^/̛7ˈkZZ|bZӇ*oa6 ˮ]ܸo~ͦ3f|ddĽ[| :b,So|ָѣteaFv9ru]D gVq &ܸq#ѯetI@qIХsہ"-FHO/U;M A֘Wz90[ձd%͏bl?c~0U$6cxz(BV]BpusZwhޡeV!=)F6m K˫s@3kkP ۷]#^zz`…_'` ۻcA}wQN˩Z#O.ѹ,aՕKB T#S+H aor=<;99a!XTyiVl̘i[d tW`c 3J]Bܽp c<N.MO(799Og)dU:3gZ`Wmv˗y>~Z)Reo|M|' "Ĵ?AyV@G;Q7ȜPe,|:03#4sȮ/.|u߅9-+M>>(U)Թ"0CM li`rrH2U[KW سR4-2T jl1M| I__GJa ~-2/}2lK=SabX&aC^|>h3'x)'B@._|۷o{wM\>}ĉ'Orʃ"""iSULUq!$`c+Xc1UrlQŧu떡'N Z B(6P~~~PO< |yhh()c&''E%X)֡aۦ /;zuXFt~X#-R9u >X\}8*ؔ[EEE+c :!@A@p 7B't$&'*FE`׮L ^JPkKP!P,Jm *U/L':!@!P Z' B Zw'BZfZFC!Pf;PTslߥ,$!U@CUiVRn%eBx7 Z;Bu\N͑9u&-[pժJ(fǎ_qTCn9SI@B hMUwnz#rkv!ɏ/~}i-ocSΥ[n7ԩuO8 !BAR٬MC[>_6eurԭoEk m;2ZЀ?>}ЧTw6뙙wZiod E6Zie={~ X? ,Z!z{wsqhd *@Y]ɘc[!`ph8031ƳY GݻuM}ݲT-/.;ҟ"|/?9msC pN-; Q[h>gNlqѪUkqɮ] Y 11۶6y$)2!@;T*ͺ1q[N# ^w߮isj]͟W@Vk@Izr򈶁fEYڨ|] xvXRtjҤ[B>ͪ;8:PD]3!PT``b\P* A&HL3xkJoJ5 Qx2 U+,lSyqRć!hG*Q!@ʦY+=j uddW],>pýY;#!^ *NO_@qCb 0TH!@TaĬXC67;;2[fRn`jƍ_!Pe(/:ɇ,90k3v1/r"{N~k9vƪ@ .G#UsäAY`*xuwCa Bf` ihSZ0֋2T.X =϶Ϟ&Vv~ o2ydĿ7bd/sWd,XE w%'֬YC\_>\D8z٣`̘µ,0OZ` c˖TjB?Nxڔ, ]iZ+m?!(`aC^|>he...))cR止1 wҬo]苷OJ ~_|^tH!@ykӛ @!@ f} B @K !@ۀiַ.PB Y^TslߥfZ3I[ ˩W7tu+LgW5liW wr씌4AUB]@4k9=v\M4֢/w8VeWnTnQMj:y2zsN^x9P߰/̝Ʌl)o±zm [ZZ: O>2 !@ЮrwnzכhDTkgL.nk9Z &,~D"'?H͕rŊF1c#pЉP=y_ ؾԧuv I u붻{#?nj/'N+*!B@Je6e9c}GQn}/Zc`myqׂɅ%GI&vG{1 :l|iﳊ*j0~3w'ލ%h(-Sqtp;:W#E=>/Pd_[KV8K>s 0'n`K!'kj~ z,ôA;E]^Y|6 #|rgng%q[{%G\ju5̴XіO4hwÇUߏnɼF^kxw:^3o9ۋ ?UhqBY_9{|Sfv^ߣz+*krqˠiSÿovc9vöSݼu߮`03ךqLDc]}<Ӱ5>i|%7  8;П%g5zzwy፼Mt' iKc~:,kSV:>gvAApX|py)qG kVD~`iiYOT13這ޚrsmPm~~5lv5VPГ#K%Պ\ "^W+PZ{[Nl Me"W8C|ԩB<!PyxO/+hoI EۇӦЩ"2p IDAT*yï^H~6_Gn}CJdPֳiVjro.rsN U!6u.K&836 '~9k:-;9 13=a9a]=ߝ{tQeJBy_mjy< Fs\d^ Imee`RԠMFF;^{pwMG3e"B---w =[$V@T*otbe5XvP覦vfС 'L?bD]=3!Or.|J-/7/).PK _ ;Kh C9ٲM }iP$d^g v,u0',.o*` ])9%Wj8drtt{_ y uS232a P ёs 1E<Ŭ/MM+΃dݠcQXܬ6?ܽ/Z.9ocmԣ= J3ʠ}#w7uXn*3;+q's{ hA0FɟU$jA%>׾I9 R2-Șb+QlunܸոѣdԾ~oeiZRV 71SHWGpƍGY=u$!@ d, F1昜-͒#LjMcAZohۨ9Ƈ<3w̩kL37tncӾE[䱚ɵ0a%L^CSR+c!>2Q?ܰDn5+#>A 5\"s\{54b.Hk礋FBOf0^ v9+[^>f(roc*$ Vo*,"08v&ƺX-@w׮m`(oXKCx/+5EKUodn-bmpV*QLz`9WGGLBׯ#Ldlo=K` k.ÆK]]aÆ||РD!@78M’S9*܍SFPlj!391>(<Rsz(RR0*XxNN!UH2z:E ׯ9gO2dBub9e8[n٣Iޟ9seJ yly< + Scjj}8,Yj޼UTToA@%B4k%Ym_%Mb䪼_n[ S{!Pi(/op:N!@ Ҭł B " Z@TL!@ Ҭł B " Z@TL!@ Ҭł B " Z@TL!@ Ҭł B " Z@TL!@ Ҭł B "PuW/>C%D fx)2zC5)+wŽʄ+S (cU&\]pebU2W>{O5ʫ7U Qdf'_"$9,[.#%c&5P#m^,H=*w ]q2ԨBX,x WFW&\X#FFU(Se]5!pl>^ZFr( 8!@U>>kCD!@(D@S!!@!P2H 7E!@(F4b\J!@ Ҭ%Íj!@ ͪ!@%C4kpZ!@bH*ƅ!@@ Z2ܨ!@!Ҭq!*!@!P2KVj!&7|<<)$ #GSE 8ʠd (e˖;XrW\QŪy֪y*aРAo9'.{ ![nYRb2 G:o޼vvl%kxڬe{rRxƋ/c$\ B ++ȐfffGO>Z sxk\h - rJJmVyxcxscΌ2WQ`L U>t+tYj%O3c0(0j/ {+>|~ w<@v Ν;eZZt->.\ٳW^5iDSN۷o͛;wwSN%$$ܻwoaVZC 2#ӵk׫W5j,]jB'''Bbiƍ;s 2KVO޹s'FzX744_ o@ٳ3..ŋxnkk[fMMMM(oddZ_w̘1xtt4j&RBx/_>iҤ;{@B?! %F!aڴiw/mz9sfHHHjj*1V=RѣG۷n"&`6c,@kXx)cag;cxՒWo(27g,Mf0c%B1%##!s3Y')PH_P{w5S[816׌ȡ_Bm޼ TPZΝ;7p@(-Z@.^K.?3**j” 0 #@ߴiӍ7_DtoH3n֬Y'Ndx>?~ Ut~}nJz^҄@˖x8$\L_aBݻ efu+4:ߨN2^=̌1ԅptsx M0teIݻw†X !HPBxs/_~%|দN@WCЕ5W?u޽B:/fѥ lVm1ybR6##~77C hTnݺ2y?]???MxJNNNOOEpL`j/\*hVa=fd,vhVh^(`؝Wx|]hVT~%)f|"]!MW4+z˗fDPYa 4Ç׭ŻTBd1skX<оa~G?ϟ/,BְI@u@ vBR:J%!*TبJKyLbQhwLf͚5p]n #{eIr(N0 e3~m+T"WRwo#OrL`|Lg !@ xC b-mXcrA$8iAǣ"BABk‰`Eք]aC?`  `5 v@BKXЗXK%_$O9<N`7؏դZ:Dd^S٘%RL pn"^e-‰ ݏeY`Tp+cV(Q"0o@ӕ5r|`E#ş"KKbHPI %cY?d.χ 4~Ic}/x(c5y _$0@?ſ S'wy_qgIkK%1V0AِX+Vd h˜.$%|p¸33&>{'3OK.KX݊D*Kc. ݃%*- & ܳ +  _1ӧOKiX( E, ˗@ĪQx)."V0ËkJ/L^"tFj/ EN47XPKp8= b*_qP^iϑQFHxقB@T(6F/dR!()^ptb~u%^T \F(+Xmƴ$e g J!P`Rk}P!2D)3TPxwACJչ0Xy,oq)T#lMUlxY]T<2d.`/CWv)(30ReoQ~pH krPNf-Y/!@9s`Fc+ʳijl-YgJY^TQ-B *Fe v:PaWy6*E1 BRMR{@EU] 5O!@YHY|I:!@iwx B ,Y,$ B]C8l pV~QM+]v`ӁmB7mI)GT`dh``a-=ZG dN\vά։N8jkV(|V)l80GEyepX >n!U(e0Fe?4eeEчe#*; q^+&j=@d3[Ntb -ne+$8Ml?6))]w2|>U>V(3"Je. #Tӟ?dR!mq꣚F$3o5!9L:`~`ư5ؕYN6' 6ڕ5fn`[eXVF"!PEO(l@0eQcɇÁc{ՒpYsEB0aI! c86+r;_zó.#al&W=_\PU cD,nalcmJ|ٹxzzy׈6?4b^dOi05yi8(|$vFPc9c蒗 4bl33cGx*3vi8iF uqlNR(P71vѲC.]ƶxǠhmwWk-94dvnqTjm _Tm 6/k+r+4Sq( x _3[mYuW6uD&wM Z{ڪ|cl)c*-mvDqWyFbr|<0]0KT8bVk\"9ڈ_ _"~ͫ0yWIJ=nJC+ℊ0.;0S=M 36'͓!u_y6~ʽ| BQ3 G溌d Ҡ;wtc;Gο($ݢ W ^\d=GJ̼" bb#>֭[q?t V涶m 3 MNA+p14m aOmfགྷKp11lH ƶ5uG' HɿT%gjSߪF~rUf Nwd!E)5;5ꨨh!"]AP9")R8TZ{` Bk:&#31< 0 !g'c3R)mOwcl/8o : z %}ۋ߈ ؠwa޹6Lc,Rď96dRkߚ;;| #gxCo2I3*F4Et `aC$>7t;Iϓ}!P3hM`kE\HdwB>׎{~_[,n --*AFSG6nZ\$9Z c!?w8uлAdCSZ3kPU[!sn]SFe! YHae^*Y\ vR*/ Z`K6'_=]ت`BypE[kyoL%'1vCEDٗbhq@"$t]πKJ0;~zZ R PhP$AU14/(MZk $NcEjni IDATQʋCkIKljmU0d% lB0[R֓2S%ƅ]'9҅B7/e|+?:F:'|O#߀S; ɁgwG=Ve8/1Ke)̶GAF(}H!N"=O&\kxgi]# zWx[VUW={_;2io s>̺PN*@7I-[Q潻+a~_;s\jTDLeK5^ \dhYXkO hdS_\iz<1W&TԘZ#`OjZ,55$jNT/. ʾ3bc#*>7ɱEouS-=cu(Q*=!@,Ox釺K[ثX)1y'8Q1s̻%y-?$9cUl!yĔmG|Oy!-cx=u*?ye|Z #ɻ|"1Jx) ]ReVğ nz7"Tr^.إS#aQnݞhaSmjVs0^% 2kbp4&H7upҮ޴[5Xe6E9oj!cSϫGmZڹ޾# jۤs|YW<(!" X7yH[S4?*->[k}OevPo$n[_ï}{&4``3Xw*X+FeոR@KW2ZcRX87~~ u[Uӓ?ZDƠQ4TJUQgD0PVFZ MbmpVVȕy֐.4Z8{* ȆfvšW|p ̯_}%/ + [n-:{tdSw]xʵPp귄lK K9?lsaҫ`fu!dK=9**#!@hjr[44||| a{  ]e!wa{M9{p@q౬tPķ,Uد#H5ʼnw:kXBfmJ)|o.`TJRS[mbǒXi$N#߱DK˗/#ohhhddd\\\rrrzz:윜LMMMLLPBxģL1EUY)`-H^ zټf^>ʻ% +KQ@.o .Ä:&d܂2l͈:}NXٛSV ]e Vٟ!{8WA['~]ا"wm&RB$^ʟyJs?wb0VOdq 5Io)pRrv aGPt_TA B# !@p"v'B XeH4Z*  BdS+~j B!3ܘh@!@@! YTq= B 6ae7W1XB C6+IXT~RK!@@D;1jFE!@Tdi1U m`R2X}X?~P1_Id řǿ3AbxFnE]Hɺ*!9z" +a11>H(PAQD Oy 쯫kz>^ VW}UM;XGq=SrnB-=uܝ|Gu%lڴ;۷'m&@CdX?ע[ZU=7x /;wsf80[`vY_z3{ |լCZgOV1;ugpe}>p1kpfg9skDZ 'clGxxD_;wɓ?N:=#=zX`)ҷoߏ?WvSO=矿:$/Iކ }d6{߿͛.BVcO>Ydɸq9$ q˗/Т{キO>'t믿v+RЅ>E_O6l}}WZ׿5|Ζdn8q9眓Lo޼QV\z7x!L~S0O@ۧNzR@KS&7TĸCzӦMŋ+++Go'--//p!C.\>+Vu]s0"" ִСCbkSNvj 7-1;D*^ݜuS5agهf̺3{3d"H89R]8#gz_8?!DMhpdvJz~':vHEN4iȑ~_|qgxo:p,,nݺ}/"qCJ~a{od i޽; 曁A$+}.C?.hwGl T+ 뮻F~iK.W]u6Bqr[֭[mV]]M4 T]w onA>[ڐڼDUo~H6|Gy$p NS*oںu5kzy*hiqn:v{M7["QJaCSocٳ'9[^|cp3NB*h;wr-K}zgSD\L>kG}Os,AҋqF"}gp_Pf5>d\5[7s{ 2P'x^PVvq:EDfIeES!.S镕#瞇ʱ~q-9(;'ҹsghj(++\qV2s>'La+'-[z>qĈ^UE>R޽CF_0}8՗&sWGKڬ@(+klٲd3C4?f9s&&,- 7!H^Yq9{=묳fϞMC4 'f(++#O"Mn8=!T!k1ulG%1t#lzǛ-\?mUI.!J۹+ģN88oFF"oΣ: M%5!NdLO袋Kҏ"*EWyfHL-}\}:rD|G.Fd}zKzg'TSeoSxWL3 T7PaH)&¡{pbjՊpb*̶N0ld8["sS9;aTN'1z5ox7t691kvuCoǛ9~Xμ%1[yQzžxx,4ڶm ]2EG|aH[zz"$!L|XT3xģ>[\T@KRSJ̺?pݹs3rc:ߣs;Ǹ4̳\6yX7ɿp3⺝ \y^˜39cF C_mЈ?Ϙb#d8o vrarzU%.Ly2z(D''G;x-Zp =z4CY{I(Rz_ L۵k\jvZj:Js(t!JPj ;2N ZH>C̠d4_x鸜[|TQ9SU43CS,Lu"N_ED@J%kƪg;[m%Nqsqujfvg4M*3}.N,njXfW%ݛYA"("S|q y. #^x!oޕ)a5ꡇV2櫜YL=ENz}rͥ `aHؤah)0eEqR+/Ȳc7;x'vmZ0ˍ+V䏔? {B NG|LFE %l"x^Y}WH`"ń_0LGgf&aJѶOM x}ũMj3eռ&)&" " "{f> 1YyMTDD@D@D "|2Ԛd3|J,w҆IѦ&xÇtƩM57>@}a2zhY)O,C/r$" "  x0y9s氹p©SzMvX1D5)"" " 'q3&l֜qpT@]A;'NrʐzLǏ'zy&W^ ^¬LAwSeX7n2U\Sd)" " ;BfR4֪ " " "`Qop/% " " "Pn>1kL7$U^D@D@"bZMl&Ila6"|UVZţ5k֬ٲe֭[YrM"͛v9"#ubԚ7$r^5f 6Vp$\av&!>hS_tm3N ¦"" " qL&@ODp*ܽ&8Yff-fK"fי}"'~5m0uγwvÊ]G/" "[Ȕ܋.rMFW-Y?AfwuqdREaP}좢sg}f?y3gn~ [|_(l\(LYF\KxAkk=mb3~QxsfmVYgbÐ/ƍYdA֔ΝX퀿]6-:k,I8k$xK]6=Ƶ Jښo{3DݢksG6b^{m۶%rL߫7esi縣]9Rz VUFgn)~׌ΥgEY_3χGP_0_F֭[|Am[ǡM^ߴĶڛ#_xɿ::vh&LWgNϞ=3N8k߿1҆@##֭ۤIMֱcǮ]zM Nm0#Šea5κ:iȸdd\_{XFd K~2d]4k4O8VmYm}oS6?]K>뒙̛+Wշ勀R.i߾}ip޽Y)WY&cN]҄ڥmR )lIi\"U/gx-CK8g|Oc[YWҪuo S7m)nkTEρp -Vq֊]>]O7WwͮSǑwqs} ʵWB4&hN Jŵlɛ\;+fu-MS5fP2b@C $ 8ɝUD/f/gem-S\7M[" " "@8NcqFai~b,SV)ߵ>k84ED@D@DkB duͨ)QNIDATm%lS R$E@D@D\xڵkTQS^b TOa0f,̔ yي@$Ю]'djaxmT65Z Me5IEq0wyd䐩U)kT@D@D@r`FAD@D@DPV$U^D@DX`A)]M2Q\D@D@%;6i2 k" " "P$#KGGQ\D@D@q!ZPa۫wrSLD@D@vs}gY++yHAD@D@g~gfn" =\7o^q֭UjYbE"4F)Y=%J>kcC/n=u2α-wU`z5W5vDowᆪ󻝅5,GvjM1KI$ݧ\_Ad}QNx"J$P<&ţVKhd[0 )k5βY| dpew/HE j$H/#_Q4FX?>`#uG"+;*;Q}Wm8P{hO 8ڸS 鮙Ѯ}VwHsHO_aϰ6z?mI/IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1450622414.0 qtconsole-5.5.2/docs/source/figs/jn.html0000664000175000017500000012760700000000000021017 0ustar00carloscarlos00000000000000

In [34]: from scipy.special import jn

In [35]: x = linspace(0,4*pi)

In [36]: for i in range(6):

    ...: plot(x,jn(i,x))

In [37]: 1/0

---------------------------------------------------------------------------

ZeroDivisionError Traceback (most recent call last)

/Users/minrk/<ipython-input-37-05c9758a9c21> in <module>()

----> 1 1/0

ZeroDivisionError: integer division or modulo by zero

In [38]:

././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1450622414.0 qtconsole-5.5.2/docs/source/figs/jn.xhtml0000664000175000017500000007105000000000000021175 0ustar00carloscarlos00000000000000

Python 2.6.1 (r261:67515, Feb 11 2010, 00:51:29)

Type "copyright", "credits" or "license" for more information.

IPython 0.11.alpha1.git -- An enhanced Interactive Python.

? -> Introduction and overview of IPython's features.

%quickref -> Quick reference.

help -> Python's own help system.

object? -> Details about 'object', use 'object??' for extra details.

%guiref -> A brief reference about the graphical user interface.

In [1]: from scipy.special import jn

In [2]: x = linspace(0,4*pi)

In [3]: for n in range(6):

   ...: plot(x,jn(n,x))

   ...:

In [4]:

././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603554021.0 qtconsole-5.5.2/docs/source/index.rst0000664000175000017500000004422400000000000020424 0ustar00carloscarlos00000000000000.. _qtconsole: ========================== The Qt Console for Jupyter ========================== :Release: |release| :Date: |today| To start the Qt console:: $ jupyter qtconsole .. toctree:: :maxdepth: 2 installation config_options changelog Overview ======== The Qt console is a very lightweight application that largely feels like a terminal, but provides a number of enhancements only possible in a GUI, such as inline figures, proper multi-line editing with syntax highlighting, graphical calltips, and much more. The Qt console can use any Jupyter kernel. .. figure:: _images/qtconsole.png :width: 400px :alt: Qt console with embedded plots :align: center :target: _images/qtconsole.png The Qt console with IPython, using inline matplotlib plots. The Qt console frontend has hand-coded emacs-style bindings for text navigation. This is not yet configurable. .. tip:: Since the Qt console tries hard to behave like a terminal, by default it immediately executes single lines of input that are complete. If you want to force multi-line input, hit :kbd:`Ctrl-Enter` at the end of the first line instead of :kbd:`Enter`, and it will open a new line for input. At any point in a multi-line block, you can force its execution (without having to go to the bottom) with :kbd:`Shift-Enter`. Inline graphics =============== One of the most exciting features of the Qt Console is embedded figures. You can plot with matplotlib in IPython, or the plotting library of choice in your kernel. .. image:: figs/besselj.png :width: 519px .. _saving: Saving and Printing =================== The Qt Console has the ability to save your current session, as either HTML or XHTML. Your inline figures will be PNG in HTML, or inlined as SVG in XHTML. PNG images have the option to be either in an external folder, as in many browsers' "Webpage, Complete" option, or inlined as well, for a larger, but more portable file. .. note:: Export to SVG+XHTML requires that you are using SVG figures, which is *not* the default. To switch the inline figure format in IPython to use SVG, do: .. sourcecode:: ipython In [10]: %config InlineBackend.figure_format = 'svg' Or, you can add the same line (c.Inline... instead of %config Inline...) to your config files. This will only affect figures plotted after making this call The widget also exposes the ability to print directly, via the default print shortcut or context menu. See these examples of :download:`png/html` and :download:`svg/xhtml ` output. Note that syntax highlighting does not survive export. This is a known issue, and is being investigated. Colors and Highlighting ======================= Terminal IPython has always had some coloring, but never syntax highlighting. There are a few simple color choices, specified by the ``colors`` flag or ``%colors`` magic: * LightBG for light backgrounds * Linux for dark backgrounds * NoColor for a simple colorless terminal The Qt widget, however, has full syntax highlighting as you type, handled by the `pygments`_ library. The ``style`` argument exposes access to any style by name that can be found by pygments, and there are several already installed. Screenshot of ``jupyter qtconsole --style monokai``, which uses the 'monokai' theme: .. image:: figs/colors_dark.png :width: 627px .. Note:: Calling ``jupyter qtconsole -h`` will show all the style names that pygments can find on your system. You can also pass the filename of a custom CSS stylesheet, if you want to do your own coloring, via the ``stylesheet`` argument. The default LightBG stylesheet: .. sourcecode:: css QPlainTextEdit, QTextEdit { background-color: white; color: black ; selection-background-color: #ccc} .error { color: red; } .in-prompt { color: navy; } .in-prompt-number { font-weight: bold; } .out-prompt { color: darkred; } .out-prompt-number { font-weight: bold; } /* .inverted is used to highlight selected completion */ .inverted { background-color: black ; color: white; } Fonts ===== The Qt console is configurable via the ConsoleWidget. To change these, set the ``font_family`` or ``font_size`` traits of the ConsoleWidget. For instance, to use 9pt Anonymous Pro:: $> jupyter qtconsole --ConsoleWidget.font_family="Anonymous Pro" --ConsoleWidget.font_size=9 Process Management ================== With the two-process ZMQ model, the frontend does not block input during execution. This means that actions can be taken by the frontend while the Kernel is executing, or even after it crashes. The most basic such command is via 'Ctrl-.', which restarts the kernel. This can be done in the middle of a blocking execution. The frontend can also know, via a heartbeat mechanism, that the kernel has died. This means that the frontend can safely restart the kernel. .. _multiple_consoles: Multiple Consoles ----------------- Since the Kernel listens on the network, multiple frontends can connect to it. These do not have to all be qt frontends - any Jupyter frontend can connect and run code. Other frontends can connect to your kernel, and share in the execution. This is great for collaboration. The ``--existing`` flag means connect to a kernel that already exists. Starting other consoles with that flag will not try to start their own kernel, but rather connect to yours. :file:`kernel-12345.json` is a small JSON file with the ip, port, and authentication information necessary to connect to your kernel. By default, this file will be in your Jupyter runtime directory. If it is somewhere else, you will need to use the full path of the connection file, rather than just its filename. If you need to find the connection info to send, and don't know where your connection file lives, there are a couple of ways to get it. If you are already running a console connected to an IPython kernel, you can use the ``%connect_info`` magic to display the information necessary to connect another frontend to the kernel. .. sourcecode:: ipython In [2]: %connect_info { "stdin_port":50255, "ip":"127.0.0.1", "hb_port":50256, "key":"70be6f0f-1564-4218-8cda-31be40a4d6aa", "shell_port":50253, "iopub_port":50254 } Paste the above JSON into a file, and connect with: $> ipython --existing or, if you are local, you can connect with just: $> ipython --existing kernel-12345.json or even just: $> ipython --existing if this is the most recent kernel you have started. Otherwise, you can find a connection file by name (and optionally profile) with :func:`jupyter_client.find_connection_file`: .. sourcecode:: bash $> python -c "from jupyter_client import find_connection_file;\ print(find_connection_file('kernel-12345.json'))" /home/you/Library/Jupyter/runtime/kernel-12345.json .. _kernel_security: Security -------- .. warning:: Since the ZMQ code currently has no encryption, listening on an external-facing IP is dangerous. You are giving any computer that can see you on the network the ability to connect to your kernel, and view your traffic. Read the rest of this section before listening on external ports or running a kernel on a shared machine. By default (for security reasons), the kernel only listens on localhost, so you can only connect multiple frontends to the kernel from your local machine. You can specify to listen on an external interface by specifying the ``ip`` argument:: $> jupyter qtconsole --ip=192.168.1.123 If you specify the ip as 0.0.0.0 or '*', that means all interfaces, so any computer that can see yours on the network can connect to the kernel. Messages are not encrypted, so users with access to the ports your kernel is using will be able to see any output of the kernel. They will **NOT** be able to issue shell commands as you due to message signatures. .. warning:: If you disable message signatures, then any user with access to the ports your kernel is listening on can issue arbitrary code as you. **DO NOT** disable message signatures unless you have a lot of trust in your environment. The one security feature Jupyter does provide is protection from unauthorized execution. Jupyter's messaging system will sign messages with HMAC digests using a shared-key. The key is never sent over the network, it is only used to generate a unique hash for each message, based on its content. When the kernel receives a message, it will check that the digest matches, and discard the message. You can use any file that only you have access to to generate this key, but the default is just to generate a new UUID. .. _ssh_tunnels: SSH Tunnels ----------- Sometimes you want to connect to machines across the internet, or just across a LAN that either doesn't permit open ports or you don't trust the other machines on the network. To do this, you can use SSH tunnels. SSH tunnels are a way to securely forward ports on your local machine to ports on another machine, to which you have SSH access. In simple cases, Jupyter's tools can forward ports over ssh by simply adding the ``--ssh=remote`` argument to the usual ``--existing...`` set of flags for connecting to a running kernel, after copying the JSON connection file (or its contents) to the second computer. .. warning:: Using SSH tunnels does *not* increase localhost security. In fact, when tunneling from one machine to another *both* machines have open ports on localhost available for connections to the kernel. There are two primary models for using SSH tunnels with Jupyter. The first is to have the Kernel listen only on localhost, and connect to it from another machine on the same LAN. First, let's start a kernel on machine **worker**, listening only on loopback:: user@worker $> ipython kernel [IPKernelApp] To connect another client to this kernel, use: [IPKernelApp] --existing kernel-12345.json In this case, the IP that you would connect to would still be 127.0.0.1, but you want to specify the additional ``--ssh`` argument with the hostname of the kernel (in this example, it's 'worker'):: user@client $> jupyter qtconsole --ssh=worker --existing /path/to/kernel-12345.json Which will write a new connection file with the forwarded ports, so you can reuse them:: [JupyterQtConsoleApp] To connect another client via this tunnel, use: [JupyterQtConsoleApp] --existing kernel-12345-ssh.json Note again that this opens ports on the *client* machine that point to your kernel. .. note:: the ssh argument is simply passed to openssh, so it can be fully specified ``user@host:port`` but it will also respect your aliases, etc. in :file:`.ssh/config` if you have any. The second pattern is for connecting to a machine behind a firewall across the internet (or otherwise wide network). This time, we have a machine **login** that you have ssh access to, which can see **kernel**, but **client** is on another network. The important difference now is that **client** can see **login**, but *not* **worker**. So we need to forward ports from client to worker *via* login. This means that the kernel must be started listening on external interfaces, so that its ports are visible to `login`:: user@worker $> ipython kernel --ip=0.0.0.0 [IPKernelApp] To connect another client to this kernel, use: [IPKernelApp] --existing kernel-12345.json Which we can connect to from the client with:: user@client $> jupyter qtconsole --ssh=login --ip=192.168.1.123 --existing /path/to/kernel-12345.json .. note:: The IP here is the address of worker as seen from *login*, and need only be specified if the kernel used the ambiguous 0.0.0.0 (all interfaces) address. If it had used 192.168.1.123 to start with, it would not be needed. Manual SSH tunnels ------------------ It's possible that Jupyter's ssh helper functions won't work for you, for various reasons. You can still connect to remote machines, as long as you set up the tunnels yourself. The basic format of forwarding a local port to a remote one is:: [client] $> ssh :: -f -N This will forward local connections to **localport** on client to **remoteip:remoteport** *via* **server**. Note that remoteip is interpreted relative to *server*, not the client. So if you have direct ssh access to the machine to which you want to forward connections, then the server *is* the remote machine, and remoteip should be server's IP as seen from the server itself, i.e. 127.0.0.1. Thus, to forward local port 12345 to remote port 54321 on a machine you can see, do:: [client] $> ssh machine 12345:127.0.0.1:54321 -f -N But if your target is actually on a LAN at 192.168.1.123, behind another machine called **login**, then you would do:: [client] $> ssh login 12345:192.168.1.16:54321 -f -N The ``-f -N`` on the end are flags that tell ssh to run in the background, and don't actually run any commands beyond creating the tunnel. .. seealso:: A short discussion of ssh tunnels: http://www.revsys.com/writings/quicktips/ssh-tunnel.html Stopping Kernels and Consoles ----------------------------- Since there can be many consoles per kernel, the shutdown mechanism and dialog are probably more complicated than you are used to. Since you don't always want to shutdown a kernel when you close a window, you are given the option to just close the console window or also close the Kernel and *all other windows*. Note that this only refers to all other *local* windows, as remote Consoles are not allowed to shutdown the kernel, and shutdowns do not close Remote consoles (to allow for saving, etc.). Rules: * Restarting the kernel automatically clears all *local* Consoles, and prompts remote Consoles about the reset. * Shutdown closes all *local* Consoles, and notifies remotes that the Kernel has been shutdown. * Remote Consoles may not restart or shutdown the kernel. Qt and the REPL =============== .. note:: This section is relevant regardless of the frontend you use to write Qt Code. This section is mostly there as it is easy to get confused and assume that writing Qt code in the QtConsole should change from usual Qt code. It should not. If you get confused, take a step back, and try writing your code using the pure terminal based ``jupyter console`` that does not involve Qt. An important part of working with the REPL – QtConsole, Jupyter notebook, IPython terminal – when you are writing your own Qt code is to remember that user code (in the kernel) is *not* in the same process as the frontend. This means that there is not necessarily any Qt code running in the kernel, and under most normal circumstances there isn't. This is true even if you are running the QtConsole. .. warning:: When executing code from the qtconsole prompt, it is **not possible** to access the QtApplication instance of the QtConsole itself. A common problem listed in the PyQt4 Gotchas_ is the fact that Python's garbage collection will destroy Qt objects (Windows, etc.) once there is no longer a Python reference to them, so you have to hold on to them. For instance, in: .. sourcecode:: python from PyQt4 import QtGui def make_window(): win = QtGui.QMainWindow() def make_and_return_window(): win = QtGui.QMainWindow() return win :func:`make_window` will never draw a window, because garbage collection will destroy it before it is drawn, whereas :func:`make_and_return_window` lets the caller decide when the window object should be destroyed. If, as a developer, you know that you always want your objects to last as long as the process, you can attach them to the ``QApplication`` instance itself: .. sourcecode:: python from PyQt4 import QtGui, QtCore # do this just once: app = QtCore.QCoreApplication.instance() if not app: # we are in the kernel in most of the case there is NO qt code running. # we need to create a Gui APP. app = QtGui.QApplication([]) app.references = set() # then when you create Windows, add them to the set def make_window(): win = QtGui.QMainWindow() app.references.add(win) Now the ``QApplication`` itself holds a reference to ``win``, so it will never be garbage collected until the application itself is destroyed. .. _Gotchas: http://pyqt.sourceforge.net/Docs/PyQt4/gotchas.html#garbage-collection Embedding the QtConsole in a Qt application ------------------------------------------- There are a few options to integrate the Jupyter Qt console with your own application: * Use :class:`qtconsole.rich_jupyter_widget.RichJupyterWidget` in your Qt application. This will embed the console widget in your GUI and start the kernel in a separate process, so code typed into the console cannot access objects in your application. See :file:`examples/embed_qtconsole.py` for an example. * Start an IPython kernel inside a PyQt application ( `ipkernel_qtapp.py `_ in the ``ipykernel`` repository shows how to do this). Then launch the Qt console in a separate process to connect to it. This means that the console will be in a separate window from your application's UI, but the code entered by the user runs in your application. * Start a special IPython kernel, the :class:`ipykernel.inprocess.ipkernel.InProcessKernel`, which allows a QtConsole in the same process. See :file:`examples/inprocess_qtconsole.py` for an example. This allows both the kernel and the console interface to be part of your application, but it is not well supported. We encourage you to use one of the above options instead if you can. Regressions =========== There are some features, where the qt console lags behind the Terminal frontend: * !cmd input: Due to our use of pexpect, we cannot pass input to subprocesses launched using the '!' escape, so you should never call a command that requires interactive input. For such cases, use the terminal IPython. This will not be fixed, as abandoning pexpect would significantly degrade the console experience. .. _PyQt: https://www.riverbankcomputing.com/software/pyqt/download .. _pygments: http://pygments.org/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693579838.0 qtconsole-5.5.2/docs/source/installation.rst0000664000175000017500000000224000000000000022006 0ustar00carloscarlos00000000000000Installation ============ The Qt console requires Qt, such as `PyQt6 `_, `PySide6 `_, `PyQt5 `_, `PySide2 `_. Although `pip `_ and `conda `_ may be used to install the Qt console, conda is simpler to use since it automatically installs PyQt. Install using conda ------------------- To install:: conda install qtconsole .. note:: If the Qt console is installed using conda, it will **automatically** install the Qt requirement as well. Install using pip ----------------- To install:: pip install qtconsole Installing Qt (if needed) ------------------------- We recommend installing PyQt with `conda `_:: conda install pyqt or with pip:: pip install PyQt5 For example with Linux Debian's system package manager, use:: sudo apt-get install python3-pyqt5 # PyQt5 on Python 3 See also:: `Installing Jupyter `_ The Qt console is part of the Jupyter ecosystem. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1714924509.6736565 qtconsole-5.5.2/examples/0000775000175000017500000000000000000000000016143 5ustar00carloscarlos00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603554021.0 qtconsole-5.5.2/examples/embed_qtconsole.py0000775000175000017500000000305400000000000021665 0ustar00carloscarlos00000000000000"""An example of embedding a RichJupyterWidget in a PyQT Application. This uses a normal kernel launched as a subprocess. It shows how to shutdown the kernel cleanly when the application quits. To run: python3 embed_qtconsole.py """ import sys from qtpy import QtWidgets from qtconsole.rich_jupyter_widget import RichJupyterWidget from qtconsole.manager import QtKernelManager # The ID of an installed kernel, e.g. 'bash' or 'ir'. USE_KERNEL = 'python3' def make_jupyter_widget_with_kernel(): """Start a kernel, connect to it, and create a RichJupyterWidget to use it """ kernel_manager = QtKernelManager(kernel_name=USE_KERNEL) kernel_manager.start_kernel() kernel_client = kernel_manager.client() kernel_client.start_channels() jupyter_widget = RichJupyterWidget() jupyter_widget.kernel_manager = kernel_manager jupyter_widget.kernel_client = kernel_client return jupyter_widget class MainWindow(QtWidgets.QMainWindow): """A window that contains a single Qt console.""" def __init__(self): super().__init__() self.jupyter_widget = make_jupyter_widget_with_kernel() self.setCentralWidget(self.jupyter_widget) def shutdown_kernel(self): print('Shutting down kernel...') self.jupyter_widget.kernel_client.stop_channels() self.jupyter_widget.kernel_manager.shutdown_kernel() if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) window = MainWindow() window.show() app.aboutToQuit.connect(window.shutdown_kernel) sys.exit(app.exec_()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603554021.0 qtconsole-5.5.2/examples/inprocess_qtconsole.py0000664000175000017500000000204100000000000022606 0ustar00carloscarlos00000000000000"""An example of embedding a RichJupyterWidget with an in-process kernel. We recommend using a kernel in a separate process as the normal option - see embed_qtconsole.py for more information. In-process kernels are not well supported. To run this example: python3 inprocess_qtconsole.py """ from qtpy import QtWidgets from qtconsole.rich_jupyter_widget import RichJupyterWidget from qtconsole.inprocess import QtInProcessKernelManager def show(): global ipython_widget # Prevent from being garbage collected # Create an in-process kernel kernel_manager = QtInProcessKernelManager() kernel_manager.start_kernel(show_banner=False) kernel = kernel_manager.kernel kernel.gui = 'qt4' kernel_client = kernel_manager.client() kernel_client.start_channels() ipython_widget = RichJupyterWidget() ipython_widget.kernel_manager = kernel_manager ipython_widget.kernel_client = kernel_client ipython_widget.show() if __name__ == "__main__": app = QtWidgets.QApplication([]) show() app.exec_() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603554021.0 qtconsole-5.5.2/examples/jupyter-qtconsole.desktop0000664000175000017500000000064500000000000023252 0ustar00carloscarlos00000000000000# If you want jupyter qtconsole to appear in a linux app launcher ("start menu"), install this by doing: # sudo desktop-file-install ipython-qtconsole.desktop [Desktop Entry] Comment=Jupyter qtconsole Exec=jupyter qtconsole GenericName[en_US]=Jupyter shell GenericName=Jupyter shell Icon=network-idle Name[en_US]=Jupyter Qt console Name=Jupyter Qt console Categories=Development;Utility; Terminal=false Type=Application ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1714924509.6816564 qtconsole-5.5.2/qtconsole/0000775000175000017500000000000000000000000016334 5ustar00carloscarlos00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1450622414.0 qtconsole-5.5.2/qtconsole/__init__.py0000664000175000017500000000006000000000000020441 0ustar00carloscarlos00000000000000from ._version import version_info, __version__ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1450622414.0 qtconsole-5.5.2/qtconsole/__main__.py0000664000175000017500000000012200000000000020421 0ustar00carloscarlos00000000000000if __name__ == '__main__': from qtconsole.qtconsoleapp import main main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1714924422.0 qtconsole-5.5.2/qtconsole/_version.py0000664000175000017500000000011000000000000020522 0ustar00carloscarlos00000000000000version_info = (5, 5, 2) __version__ = '.'.join(map(str, version_info)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1636915703.0 qtconsole-5.5.2/qtconsole/ansi_code_processor.py0000664000175000017500000003355100000000000022740 0ustar00carloscarlos00000000000000""" Utilities for processing ANSI escape codes and special ASCII characters. """ #----------------------------------------------------------------------------- # Imports #----------------------------------------------------------------------------- # Standard library imports from collections import namedtuple import re # System library imports from qtpy import QtGui # Local imports from qtconsole.styles import dark_style #----------------------------------------------------------------------------- # Constants and datatypes #----------------------------------------------------------------------------- # An action for erase requests (ED and EL commands). EraseAction = namedtuple('EraseAction', ['action', 'area', 'erase_to']) # An action for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL, CHA, CUP, # and HVP commands). # FIXME: Not implemented in AnsiCodeProcessor. MoveAction = namedtuple('MoveAction', ['action', 'dir', 'unit', 'count']) # An action for scroll requests (SU and ST) and form feeds. ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count']) # An action for the carriage return character CarriageReturnAction = namedtuple('CarriageReturnAction', ['action']) # An action for the \n character NewLineAction = namedtuple('NewLineAction', ['action']) # An action for the beep character BeepAction = namedtuple('BeepAction', ['action']) # An action for backspace BackSpaceAction = namedtuple('BackSpaceAction', ['action']) # Regular expressions. CSI_COMMANDS = 'ABCDEFGHJKSTfmnsu' CSI_SUBPATTERN = '\\[(.*?)([%s])' % CSI_COMMANDS OSC_SUBPATTERN = '\\](.*?)[\x07\x1b]' ANSI_PATTERN = ('\x01?\x1b(%s|%s)\x02?' % \ (CSI_SUBPATTERN, OSC_SUBPATTERN)) ANSI_OR_SPECIAL_PATTERN = re.compile('(\a|\b|\r(?!\n)|\r?\n)|(?:%s)' % ANSI_PATTERN) SPECIAL_PATTERN = re.compile('([\f])') #----------------------------------------------------------------------------- # Classes #----------------------------------------------------------------------------- class AnsiCodeProcessor(object): """ Translates special ASCII characters and ANSI escape codes into readable attributes. It also supports a few non-standard, xterm-specific codes. """ # Whether to increase intensity or set boldness for SGR code 1. # (Different terminals handle this in different ways.) bold_text_enabled = False # We provide an empty default color map because subclasses will likely want # to use a custom color format. default_color_map = {} #--------------------------------------------------------------------------- # AnsiCodeProcessor interface #--------------------------------------------------------------------------- def __init__(self): self.actions = [] self.color_map = self.default_color_map.copy() self.reset_sgr() def reset_sgr(self): """ Reset graphics attributs to their default values. """ self.intensity = 0 self.italic = False self.bold = False self.underline = False self.foreground_color = None self.background_color = None def split_string(self, string): """ Yields substrings for which the same escape code applies. """ self.actions = [] start = 0 # strings ending with \r are assumed to be ending in \r\n since # \n is appended to output strings automatically. Accounting # for that, here. last_char = '\n' if len(string) > 0 and string[-1] == '\n' else None string = string[:-1] if last_char is not None else string for match in ANSI_OR_SPECIAL_PATTERN.finditer(string): raw = string[start:match.start()] substring = SPECIAL_PATTERN.sub(self._replace_special, raw) if substring or self.actions: yield substring self.actions = [] start = match.end() groups = [g for g in match.groups() if (g is not None)] g0 = groups[0] if g0 == '\a': self.actions.append(BeepAction('beep')) yield None self.actions = [] elif g0 == '\r': self.actions.append(CarriageReturnAction('carriage-return')) yield None self.actions = [] elif g0 == '\b': self.actions.append(BackSpaceAction('backspace')) yield None self.actions = [] elif g0 == '\n' or g0 == '\r\n': self.actions.append(NewLineAction('newline')) yield g0 self.actions = [] else: params = [ param for param in groups[1].split(';') if param ] if g0.startswith('['): # Case 1: CSI code. try: params = list(map(int, params)) except ValueError: # Silently discard badly formed codes. pass else: self.set_csi_code(groups[2], params) elif g0.startswith(']'): # Case 2: OSC code. self.set_osc_code(params) raw = string[start:] substring = SPECIAL_PATTERN.sub(self._replace_special, raw) if substring or self.actions: yield substring if last_char is not None: self.actions.append(NewLineAction('newline')) yield last_char def set_csi_code(self, command, params=[]): """ Set attributes based on CSI (Control Sequence Introducer) code. Parameters ---------- command : str The code identifier, i.e. the final character in the sequence. params : sequence of integers, optional The parameter codes for the command. """ if command == 'm': # SGR - Select Graphic Rendition if params: self.set_sgr_code(params) else: self.set_sgr_code([0]) elif (command == 'J' or # ED - Erase Data command == 'K'): # EL - Erase in Line code = params[0] if params else 0 if 0 <= code <= 2: area = 'screen' if command == 'J' else 'line' if code == 0: erase_to = 'end' elif code == 1: erase_to = 'start' elif code == 2: erase_to = 'all' self.actions.append(EraseAction('erase', area, erase_to)) elif (command == 'S' or # SU - Scroll Up command == 'T'): # SD - Scroll Down dir = 'up' if command == 'S' else 'down' count = params[0] if params else 1 self.actions.append(ScrollAction('scroll', dir, 'line', count)) def set_osc_code(self, params): """ Set attributes based on OSC (Operating System Command) parameters. Parameters ---------- params : sequence of str The parameters for the command. """ try: command = int(params.pop(0)) except (IndexError, ValueError): return if command == 4: # xterm-specific: set color number to color spec. try: color = int(params.pop(0)) spec = params.pop(0) self.color_map[color] = self._parse_xterm_color_spec(spec) except (IndexError, ValueError): pass def set_sgr_code(self, params): """ Set attributes based on SGR (Select Graphic Rendition) codes. Parameters ---------- params : sequence of ints A list of SGR codes for one or more SGR commands. Usually this sequence will have one element per command, although certain xterm-specific commands requires multiple elements. """ # Always consume the first parameter. if not params: return code = params.pop(0) if code == 0: self.reset_sgr() elif code == 1: if self.bold_text_enabled: self.bold = True else: self.intensity = 1 elif code == 2: self.intensity = 0 elif code == 3: self.italic = True elif code == 4: self.underline = True elif code == 22: self.intensity = 0 self.bold = False elif code == 23: self.italic = False elif code == 24: self.underline = False elif code >= 30 and code <= 37: self.foreground_color = code - 30 elif code == 38 and params: _color_type = params.pop(0) if _color_type == 5 and params: # xterm-specific: 256 color support. self.foreground_color = params.pop(0) elif _color_type == 2: # 24bit true colour support. self.foreground_color = params[:3] params[:3] = [] elif code == 39: self.foreground_color = None elif code >= 40 and code <= 47: self.background_color = code - 40 elif code == 48 and params: _color_type = params.pop(0) if _color_type == 5 and params: # xterm-specific: 256 color support. self.background_color = params.pop(0) elif _color_type == 2: # 24bit true colour support. self.background_color = params[:3] params[:3] = [] elif code == 49: self.background_color = None # Recurse with unconsumed parameters. self.set_sgr_code(params) #--------------------------------------------------------------------------- # Protected interface #--------------------------------------------------------------------------- def _parse_xterm_color_spec(self, spec): if spec.startswith('rgb:'): return tuple(map(lambda x: int(x, 16), spec[4:].split('/'))) elif spec.startswith('rgbi:'): return tuple(map(lambda x: int(float(x) * 255), spec[5:].split('/'))) elif spec == '?': raise ValueError('Unsupported xterm color spec') return spec def _replace_special(self, match): special = match.group(1) if special == '\f': self.actions.append(ScrollAction('scroll', 'down', 'page', 1)) return '' class QtAnsiCodeProcessor(AnsiCodeProcessor): """ Translates ANSI escape codes into QTextCharFormats. """ # A map from ANSI color codes to SVG color names or RGB(A) tuples. darkbg_color_map = { 0 : 'black', # black 1 : 'darkred', # red 2 : 'darkgreen', # green 3 : 'brown', # yellow 4 : 'darkblue', # blue 5 : 'darkviolet', # magenta 6 : 'steelblue', # cyan 7 : 'grey', # white 8 : 'grey', # black (bright) 9 : 'red', # red (bright) 10 : 'lime', # green (bright) 11 : 'yellow', # yellow (bright) 12 : 'deepskyblue', # blue (bright) 13 : 'magenta', # magenta (bright) 14 : 'cyan', # cyan (bright) 15 : 'white' } # white (bright) # Set the default color map for super class. default_color_map = darkbg_color_map.copy() def get_color(self, color, intensity=0): """ Returns a QColor for a given color code or rgb list, or None if one cannot be constructed. """ if isinstance(color, int): # Adjust for intensity, if possible. if color < 8 and intensity > 0: color += 8 constructor = self.color_map.get(color, None) elif isinstance(color, (tuple, list)): constructor = color else: return None if isinstance(constructor, str): # If this is an X11 color name, we just hope there is a close SVG # color name. We could use QColor's static method # 'setAllowX11ColorNames()', but this is global and only available # on X11. It seems cleaner to aim for uniformity of behavior. return QtGui.QColor(constructor) elif isinstance(constructor, (tuple, list)): return QtGui.QColor(*constructor) return None def get_format(self): """ Returns a QTextCharFormat that encodes the current style attributes. """ format = QtGui.QTextCharFormat() # Set foreground color qcolor = self.get_color(self.foreground_color, self.intensity) if qcolor is not None: format.setForeground(qcolor) # Set background color qcolor = self.get_color(self.background_color, self.intensity) if qcolor is not None: format.setBackground(qcolor) # Set font weight/style options if self.bold: format.setFontWeight(QtGui.QFont.Bold) else: format.setFontWeight(QtGui.QFont.Normal) format.setFontItalic(self.italic) format.setFontUnderline(self.underline) return format def set_background_color(self, style): """ Given a syntax style, attempt to set a color map that will be aesthetically pleasing. """ # Set a new default color map. self.default_color_map = self.darkbg_color_map.copy() if not dark_style(style): # Colors appropriate for a terminal with a light background. For # now, only use non-bright colors... for i in range(8): self.default_color_map[i + 8] = self.default_color_map[i] # ...and replace white with black. self.default_color_map[7] = self.default_color_map[15] = 'black' # Update the current color map with the new defaults. self.color_map.update(self.default_color_map) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1636915703.0 qtconsole-5.5.2/qtconsole/base_frontend_mixin.py0000664000175000017500000001422700000000000022731 0ustar00carloscarlos00000000000000"""Defines a convenient mix-in class for implementing Qt frontends.""" class BaseFrontendMixin(object): """ A mix-in class for implementing Qt frontends. To handle messages of a particular type, frontends need only define an appropriate handler method. For example, to handle 'stream' messaged, define a '_handle_stream(msg)' method. """ #--------------------------------------------------------------------------- # 'BaseFrontendMixin' concrete interface #--------------------------------------------------------------------------- _kernel_client = None _kernel_manager = None @property def kernel_client(self): """Returns the current kernel client.""" return self._kernel_client @kernel_client.setter def kernel_client(self, kernel_client): """Disconnect from the current kernel client (if any) and set a new kernel client. """ # Disconnect the old kernel client, if necessary. old_client = self._kernel_client if old_client is not None: old_client.started_channels.disconnect(self._started_channels) old_client.stopped_channels.disconnect(self._stopped_channels) # Disconnect the old kernel client's channels. old_client.iopub_channel.message_received.disconnect(self._dispatch) old_client.shell_channel.message_received.disconnect(self._dispatch) old_client.stdin_channel.message_received.disconnect(self._dispatch) old_client.hb_channel.kernel_died.disconnect( self._handle_kernel_died) # Handle the case where the old kernel client is still listening. if old_client.channels_running: self._stopped_channels() # Set the new kernel client. self._kernel_client = kernel_client if kernel_client is None: return # Connect the new kernel client. kernel_client.started_channels.connect(self._started_channels) kernel_client.stopped_channels.connect(self._stopped_channels) # Connect the new kernel client's channels. kernel_client.iopub_channel.message_received.connect(self._dispatch) kernel_client.shell_channel.message_received.connect(self._dispatch) kernel_client.stdin_channel.message_received.connect(self._dispatch) # hb_channel kernel_client.hb_channel.kernel_died.connect(self._handle_kernel_died) # Handle the case where the kernel client started channels before # we connected. if kernel_client.channels_running: self._started_channels() @property def kernel_manager(self): """The kernel manager, if any""" return self._kernel_manager @kernel_manager.setter def kernel_manager(self, kernel_manager): old_man = self._kernel_manager if old_man is not None: old_man.kernel_restarted.disconnect(self._handle_kernel_restarted) self._kernel_manager = kernel_manager if kernel_manager is None: return kernel_manager.kernel_restarted.connect(self._handle_kernel_restarted) #--------------------------------------------------------------------------- # 'BaseFrontendMixin' abstract interface #--------------------------------------------------------------------------- def _handle_kernel_died(self, since_last_heartbeat): """ This is called when the ``kernel_died`` signal is emitted. This method is called when the kernel heartbeat has not been active for a certain amount of time. This is a strictly passive notification - the kernel is likely being restarted by its KernelManager. Parameters ---------- since_last_heartbeat : float The time since the heartbeat was last received. """ def _handle_kernel_restarted(self): """ This is called when the ``kernel_restarted`` signal is emitted. This method is called when the kernel has been restarted by the autorestart mechanism. Parameters ---------- since_last_heartbeat : float The time since the heartbeat was last received. """ def _started_kernel(self): """Called when the KernelManager starts (or restarts) the kernel subprocess. Channels may or may not be running at this point. """ def _started_channels(self): """ Called when the KernelManager channels have started listening or when the frontend is assigned an already listening KernelManager. """ def _stopped_channels(self): """ Called when the KernelManager channels have stopped listening or when a listening KernelManager is removed from the frontend. """ #--------------------------------------------------------------------------- # 'BaseFrontendMixin' protected interface #--------------------------------------------------------------------------- def _dispatch(self, msg): """ Calls the frontend handler associated with the message type of the given message. """ msg_type = msg['header']['msg_type'] handler = getattr(self, '_handle_' + msg_type, None) if handler: handler(msg) def from_here(self, msg): """Return whether a message is from this session""" session_id = self._kernel_client.session.session return msg['parent_header'].get("session", session_id) == session_id def include_output(self, msg): """Return whether we should include a given output message""" if msg['parent_header']: # If parent message is from hidden execution, don't include it. msg_id = msg['parent_header']['msg_id'] info = self._request_info['execute'].get(msg_id) if info and info.hidden: return False from_here = self.from_here(msg) if msg['msg_type'] == 'execute_input': # only echo inputs not from here return self.include_other_output and not from_here if self.include_other_output: return True else: return from_here ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1615943229.0 qtconsole-5.5.2/qtconsole/bracket_matcher.py0000664000175000017500000000722500000000000022032 0ustar00carloscarlos00000000000000""" Provides bracket matching for Q[Plain]TextEdit widgets. """ # System library imports from qtpy import QtCore, QtGui, QtWidgets class BracketMatcher(QtCore.QObject): """ Matches square brackets, braces, and parentheses based on cursor position. """ # Protected class variables. _opening_map = { '(':')', '{':'}', '[':']' } _closing_map = { ')':'(', '}':'{', ']':'[' } #-------------------------------------------------------------------------- # 'QObject' interface #-------------------------------------------------------------------------- def __init__(self, text_edit): """ Create a call tip manager that is attached to the specified Qt text edit widget. """ assert isinstance(text_edit, (QtWidgets.QTextEdit, QtWidgets.QPlainTextEdit)) super().__init__() # The format to apply to matching brackets. self.format = QtGui.QTextCharFormat() self.format.setBackground(QtGui.QColor('silver')) self._text_edit = text_edit text_edit.cursorPositionChanged.connect(self._cursor_position_changed) #-------------------------------------------------------------------------- # Protected interface #-------------------------------------------------------------------------- def _find_match(self, position): """ Given a valid position in the text document, try to find the position of the matching bracket. Returns -1 if unsuccessful. """ # Decide what character to search for and what direction to search in. document = self._text_edit.document() start_char = document.characterAt(position) search_char = self._opening_map.get(start_char) if search_char: increment = 1 else: search_char = self._closing_map.get(start_char) if search_char: increment = -1 else: return -1 # Search for the character. char = start_char depth = 0 while position >= 0 and position < document.characterCount(): if char == start_char: depth += 1 elif char == search_char: depth -= 1 if depth == 0: break position += increment char = document.characterAt(position) else: position = -1 return position def _selection_for_character(self, position): """ Convenience method for selecting a character. """ selection = QtWidgets.QTextEdit.ExtraSelection() cursor = self._text_edit.textCursor() cursor.setPosition(position) cursor.movePosition(QtGui.QTextCursor.NextCharacter, QtGui.QTextCursor.KeepAnchor) selection.cursor = cursor selection.format = self.format return selection #------ Signal handlers ---------------------------------------------------- def _cursor_position_changed(self): """ Updates the document formatting based on the new cursor position. """ # Clear out the old formatting. self._text_edit.setExtraSelections([]) # Attempt to match a bracket for the new cursor position. cursor = self._text_edit.textCursor() if not cursor.hasSelection(): position = cursor.position() - 1 match_position = self._find_match(position) if match_position != -1: extra_selections = [ self._selection_for_character(pos) for pos in (position, match_position) ] self._text_edit.setExtraSelections(extra_selections) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1648405558.0 qtconsole-5.5.2/qtconsole/call_tip_widget.py0000664000175000017500000002467300000000000022054 0ustar00carloscarlos00000000000000# Standard library imports import re from unicodedata import category # System library imports from qtpy import QT6 from qtpy import QtCore, QtGui, QtWidgets class CallTipWidget(QtWidgets.QLabel): """ Shows call tips by parsing the current text of Q[Plain]TextEdit. """ #-------------------------------------------------------------------------- # 'QObject' interface #-------------------------------------------------------------------------- def __init__(self, text_edit): """ Create a call tip manager that is attached to the specified Qt text edit widget. """ assert isinstance(text_edit, (QtWidgets.QTextEdit, QtWidgets.QPlainTextEdit)) super().__init__(None, QtCore.Qt.ToolTip) text_edit.destroyed.connect(self.deleteLater) self._hide_timer = QtCore.QBasicTimer() self._text_edit = text_edit self.setFont(text_edit.document().defaultFont()) self.setForegroundRole(QtGui.QPalette.ToolTipText) self.setBackgroundRole(QtGui.QPalette.ToolTipBase) self.setPalette(QtWidgets.QToolTip.palette()) self.setAlignment(QtCore.Qt.AlignLeft) self.setIndent(1) self.setFrameStyle(QtWidgets.QFrame.NoFrame) self.setMargin(1 + self.style().pixelMetric( QtWidgets.QStyle.PM_ToolTipLabelFrameWidth, None, self)) self.setWindowOpacity(self.style().styleHint( QtWidgets.QStyle.SH_ToolTipLabel_Opacity, None, self, None) / 255.0) self.setWordWrap(True) def eventFilter(self, obj, event): """ Reimplemented to hide on certain key presses and on text edit focus changes. """ if obj == self._text_edit: etype = event.type() if etype == QtCore.QEvent.KeyPress: key = event.key() if key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return): self.hide() elif key == QtCore.Qt.Key_Escape: self.hide() return True elif etype == QtCore.QEvent.FocusOut: self.hide() elif etype == QtCore.QEvent.Enter: self._hide_timer.stop() elif etype == QtCore.QEvent.Leave: self._leave_event_hide() return super().eventFilter(obj, event) def timerEvent(self, event): """ Reimplemented to hide the widget when the hide timer fires. """ if event.timerId() == self._hide_timer.timerId(): self._hide_timer.stop() self.hide() #-------------------------------------------------------------------------- # 'QWidget' interface #-------------------------------------------------------------------------- def enterEvent(self, event): """ Reimplemented to cancel the hide timer. """ super().enterEvent(event) self._hide_timer.stop() def hideEvent(self, event): """ Reimplemented to disconnect signal handlers and event filter. """ super().hideEvent(event) # This fixes issue jupyter/qtconsole#383 try: self._text_edit.cursorPositionChanged.disconnect( self._cursor_position_changed) except TypeError: pass self._text_edit.removeEventFilter(self) def leaveEvent(self, event): """ Reimplemented to start the hide timer. """ super().leaveEvent(event) self._leave_event_hide() def paintEvent(self, event): """ Reimplemented to paint the background panel. """ painter = QtWidgets.QStylePainter(self) option = QtWidgets.QStyleOptionFrame() option.initFrom(self) painter.drawPrimitive(QtWidgets.QStyle.PE_PanelTipLabel, option) painter.end() super().paintEvent(event) def setFont(self, font): """ Reimplemented to allow use of this method as a slot. """ super().setFont(font) def showEvent(self, event): """ Reimplemented to connect signal handlers and event filter. """ super().showEvent(event) self._text_edit.cursorPositionChanged.connect( self._cursor_position_changed) self._text_edit.installEventFilter(self) def deleteLater(self): """ Avoids an error when the widget has already been deleted. Fixes jupyter/qtconsole#507. """ try: return super().deleteLater() except RuntimeError: pass #-------------------------------------------------------------------------- # 'CallTipWidget' interface #-------------------------------------------------------------------------- def show_inspect_data(self, content, maxlines=20): """Show inspection data as a tooltip""" data = content.get('data', {}) text = data.get('text/plain', '') match = re.match("(?:[^\n]*\n){%i}" % maxlines, text) if match: text = text[:match.end()] + '\n[Documentation continues...]' return self.show_tip(self._format_tooltip(text)) def show_tip(self, tip): """ Attempts to show the specified tip at the current cursor location. """ # Attempt to find the cursor position at which to show the call tip. text_edit = self._text_edit document = text_edit.document() cursor = text_edit.textCursor() search_pos = cursor.position() - 1 self._start_position, _ = self._find_parenthesis(search_pos, forward=False) if self._start_position == -1: return False # Set the text and resize the widget accordingly. self.setText(tip) self.resize(self.sizeHint()) # Locate and show the widget. Place the tip below the current line # unless it would be off the screen. In that case, decide the best # location based trying to minimize the area that goes off-screen. padding = 3 # Distance in pixels between cursor bounds and tip box. cursor_rect = text_edit.cursorRect(cursor) if QT6: screen_rect = text_edit.screen().geometry() else: screen_rect = QtWidgets.QApplication.instance().desktop().screenGeometry(text_edit) point = text_edit.mapToGlobal(cursor_rect.bottomRight()) point.setY(point.y() + padding) tip_height = self.size().height() tip_width = self.size().width() vertical = 'bottom' horizontal = 'Right' if point.y() + tip_height > screen_rect.height() + screen_rect.y(): point_ = text_edit.mapToGlobal(cursor_rect.topRight()) # If tip is still off screen, check if point is in top or bottom # half of screen. if point_.y() - tip_height < padding: # If point is in upper half of screen, show tip below it. # otherwise above it. if 2*point.y() < screen_rect.height(): vertical = 'bottom' else: vertical = 'top' else: vertical = 'top' if point.x() + tip_width > screen_rect.width() + screen_rect.x(): point_ = text_edit.mapToGlobal(cursor_rect.topRight()) # If tip is still off-screen, check if point is in the right or # left half of the screen. if point_.x() - tip_width < padding: if 2*point.x() < screen_rect.width(): horizontal = 'Right' else: horizontal = 'Left' else: horizontal = 'Left' pos = getattr(cursor_rect, '%s%s' %(vertical, horizontal)) point = text_edit.mapToGlobal(pos()) point.setY(point.y() + padding) if vertical == 'top': point.setY(point.y() - tip_height) if horizontal == 'Left': point.setX(point.x() - tip_width - padding) self.move(point) self.show() return True #-------------------------------------------------------------------------- # Protected interface #-------------------------------------------------------------------------- def _find_parenthesis(self, position, forward=True): """ If 'forward' is True (resp. False), proceed forwards (resp. backwards) through the line that contains 'position' until an unmatched closing (resp. opening) parenthesis is found. Returns a tuple containing the position of this parenthesis (or -1 if it is not found) and the number commas (at depth 0) found along the way. """ commas = depth = 0 document = self._text_edit.document() char = document.characterAt(position) # Search until a match is found or a non-printable character is # encountered. while category(char) != 'Cc' and position > 0: if char == ',' and depth == 0: commas += 1 elif char == ')': if forward and depth == 0: break depth += 1 elif char == '(': if not forward and depth == 0: break depth -= 1 position += 1 if forward else -1 char = document.characterAt(position) else: position = -1 return position, commas def _leave_event_hide(self): """ Hides the tooltip after some time has passed (assuming the cursor is not over the tooltip). """ if (not self._hide_timer.isActive() and # If Enter events always came after Leave events, we wouldn't need # this check. But on Mac OS, it sometimes happens the other way # around when the tooltip is created. QtWidgets.QApplication.instance().topLevelAt(QtGui.QCursor.pos()) != self): self._hide_timer.start(300, self) def _format_tooltip(self, doc): doc = re.sub(r'\033\[(\d|;)+?m', '', doc) return doc #------ Signal handlers ---------------------------------------------------- def _cursor_position_changed(self): """ Updates the tip based on user cursor movement. """ cursor = self._text_edit.textCursor() if cursor.position() <= self._start_position: self.hide() else: position, commas = self._find_parenthesis(self._start_position + 1) if position != -1: self.hide() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1680190457.0 qtconsole-5.5.2/qtconsole/client.py0000664000175000017500000000366300000000000020174 0ustar00carloscarlos00000000000000""" Defines a KernelClient that provides signals and slots. """ # Third-party imports from jupyter_client.channels import HBChannel from jupyter_client.threaded import ThreadedKernelClient, ThreadedZMQSocketChannel from qtpy import QtCore from traitlets import Type # Local imports from .kernel_mixins import QtKernelClientMixin from .util import SuperQObject class QtHBChannel(SuperQObject, HBChannel): # A longer timeout than the base class time_to_dead = 3.0 # Emitted when the kernel has died. kernel_died = QtCore.Signal(object) def call_handlers(self, since_last_heartbeat): """ Reimplemented to emit signals instead of making callbacks. """ # Emit the generic signal. self.kernel_died.emit(since_last_heartbeat) class QtZMQSocketChannel(ThreadedZMQSocketChannel, SuperQObject): """A ZMQ socket emitting a Qt signal when a message is received.""" message_received = QtCore.Signal(object) def process_events(self): """ Process any pending GUI events. """ QtCore.QCoreApplication.instance().processEvents() def call_handlers(self, msg): """This method is called in the ioloop thread when a message arrives. It is important to remember that this method is called in the thread so that some logic must be done to ensure that the application level handlers are called in the application thread. """ # Emit the generic signal. self.message_received.emit(msg) def closed(self): """Check if the channel is closed.""" return self.stream is None or self.stream.closed() class QtKernelClient(QtKernelClientMixin, ThreadedKernelClient): """ A KernelClient that provides signals and slots. """ iopub_channel_class = Type(QtZMQSocketChannel) shell_channel_class = Type(QtZMQSocketChannel) stdin_channel_class = Type(QtZMQSocketChannel) hb_channel_class = Type(QtHBChannel) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1697726811.0 qtconsole-5.5.2/qtconsole/comms.py0000664000175000017500000002122000000000000020021 0ustar00carloscarlos00000000000000""" Based on https://github.com/jupyter/notebook/blob/master/notebook/static/services/kernels/comm.js https://github.com/ipython/ipykernel/blob/master/ipykernel/comm/manager.py https://github.com/ipython/ipykernel/blob/master/ipykernel/comm/comm.py Which are distributed under the terms of the Modified BSD License. """ import logging from traitlets.config import LoggingConfigurable import uuid from qtpy import QtCore from qtconsole.util import MetaQObjectHasTraits, SuperQObject, import_item class CommManager(MetaQObjectHasTraits( 'NewBase', (LoggingConfigurable, SuperQObject), {})): """ Manager for Comms in the Frontend """ def __init__(self, kernel_client, *args, **kwargs): super().__init__(*args, **kwargs) self.comms = {} self.targets = {} if kernel_client: self.init_kernel_client(kernel_client) def init_kernel_client(self, kernel_client): """ connect the kernel, and register message handlers """ self.kernel_client = kernel_client kernel_client.iopub_channel.message_received.connect(self._dispatch) @QtCore.Slot(object) def _dispatch(self, msg): """Dispatch messages""" msg_type = msg['header']['msg_type'] handled_msg_types = ['comm_open', 'comm_msg', 'comm_close'] if msg_type in handled_msg_types: getattr(self, msg_type)(msg) def new_comm(self, target_name, data=None, metadata=None, comm_id=None, buffers=None): """ Create a new Comm, register it, and open its Kernel-side counterpart Mimics the auto-registration in `Comm.__init__` in the Jupyter Comm. argument comm_id is optional """ comm = Comm(target_name, self.kernel_client, comm_id) self.register_comm(comm) try: comm.open(data, metadata, buffers) except Exception: self.unregister_comm(comm) raise return comm def register_target(self, target_name, f): """Register a callable f for a given target name f will be called with two arguments when a comm_open message is received with `target`: - the Comm instance - the `comm_open` message itself. f can be a Python callable or an import string for one. """ if isinstance(f, str): f = import_item(f) self.targets[target_name] = f def unregister_target(self, target_name, f): """Unregister a callable registered with register_target""" return self.targets.pop(target_name) def register_comm(self, comm): """Register a new comm""" comm_id = comm.comm_id comm.kernel_client = self.kernel_client self.comms[comm_id] = comm comm.sig_is_closing.connect(self.unregister_comm) return comm_id @QtCore.Slot(object) def unregister_comm(self, comm): """Unregister a comm, and close its counterpart.""" # unlike get_comm, this should raise a KeyError comm.sig_is_closing.disconnect(self.unregister_comm) self.comms.pop(comm.comm_id) def get_comm(self, comm_id, closing=False): """Get a comm with a particular id Returns the comm if found, otherwise None. This will not raise an error, it will log messages if the comm cannot be found. If the comm is closing, it might already have closed, so this is ignored. """ try: return self.comms[comm_id] except KeyError: if closing: return self.log.warning("No such comm: %s", comm_id) # don't create the list of keys if debug messages aren't enabled if self.log.isEnabledFor(logging.DEBUG): self.log.debug("Current comms: %s", list(self.comms.keys())) # comm message handlers def comm_open(self, msg): """Handler for comm_open messages""" content = msg['content'] comm_id = content['comm_id'] target_name = content['target_name'] f = self.targets.get(target_name, None) comm = Comm(target_name, self.kernel_client, comm_id) self.register_comm(comm) if f is None: self.log.error("No such comm target registered: %s", target_name) else: try: f(comm, msg) return except Exception: self.log.error("Exception opening comm with target: %s", target_name, exc_info=True) # Failure. try: comm.close() except Exception: self.log.error( "Could not close comm during `comm_open` failure " "clean-up. The comm may not have been opened yet.""", exc_info=True) def comm_close(self, msg): """Handler for comm_close messages""" content = msg['content'] comm_id = content['comm_id'] comm = self.get_comm(comm_id, closing=True) if comm is None: return self.unregister_comm(comm) try: comm.handle_close(msg) except Exception: self.log.error('Exception in comm_close for %s', comm_id, exc_info=True) def comm_msg(self, msg): """Handler for comm_msg messages""" content = msg['content'] comm_id = content['comm_id'] comm = self.get_comm(comm_id) if comm is None: return try: comm.handle_msg(msg) except Exception: self.log.error('Exception in comm_msg for %s', comm_id, exc_info=True) class Comm(MetaQObjectHasTraits( 'NewBase', (LoggingConfigurable, SuperQObject), {})): """ Comm base class """ sig_is_closing = QtCore.Signal(object) def __init__(self, target_name, kernel_client, comm_id=None, msg_callback=None, close_callback=None): """ Create a new comm. Must call open to use. """ super().__init__(target_name=target_name) self.target_name = target_name self.kernel_client = kernel_client if comm_id is None: comm_id = uuid.uuid1().hex self.comm_id = comm_id self._msg_callback = msg_callback self._close_callback = close_callback self._send_channel = self.kernel_client.shell_channel def _send_msg(self, msg_type, content, data, metadata, buffers): """ Send a message on the shell channel. """ if data is None: data = {} if content is None: content = {} content['comm_id'] = self.comm_id content['data'] = data msg = self.kernel_client.session.msg( msg_type, content, metadata=metadata) if buffers: msg['buffers'] = buffers return self._send_channel.send(msg) # methods for sending messages def open(self, data=None, metadata=None, buffers=None): """Open the kernel-side version of this comm""" return self._send_msg( 'comm_open', {'target_name': self.target_name}, data, metadata, buffers) def send(self, data=None, metadata=None, buffers=None): """Send a message to the kernel-side version of this comm""" return self._send_msg( 'comm_msg', {}, data, metadata, buffers) def close(self, data=None, metadata=None, buffers=None): """Close the kernel-side version of this comm""" self.sig_is_closing.emit(self) return self._send_msg( 'comm_close', {}, data, metadata, buffers) # methods for registering callbacks for incoming messages def on_msg(self, callback): """Register a callback for comm_msg Will be called with the `data` of any comm_msg messages. Call `on_msg(None)` to disable an existing callback. """ self._msg_callback = callback def on_close(self, callback): """Register a callback for comm_close Will be called with the `data` of the close message. Call `on_close(None)` to disable an existing callback. """ self._close_callback = callback # methods for handling incoming messages def handle_msg(self, msg): """Handle a comm_msg message""" self.log.debug("handle_msg[%s](%s)", self.comm_id, msg) if self._msg_callback: return self._msg_callback(msg) def handle_close(self, msg): """Handle a comm_close message""" self.log.debug("handle_close[%s](%s)", self.comm_id, msg) if self._close_callback: return self._close_callback(msg) __all__ = ['CommManager'] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1697726811.0 qtconsole-5.5.2/qtconsole/completion_html.py0000664000175000017500000003115500000000000022110 0ustar00carloscarlos00000000000000"""A navigable completer for the qtconsole""" # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. from qtpy import QtCore, QtGui, QtWidgets from .util import compute_item_matrix #-------------------------------------------------------------------------- # Return an HTML table with selected item in a special class #-------------------------------------------------------------------------- def html_tableify(item_matrix, select=None, header=None , footer=None) : """ returnr a string for an html table""" if not item_matrix : return '' html_cols = [] tds = lambda text : ''+text+' ' trs = lambda text : ''+text+'' tds_items = [list(map(tds, row)) for row in item_matrix] if select : row, col = select tds_items[row][col] = ''\ +item_matrix[row][col]\ +' ' #select the right item html_cols = map(trs, (''.join(row) for row in tds_items)) head = '' foot = '' if header : head = (''\ +''.join((''+header+'')*len(item_matrix[0]))\ +'') if footer : foot = (''\ +''.join((''+footer+'')*len(item_matrix[0]))\ +'') html = ('' + head + (''.join(html_cols)) + foot + '
') return html class SlidingInterval(object): """a bound interval that follows a cursor internally used to scoll the completion view when the cursor try to go beyond the edges, and show '...' when rows are hidden """ _min = 0 _max = 1 _current = 0 def __init__(self, maximum=1, width=6, minimum=0, sticky_lenght=1): """Create a new bounded interval any value return by this will be bound between maximum and minimum. usual width will be 'width', and sticky_length set when the return interval should expand to max and min """ self._min = minimum self._max = maximum self._start = 0 self._width = width self._stop = self._start+self._width+1 self._sticky_lenght = sticky_lenght @property def current(self): """current cursor position""" return self._current @current.setter def current(self, value): """set current cursor position""" current = min(max(self._min, value), self._max) self._current = current if current > self._stop : self._stop = current self._start = current-self._width elif current < self._start : self._start = current self._stop = current + self._width if abs(self._start - self._min) <= self._sticky_lenght : self._start = self._min if abs(self._stop - self._max) <= self._sticky_lenght : self._stop = self._max @property def start(self): """begiiing of interval to show""" return self._start @property def stop(self): """end of interval to show""" return self._stop @property def width(self): return self._stop - self._start @property def nth(self): return self.current - self.start class CompletionHtml(QtWidgets.QWidget): """ A widget for tab completion, navigable by arrow keys """ #-------------------------------------------------------------------------- # 'QObject' interface #-------------------------------------------------------------------------- _items = () _index = (0, 0) _consecutive_tab = 0 _size = (1, 1) _old_cursor = None _start_position = 0 _slice_start = 0 _slice_len = 4 def __init__(self, console_widget, rows=10): """ Create a completion widget that is attached to the specified Qt text edit widget. """ assert isinstance(console_widget._control, (QtWidgets.QTextEdit, QtWidgets.QPlainTextEdit)) super().__init__() self._text_edit = console_widget._control self._console_widget = console_widget self._rows = rows if rows > 0 else 10 self._text_edit.installEventFilter(self) self._sliding_interval = None self._justified_items = None # Ensure that the text edit keeps focus when widget is displayed. self.setFocusProxy(self._text_edit) def eventFilter(self, obj, event): """ Reimplemented to handle keyboard input and to auto-hide when the text edit loses focus. """ if obj == self._text_edit: etype = event.type() if etype == QtCore.QEvent.KeyPress: key = event.key() if self._consecutive_tab == 0 and key in (QtCore.Qt.Key_Tab,): return False elif self._consecutive_tab == 1 and key in (QtCore.Qt.Key_Tab,): # ok , called twice, we grab focus, and show the cursor self._consecutive_tab = self._consecutive_tab+1 self._update_list() return True elif self._consecutive_tab == 2: if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): self._complete_current() return True if key in (QtCore.Qt.Key_Tab,): self.select_right() self._update_list() return True elif key in ( QtCore.Qt.Key_Down,): self.select_down() self._update_list() return True elif key in (QtCore.Qt.Key_Right,): self.select_right() self._update_list() return True elif key in ( QtCore.Qt.Key_Up,): self.select_up() self._update_list() return True elif key in ( QtCore.Qt.Key_Left,): self.select_left() self._update_list() return True elif key in ( QtCore.Qt.Key_Escape,): self.cancel_completion() return True else : self.cancel_completion() else: self.cancel_completion() elif etype == QtCore.QEvent.FocusOut: self.cancel_completion() return super().eventFilter(obj, event) #-------------------------------------------------------------------------- # 'CompletionHtml' interface #-------------------------------------------------------------------------- def cancel_completion(self): """Cancel the completion should be called when the completer have to be dismissed This reset internal variable, clearing the temporary buffer of the console where the completion are shown. """ self._consecutive_tab = 0 self._slice_start = 0 self._console_widget._clear_temporary_buffer() self._index = (0, 0) if(self._sliding_interval): self._sliding_interval = None # # ... 2 4 4 4 4 4 4 4 4 4 4 4 4 # 2 2 4 4 4 4 4 4 4 4 4 4 4 4 # #2 2 x x x x x x x x x x x 5 5 #6 6 x x x x x x x x x x x 5 5 #6 6 x x x x x x x x x x ? 5 5 #6 6 x x x x x x x x x x ? 1 1 # #3 3 3 3 3 3 3 3 3 3 3 3 1 1 1 ... #3 3 3 3 3 3 3 3 3 3 3 3 1 1 1 ... def _select_index(self, row, col): """Change the selection index, and make sure it stays in the right range A little more complicated than just dooing modulo the number of row columns to be sure to cycle through all element. horizontaly, the element are maped like this : to r <-- a b c d e f --> to g to f <-- g h i j k l --> to m to l <-- m n o p q r --> to a and vertically a d g j m p b e h k n q c f i l o r """ nr, nc = self._size nr = nr-1 nc = nc-1 # case 1 if (row > nr and col >= nc) or (row >= nr and col > nc): self._select_index(0, 0) # case 2 elif (row <= 0 and col < 0) or (row < 0 and col <= 0): self._select_index(nr, nc) # case 3 elif row > nr : self._select_index(0, col+1) # case 4 elif row < 0 : self._select_index(nr, col-1) # case 5 elif col > nc : self._select_index(row+1, 0) # case 6 elif col < 0 : self._select_index(row-1, nc) elif 0 <= row and row <= nr and 0 <= col and col <= nc : self._index = (row, col) else : raise NotImplementedError("you'r trying to go where no completion\ have gone before : %d:%d (%d:%d)"%(row, col, nr, nc) ) @property def _slice_end(self): end = self._slice_start+self._slice_len if end > len(self._items) : return None return end def select_up(self): """move cursor up""" r, c = self._index self._select_index(r-1, c) def select_down(self): """move cursor down""" r, c = self._index self._select_index(r+1, c) def select_left(self): """move cursor left""" r, c = self._index self._select_index(r, c-1) def select_right(self): """move cursor right""" r, c = self._index self._select_index(r, c+1) def show_items(self, cursor, items, prefix_length=0): """ Shows the completion widget with 'items' at the position specified by 'cursor'. """ if not items : return # Move cursor to start of the prefix to replace it # when a item is selected cursor.movePosition(QtGui.QTextCursor.Left, n=prefix_length) self._start_position = cursor.position() self._consecutive_tab = 1 # Calculate the number of characters available. width = self._text_edit.document().textWidth() char_width = self._console_widget._get_font_width() displaywidth = int(max(10, (width / char_width) - 1)) items_m, ci = compute_item_matrix(items, empty=" ", displaywidth=displaywidth) self._sliding_interval = SlidingInterval(len(items_m) - 1, width=self._rows) self._items = items_m self._size = (ci['rows_numbers'], ci['columns_numbers']) self._old_cursor = cursor self._index = (0, 0) sjoin = lambda x : [ y.ljust(w, ' ') for y, w in zip(x, ci['columns_width'])] self._justified_items = list(map(sjoin, items_m)) self._update_list(hilight=False) def _update_list(self, hilight=True): """ update the list of completion and hilight the currently selected completion """ self._sliding_interval.current = self._index[0] head = None foot = None if self._sliding_interval.start > 0 : head = '...' if self._sliding_interval.stop < self._sliding_interval._max: foot = '...' items_m = self._justified_items[\ self._sliding_interval.start:\ self._sliding_interval.stop+1\ ] self._console_widget._clear_temporary_buffer() if(hilight): sel = (self._sliding_interval.nth, self._index[1]) else : sel = None strng = html_tableify(items_m, select=sel, header=head, footer=foot) self._console_widget._fill_temporary_buffer(self._old_cursor, strng, html=True) #-------------------------------------------------------------------------- # Protected interface #-------------------------------------------------------------------------- def _complete_current(self): """ Perform the completion with the currently selected item. """ i = self._index item = self._items[i[0]][i[1]] item = item.strip() if item : self._current_text_cursor().insertText(item) self.cancel_completion() def _current_text_cursor(self): """ Returns a cursor with text between the start position and the current position selected. """ cursor = self._text_edit.textCursor() if cursor.position() >= self._start_position: cursor.setPosition(self._start_position, QtGui.QTextCursor.KeepAnchor) return cursor ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1697726811.0 qtconsole-5.5.2/qtconsole/completion_plain.py0000664000175000017500000000427200000000000022247 0ustar00carloscarlos00000000000000"""A simple completer for the qtconsole""" # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. from qtpy import QtCore, QtGui, QtWidgets from .util import columnize class CompletionPlain(QtWidgets.QWidget): """ A widget for tab completion, navigable by arrow keys """ #-------------------------------------------------------------------------- # 'QObject' interface #-------------------------------------------------------------------------- def __init__(self, console_widget): """ Create a completion widget that is attached to the specified Qt text edit widget. """ assert isinstance(console_widget._control, (QtWidgets.QTextEdit, QtWidgets.QPlainTextEdit)) super().__init__() self._text_edit = console_widget._control self._console_widget = console_widget self._text_edit.installEventFilter(self) def eventFilter(self, obj, event): """ Reimplemented to handle keyboard input and to auto-hide when the text edit loses focus. """ if obj == self._text_edit: etype = event.type() if etype in( QtCore.QEvent.KeyPress, QtCore.QEvent.FocusOut ): self.cancel_completion() return super().eventFilter(obj, event) #-------------------------------------------------------------------------- # 'CompletionPlain' interface #-------------------------------------------------------------------------- def cancel_completion(self): """Cancel the completion, reseting internal variable, clearing buffer """ self._console_widget._clear_temporary_buffer() def show_items(self, cursor, items, prefix_length=0): """ Shows the completion widget with 'items' at the position specified by 'cursor'. """ if not items : return self.cancel_completion() strng = columnize(items) # Move cursor to start of the prefix to replace it # when a item is selected cursor.movePosition(QtGui.QTextCursor.Left, n=prefix_length) self._console_widget._fill_temporary_buffer(cursor, strng, html=False) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1667062695.0 qtconsole-5.5.2/qtconsole/completion_widget.py0000664000175000017500000001772700000000000022440 0ustar00carloscarlos00000000000000"""A dropdown completer widget for the qtconsole.""" import os import sys from qtpy import QT6 from qtpy import QtCore, QtGui, QtWidgets class CompletionWidget(QtWidgets.QListWidget): """ A widget for GUI tab completion. """ #-------------------------------------------------------------------------- # 'QObject' interface #-------------------------------------------------------------------------- def __init__(self, console_widget, height=0): """ Create a completion widget that is attached to the specified Qt text edit widget. """ text_edit = console_widget._control assert isinstance(text_edit, (QtWidgets.QTextEdit, QtWidgets.QPlainTextEdit)) super().__init__(parent=console_widget) self._text_edit = text_edit self._height_max = height if height > 0 else self.sizeHint().height() self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) # We need Popup style to ensure correct mouse interaction # (dialog would dissappear on mouse click with ToolTip style) self.setWindowFlags(QtCore.Qt.Popup) self.setAttribute(QtCore.Qt.WA_StaticContents) original_policy = text_edit.focusPolicy() self.setFocusPolicy(QtCore.Qt.NoFocus) text_edit.setFocusPolicy(original_policy) # Ensure that the text edit keeps focus when widget is displayed. self.setFocusProxy(self._text_edit) self.setFrameShadow(QtWidgets.QFrame.Plain) self.setFrameShape(QtWidgets.QFrame.StyledPanel) self.itemActivated.connect(self._complete_current) def eventFilter(self, obj, event): """ Reimplemented to handle mouse input and to auto-hide when the text edit loses focus. """ if obj is self: if event.type() == QtCore.QEvent.MouseButtonPress: pos = self.mapToGlobal(event.pos()) target = QtWidgets.QApplication.widgetAt(pos) if (target and self.isAncestorOf(target) or target is self): return False else: self.cancel_completion() return super().eventFilter(obj, event) def keyPressEvent(self, event): key = event.key() if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Tab): self._complete_current() elif key == QtCore.Qt.Key_Escape: self.hide() elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown, QtCore.Qt.Key_Home, QtCore.Qt.Key_End): return super().keyPressEvent(event) else: QtWidgets.QApplication.sendEvent(self._text_edit, event) #-------------------------------------------------------------------------- # 'QWidget' interface #-------------------------------------------------------------------------- def hideEvent(self, event): """ Reimplemented to disconnect signal handlers and event filter. """ super().hideEvent(event) try: self._text_edit.cursorPositionChanged.disconnect(self._update_current) except TypeError: pass self.removeEventFilter(self) def showEvent(self, event): """ Reimplemented to connect signal handlers and event filter. """ super().showEvent(event) self._text_edit.cursorPositionChanged.connect(self._update_current) self.installEventFilter(self) #-------------------------------------------------------------------------- # 'CompletionWidget' interface #-------------------------------------------------------------------------- def show_items(self, cursor, items, prefix_length=0): """ Shows the completion widget with 'items' at the position specified by 'cursor'. """ point = self._get_top_left_position(cursor) self.clear() path_items = [] for item in items: # Check if the item could refer to a file or dir. The replacing # of '"' is needed for items on Windows if (os.path.isfile(os.path.abspath(item.replace("\"", ""))) or os.path.isdir(os.path.abspath(item.replace("\"", "")))): path_items.append(item.replace("\"", "")) else: list_item = QtWidgets.QListWidgetItem() list_item.setData(QtCore.Qt.UserRole, item) # Need to split to only show last element of a dot completion list_item.setText(item.split(".")[-1]) self.addItem(list_item) common_prefix = os.path.dirname(os.path.commonprefix(path_items)) for path_item in path_items: list_item = QtWidgets.QListWidgetItem() list_item.setData(QtCore.Qt.UserRole, path_item) if common_prefix: text = path_item.split(common_prefix)[-1] else: text = path_item list_item.setText(text) self.addItem(list_item) if QT6: screen_rect = self.screen().availableGeometry() else: screen_rect = QtWidgets.QApplication.desktop().availableGeometry(self) screen_height = screen_rect.height() height = int(min(self._height_max, screen_height - 50)) # -50px if ((screen_height - point.y() - height) < 0): point = self._text_edit.mapToGlobal(self._text_edit.cursorRect().topRight()) py = point.y() point.setY(int(py - min(height, py - 10))) # -10px w = (self.sizeHintForColumn(0) + self.verticalScrollBar().sizeHint().width() + 2 * self.frameWidth()) self.setGeometry(point.x(), point.y(), w, height) # Move cursor to start of the prefix to replace it # when a item is selected cursor.movePosition(QtGui.QTextCursor.Left, n=prefix_length) self._start_position = cursor.position() self.setCurrentRow(0) self.raise_() self.show() #-------------------------------------------------------------------------- # Protected interface #-------------------------------------------------------------------------- def _get_top_left_position(self, cursor): """ Get top left position for this widget. """ return self._text_edit.mapToGlobal(self._text_edit.cursorRect().bottomRight()) def _complete_current(self): """ Perform the completion with the currently selected item. """ text = self.currentItem().data(QtCore.Qt.UserRole) self._current_text_cursor().insertText(text) self.hide() def _current_text_cursor(self): """ Returns a cursor with text between the start position and the current position selected. """ cursor = self._text_edit.textCursor() if cursor.position() >= self._start_position: cursor.setPosition(self._start_position, QtGui.QTextCursor.KeepAnchor) return cursor def _update_current(self): """ Updates the current item based on the current text and the position of the widget. """ # Update widget position cursor = self._text_edit.textCursor() point = self._get_top_left_position(cursor) point.setY(self.y()) self.move(point) # Update current item prefix = self._current_text_cursor().selection().toPlainText() if prefix: items = self.findItems(prefix, (QtCore.Qt.MatchStartsWith | QtCore.Qt.MatchCaseSensitive)) if items: self.setCurrentItem(items[0]) else: self.hide() else: self.hide() def cancel_completion(self): self.hide() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1699230380.0 qtconsole-5.5.2/qtconsole/console_widget.py0000664000175000017500000031443700000000000021727 0ustar00carloscarlos00000000000000"""An abstract base class for console-type widgets.""" # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. from functools import partial import os import os.path import re import sys from textwrap import dedent import time from unicodedata import category import webbrowser from qtpy import QT6 from qtpy import QtCore, QtGui, QtPrintSupport, QtWidgets from qtconsole.rich_text import HtmlExporter from qtconsole.util import MetaQObjectHasTraits, get_font, superQ from traitlets.config.configurable import LoggingConfigurable from traitlets import Bool, Enum, Integer, Unicode from .ansi_code_processor import QtAnsiCodeProcessor from .completion_widget import CompletionWidget from .completion_html import CompletionHtml from .completion_plain import CompletionPlain from .kill_ring import QtKillRing from .util import columnize def is_letter_or_number(char): """ Returns whether the specified unicode character is a letter or a number. """ cat = category(char) return cat.startswith('L') or cat.startswith('N') def is_whitespace(char): """Check whether a given char counts as white space.""" return category(char).startswith('Z') #----------------------------------------------------------------------------- # Classes #----------------------------------------------------------------------------- class ConsoleWidget(MetaQObjectHasTraits('NewBase', (LoggingConfigurable, superQ(QtWidgets.QWidget)), {})): """ An abstract base class for console-type widgets. This class has functionality for: * Maintaining a prompt and editing region * Providing the traditional Unix-style console keyboard shortcuts * Performing tab completion * Paging text * Handling ANSI escape codes ConsoleWidget also provides a number of utility methods that will be convenient to implementors of a console-style widget. """ #------ Configuration ------------------------------------------------------ ansi_codes = Bool(True, config=True, help="Whether to process ANSI escape codes." ) buffer_size = Integer(500, config=True, help=""" The maximum number of lines of text before truncation. Specifying a non-positive number disables text truncation (not recommended). """ ) execute_on_complete_input = Bool(True, config=True, help="""Whether to automatically execute on syntactically complete input. If False, Shift-Enter is required to submit each execution. Disabling this is mainly useful for non-Python kernels, where the completion check would be wrong. """ ) gui_completion = Enum(['plain', 'droplist', 'ncurses'], config=True, default_value = 'ncurses', help=""" The type of completer to use. Valid values are: 'plain' : Show the available completion as a text list Below the editing area. 'droplist': Show the completion in a drop down list navigable by the arrow keys, and from which you can select completion by pressing Return. 'ncurses' : Show the completion as a text list which is navigable by `tab` and arrow keys. """ ) gui_completion_height = Integer(0, config=True, help=""" Set Height for completion. 'droplist' Height in pixels. 'ncurses' Maximum number of rows. """ ) # NOTE: this value can only be specified during initialization. kind = Enum(['plain', 'rich'], default_value='plain', config=True, help=""" The type of underlying text widget to use. Valid values are 'plain', which specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit. """ ) # NOTE: this value can only be specified during initialization. paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'], default_value='inside', config=True, help=""" The type of paging to use. Valid values are: 'inside' The widget pages like a traditional terminal. 'hsplit' When paging is requested, the widget is split horizontally. The top pane contains the console, and the bottom pane contains the paged text. 'vsplit' Similar to 'hsplit', except that a vertical splitter is used. 'custom' No action is taken by the widget beyond emitting a 'custom_page_requested(str)' signal. 'none' The text is written directly to the console. """) scrollbar_visibility = Bool(True, config=True, help="""The visibility of the scrollar. If False then the scrollbar will be invisible.""" ) font_family = Unicode(config=True, help="""The font family to use for the console. On OSX this defaults to Monaco, on Windows the default is Consolas with fallback of Courier, and on other platforms the default is Monospace. """) def _font_family_default(self): if sys.platform == 'win32': # Consolas ships with Vista/Win7, fallback to Courier if needed return 'Consolas' elif sys.platform == 'darwin': # OSX always has Monaco, no need for a fallback return 'Monaco' else: # Monospace should always exist, no need for a fallback return 'Monospace' font_size = Integer(config=True, help="""The font size. If unconfigured, Qt will be entrusted with the size of the font. """) console_width = Integer(81, config=True, help="""The width of the console at start time in number of characters (will double with `hsplit` paging) """) console_height = Integer(25, config=True, help="""The height of the console at start time in number of characters (will double with `vsplit` paging) """) # Whether to override ShortcutEvents for the keybindings defined by this # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take # priority (when it has focus) over, e.g., window-level menu shortcuts. override_shortcuts = Bool(False) # ------ Custom Qt Widgets ------------------------------------------------- # For other projects to easily override the Qt widgets used by the console # (e.g. Spyder) custom_control = None custom_page_control = None #------ Signals ------------------------------------------------------------ # Signals that indicate ConsoleWidget state. copy_available = QtCore.Signal(bool) redo_available = QtCore.Signal(bool) undo_available = QtCore.Signal(bool) # Signal emitted when paging is needed and the paging style has been # specified as 'custom'. custom_page_requested = QtCore.Signal(object) # Signal emitted when the font is changed. font_changed = QtCore.Signal(QtGui.QFont) #------ Protected class variables ------------------------------------------ # control handles _control = None _page_control = None _splitter = None # When the control key is down, these keys are mapped. _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left, QtCore.Qt.Key_F : QtCore.Qt.Key_Right, QtCore.Qt.Key_A : QtCore.Qt.Key_Home, QtCore.Qt.Key_P : QtCore.Qt.Key_Up, QtCore.Qt.Key_N : QtCore.Qt.Key_Down, QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, } if not sys.platform == 'darwin': # On OS X, Ctrl-E already does the right thing, whereas End moves the # cursor to the bottom of the buffer. _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End # The shortcuts defined by this widget. We need to keep track of these to # support 'override_shortcuts' above. _shortcuts = set(_ctrl_down_remap.keys()) | \ { QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O, QtCore.Qt.Key_V } _temp_buffer_filled = False #--------------------------------------------------------------------------- # 'QObject' interface #--------------------------------------------------------------------------- def __init__(self, parent=None, **kw): """ Create a ConsoleWidget. Parameters ---------- parent : QWidget, optional [default None] The parent for this widget. """ super().__init__(**kw) if parent: self.setParent(parent) self._is_complete_msg_id = None self._is_complete_timeout = 0.1 self._is_complete_max_time = None # While scrolling the pager on Mac OS X, it tears badly. The # NativeGesture is platform and perhaps build-specific hence # we take adequate precautions here. self._pager_scroll_events = [QtCore.QEvent.Wheel] if hasattr(QtCore.QEvent, 'NativeGesture'): self._pager_scroll_events.append(QtCore.QEvent.NativeGesture) # Create the layout and underlying text widget. layout = QtWidgets.QStackedLayout(self) layout.setContentsMargins(0, 0, 0, 0) self._control = self._create_control() if self.paging in ('hsplit', 'vsplit'): self._splitter = QtWidgets.QSplitter() if self.paging == 'hsplit': self._splitter.setOrientation(QtCore.Qt.Horizontal) else: self._splitter.setOrientation(QtCore.Qt.Vertical) self._splitter.addWidget(self._control) layout.addWidget(self._splitter) else: layout.addWidget(self._control) # Create the paging widget, if necessary. if self.paging in ('inside', 'hsplit', 'vsplit'): self._page_control = self._create_page_control() if self._splitter: self._page_control.hide() self._splitter.addWidget(self._page_control) else: layout.addWidget(self._page_control) # Initialize protected variables. Some variables contain useful state # information for subclasses; they should be considered read-only. self._append_before_prompt_cursor = self._control.textCursor() self._ansi_processor = QtAnsiCodeProcessor() if self.gui_completion == 'ncurses': self._completion_widget = CompletionHtml(self, self.gui_completion_height) elif self.gui_completion == 'droplist': self._completion_widget = CompletionWidget(self, self.gui_completion_height) elif self.gui_completion == 'plain': self._completion_widget = CompletionPlain(self) self._continuation_prompt = '> ' self._continuation_prompt_html = None self._executing = False self._filter_resize = False self._html_exporter = HtmlExporter(self._control) self._input_buffer_executing = '' self._input_buffer_pending = '' self._kill_ring = QtKillRing(self._control) self._prompt = '' self._prompt_html = None self._prompt_cursor = self._control.textCursor() self._prompt_sep = '' self._reading = False self._reading_callback = None self._tab_width = 4 # List of strings pending to be appended as plain text in the widget. # The text is not immediately inserted when available to not # choke the Qt event loop with paint events for the widget in # case of lots of output from kernel. self._pending_insert_text = [] # Timer to flush the pending stream messages. The interval is adjusted # later based on actual time taken for flushing a screen (buffer_size) # of output text. self._pending_text_flush_interval = QtCore.QTimer(self._control) self._pending_text_flush_interval.setInterval(100) self._pending_text_flush_interval.setSingleShot(True) self._pending_text_flush_interval.timeout.connect( self._on_flush_pending_stream_timer) # Set a monospaced font. self.reset_font() # Configure actions. action = QtWidgets.QAction('Print', None) action.setEnabled(True) printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print) if printkey.matches("Ctrl+P") and sys.platform != 'darwin': # Only override the default if there is a collision. # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX. printkey = "Ctrl+Shift+P" action.setShortcut(printkey) action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.print_) self.addAction(action) self.print_action = action action = QtWidgets.QAction('Save as HTML/XML', None) action.setShortcut(QtGui.QKeySequence.Save) action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.export_html) self.addAction(action) self.export_action = action action = QtWidgets.QAction('Select All', None) action.setEnabled(True) selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll) if selectall.matches("Ctrl+A") and sys.platform != 'darwin': # Only override the default if there is a collision. # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX. selectall = "Ctrl+Shift+A" action.setShortcut(selectall) action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.select_all_smart) self.addAction(action) self.select_all_action = action self.increase_font_size = QtWidgets.QAction("Bigger Font", self, shortcut=QtGui.QKeySequence.ZoomIn, shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut, statusTip="Increase the font size by one point", triggered=self._increase_font_size) self.addAction(self.increase_font_size) self.decrease_font_size = QtWidgets.QAction("Smaller Font", self, shortcut=QtGui.QKeySequence.ZoomOut, shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut, statusTip="Decrease the font size by one point", triggered=self._decrease_font_size) self.addAction(self.decrease_font_size) self.reset_font_size = QtWidgets.QAction("Normal Font", self, shortcut="Ctrl+0", shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut, statusTip="Restore the Normal font size", triggered=self.reset_font) self.addAction(self.reset_font_size) # Accept drag and drop events here. Drops were already turned off # in self._control when that widget was created. self.setAcceptDrops(True) #--------------------------------------------------------------------------- # Drag and drop support #--------------------------------------------------------------------------- def dragEnterEvent(self, e): if e.mimeData().hasUrls(): # The link action should indicate to that the drop will insert # the file anme. e.setDropAction(QtCore.Qt.LinkAction) e.accept() elif e.mimeData().hasText(): # By changing the action to copy we don't need to worry about # the user accidentally moving text around in the widget. e.setDropAction(QtCore.Qt.CopyAction) e.accept() def dragMoveEvent(self, e): if e.mimeData().hasUrls(): pass elif e.mimeData().hasText(): cursor = self._control.cursorForPosition(e.pos()) if self._in_buffer(cursor.position()): e.setDropAction(QtCore.Qt.CopyAction) self._control.setTextCursor(cursor) else: e.setDropAction(QtCore.Qt.IgnoreAction) e.accept() def dropEvent(self, e): if e.mimeData().hasUrls(): self._keep_cursor_in_buffer() cursor = self._control.textCursor() filenames = [url.toLocalFile() for url in e.mimeData().urls()] text = ', '.join("'" + f.replace("'", "'\"'\"'") + "'" for f in filenames) self._insert_plain_text_into_buffer(cursor, text) elif e.mimeData().hasText(): cursor = self._control.cursorForPosition(e.pos()) if self._in_buffer(cursor.position()): text = e.mimeData().text() self._insert_plain_text_into_buffer(cursor, text) def eventFilter(self, obj, event): """ Reimplemented to ensure a console-like behavior in the underlying text widgets. """ etype = event.type() if etype == QtCore.QEvent.KeyPress: # Re-map keys for all filtered widgets. key = event.key() if self._control_key_down(event.modifiers()) and \ key in self._ctrl_down_remap: new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, self._ctrl_down_remap[key], QtCore.Qt.NoModifier) QtWidgets.QApplication.instance().sendEvent(obj, new_event) return True elif obj == self._control: return self._event_filter_console_keypress(event) elif obj == self._page_control: return self._event_filter_page_keypress(event) # Make middle-click paste safe. elif getattr(event, 'button', False) and \ etype == QtCore.QEvent.MouseButtonRelease and \ event.button() == QtCore.Qt.MiddleButton and \ obj == self._control.viewport(): cursor = self._control.cursorForPosition(event.pos()) self._control.setTextCursor(cursor) self.paste(QtGui.QClipboard.Selection) return True # Manually adjust the scrollbars *after* a resize event is dispatched. elif etype == QtCore.QEvent.Resize and not self._filter_resize: self._filter_resize = True QtWidgets.QApplication.instance().sendEvent(obj, event) self._adjust_scrollbars() self._filter_resize = False return True # Override shortcuts for all filtered widgets. elif etype == QtCore.QEvent.ShortcutOverride and \ self.override_shortcuts and \ self._control_key_down(event.modifiers()) and \ event.key() in self._shortcuts: event.accept() # Handle scrolling of the vsplit pager. This hack attempts to solve # problems with tearing of the help text inside the pager window. This # happens only on Mac OS X with both PySide and PyQt. This fix isn't # perfect but makes the pager more usable. elif etype in self._pager_scroll_events and \ obj == self._page_control: self._page_control.repaint() return True elif etype == QtCore.QEvent.MouseMove: anchor = self._control.anchorAt(event.pos()) if QT6: pos = event.globalPosition().toPoint() else: pos = event.globalPos() QtWidgets.QToolTip.showText(pos, anchor) return super().eventFilter(obj, event) #--------------------------------------------------------------------------- # 'QWidget' interface #--------------------------------------------------------------------------- def sizeHint(self): """ Reimplemented to suggest a size that is 80 characters wide and 25 lines high. """ font_metrics = QtGui.QFontMetrics(self.font) margin = (self._control.frameWidth() + self._control.document().documentMargin()) * 2 style = self.style() splitwidth = style.pixelMetric(QtWidgets.QStyle.PM_SplitterWidth) # Note 1: Despite my best efforts to take the various margins into # account, the width is still coming out a bit too small, so we include # a fudge factor of one character here. # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due # to a Qt bug on certain Mac OS systems where it returns 0. width = self._get_font_width() * self.console_width + margin width += style.pixelMetric(QtWidgets.QStyle.PM_ScrollBarExtent) if self.paging == 'hsplit': width = width * 2 + splitwidth height = font_metrics.height() * self.console_height + margin if self.paging == 'vsplit': height = height * 2 + splitwidth return QtCore.QSize(int(width), int(height)) #--------------------------------------------------------------------------- # 'ConsoleWidget' public interface #--------------------------------------------------------------------------- include_other_output = Bool(False, config=True, help="""Whether to include output from clients other than this one sharing the same kernel. Outputs are not displayed until enter is pressed. """ ) other_output_prefix = Unicode('[remote] ', config=True, help="""Prefix to add to outputs coming from clients other than this one. Only relevant if include_other_output is True. """ ) def can_copy(self): """ Returns whether text can be copied to the clipboard. """ return self._control.textCursor().hasSelection() def can_cut(self): """ Returns whether text can be cut to the clipboard. """ cursor = self._control.textCursor() return (cursor.hasSelection() and self._in_buffer(cursor.anchor()) and self._in_buffer(cursor.position())) def can_paste(self): """ Returns whether text can be pasted from the clipboard. """ if self._control.textInteractionFlags() & QtCore.Qt.TextEditable: return bool(QtWidgets.QApplication.clipboard().text()) return False def clear(self, keep_input=True): """ Clear the console. Parameters ---------- keep_input : bool, optional (default True) If set, restores the old input buffer if a new prompt is written. """ if self._executing: self._control.clear() else: if keep_input: input_buffer = self.input_buffer self._control.clear() self._show_prompt() if keep_input: self.input_buffer = input_buffer def copy(self): """ Copy the currently selected text to the clipboard. """ self.layout().currentWidget().copy() def copy_anchor(self, anchor): """ Copy anchor text to the clipboard """ QtWidgets.QApplication.clipboard().setText(anchor) def cut(self): """ Copy the currently selected text to the clipboard and delete it if it's inside the input buffer. """ self.copy() if self.can_cut(): self._control.textCursor().removeSelectedText() def _handle_is_complete_reply(self, msg): if msg['parent_header'].get('msg_id', 0) != self._is_complete_msg_id: return status = msg['content'].get('status', 'complete') indent = msg['content'].get('indent', '') self._trigger_is_complete_callback(status != 'incomplete', indent) def _trigger_is_complete_callback(self, complete=False, indent=''): if self._is_complete_msg_id is not None: self._is_complete_msg_id = None self._is_complete_callback(complete, indent) def _register_is_complete_callback(self, source, callback): if self._is_complete_msg_id is not None: if self._is_complete_max_time < time.time(): # Second return while waiting for is_complete return else: # request timed out self._trigger_is_complete_callback() self._is_complete_max_time = time.time() + self._is_complete_timeout self._is_complete_callback = callback self._is_complete_msg_id = self.kernel_client.is_complete(source) def execute(self, source=None, hidden=False, interactive=False): """ Executes source or the input buffer, possibly prompting for more input. Parameters ---------- source : str, optional The source to execute. If not specified, the input buffer will be used. If specified and 'hidden' is False, the input buffer will be replaced with the source before execution. hidden : bool, optional (default False) If set, no output will be shown and the prompt will not be modified. In other words, it will be completely invisible to the user that an execution has occurred. interactive : bool, optional (default False) Whether the console is to treat the source as having been manually entered by the user. The effect of this parameter depends on the subclass implementation. Raises ------ RuntimeError If incomplete input is given and 'hidden' is True. In this case, it is not possible to prompt for more input. Returns ------- A boolean indicating whether the source was executed. """ # WARNING: The order in which things happen here is very particular, in # large part because our syntax highlighting is fragile. If you change # something, test carefully! # Decide what to execute. if source is None: source = self.input_buffer elif not hidden: self.input_buffer = source if hidden: self._execute(source, hidden) # Execute the source or show a continuation prompt if it is incomplete. elif interactive and self.execute_on_complete_input: self._register_is_complete_callback( source, partial(self.do_execute, source)) else: self.do_execute(source, True, '') def do_execute(self, source, complete, indent): if complete: self._append_plain_text('\n') self._input_buffer_executing = self.input_buffer self._executing = True self._finalize_input_request() # Perform actual execution. self._execute(source, False) else: # Do this inside an edit block so continuation prompts are # removed seamlessly via undo/redo. cursor = self._get_end_cursor() cursor.beginEditBlock() try: cursor.insertText('\n') self._insert_continuation_prompt(cursor, indent) finally: cursor.endEditBlock() # Do not do this inside the edit block. It works as expected # when using a QPlainTextEdit control, but does not have an # effect when using a QTextEdit. I believe this is a Qt bug. self._control.moveCursor(QtGui.QTextCursor.End) def export_html(self): """ Shows a dialog to export HTML/XML in various formats. """ self._html_exporter.export() def _finalize_input_request(self): """ Set the widget to a non-reading state. """ # Must set _reading to False before calling _prompt_finished self._reading = False self._prompt_finished() # There is no prompt now, so before_prompt_position is eof self._append_before_prompt_cursor.setPosition( self._get_end_cursor().position()) # The maximum block count is only in effect during execution. # This ensures that _prompt_pos does not become invalid due to # text truncation. self._control.document().setMaximumBlockCount(self.buffer_size) # Setting a positive maximum block count will automatically # disable the undo/redo history, but just to be safe: self._control.setUndoRedoEnabled(False) def _get_input_buffer(self, force=False): """ The text that the user has entered entered at the current prompt. If the console is currently executing, the text that is executing will always be returned. """ # If we're executing, the input buffer may not even exist anymore due to # the limit imposed by 'buffer_size'. Therefore, we store it. if self._executing and not force: return self._input_buffer_executing cursor = self._get_end_cursor() cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor) input_buffer = cursor.selection().toPlainText() # Strip out continuation prompts. return input_buffer.replace('\n' + self._continuation_prompt, '\n') def _set_input_buffer(self, string): """ Sets the text in the input buffer. If the console is currently executing, this call has no *immediate* effect. When the execution is finished, the input buffer will be updated appropriately. """ # If we're executing, store the text for later. if self._executing: self._input_buffer_pending = string return # Remove old text. cursor = self._get_end_cursor() cursor.beginEditBlock() cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor) cursor.removeSelectedText() # Insert new text with continuation prompts. self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string) cursor.endEditBlock() self._control.moveCursor(QtGui.QTextCursor.End) input_buffer = property(_get_input_buffer, _set_input_buffer) def _get_font(self): """ The base font being used by the ConsoleWidget. """ return self._control.document().defaultFont() def _get_font_width(self, font=None): if font is None: font = self.font font_metrics = QtGui.QFontMetrics(font) if hasattr(font_metrics, 'horizontalAdvance'): return font_metrics.horizontalAdvance(' ') else: return font_metrics.width(' ') def _set_font(self, font): """ Sets the base font for the ConsoleWidget to the specified QFont. """ self._control.setTabStopWidth( self.tab_width * self._get_font_width(font) ) self._completion_widget.setFont(font) self._control.document().setDefaultFont(font) if self._page_control: self._page_control.document().setDefaultFont(font) self.font_changed.emit(font) font = property(_get_font, _set_font) def _set_completion_widget(self, gui_completion): """ Set gui completion widget. """ if gui_completion == 'ncurses': self._completion_widget = CompletionHtml(self) elif gui_completion == 'droplist': self._completion_widget = CompletionWidget(self) elif gui_completion == 'plain': self._completion_widget = CompletionPlain(self) self.gui_completion = gui_completion def open_anchor(self, anchor): """ Open selected anchor in the default webbrowser """ webbrowser.open( anchor ) def paste(self, mode=QtGui.QClipboard.Clipboard): """ Paste the contents of the clipboard into the input region. Parameters ---------- mode : QClipboard::Mode, optional [default QClipboard::Clipboard] Controls which part of the system clipboard is used. This can be used to access the selection clipboard in X11 and the Find buffer in Mac OS. By default, the regular clipboard is used. """ if self._control.textInteractionFlags() & QtCore.Qt.TextEditable: # Make sure the paste is safe. self._keep_cursor_in_buffer() cursor = self._control.textCursor() # Remove any trailing newline, which confuses the GUI and forces the # user to backspace. text = QtWidgets.QApplication.clipboard().text(mode).rstrip() # dedent removes "common leading whitespace" but to preserve relative # indent of multiline code, we have to compensate for any # leading space on the first line, if we're pasting into # an indented position. cursor_offset = cursor.position() - self._get_line_start_pos() if text.startswith(' ' * cursor_offset): text = text[cursor_offset:] self._insert_plain_text_into_buffer(cursor, dedent(text)) def print_(self, printer = None): """ Print the contents of the ConsoleWidget to the specified QPrinter. """ if (not printer): printer = QtPrintSupport.QPrinter() if(QtPrintSupport.QPrintDialog(printer).exec_() != QtPrintSupport.QPrintDialog.Accepted): return self._control.print_(printer) def prompt_to_top(self): """ Moves the prompt to the top of the viewport. """ if not self._executing: prompt_cursor = self._get_prompt_cursor() if self._get_cursor().blockNumber() < prompt_cursor.blockNumber(): self._set_cursor(prompt_cursor) self._set_top_cursor(prompt_cursor) def redo(self): """ Redo the last operation. If there is no operation to redo, nothing happens. """ self._control.redo() def reset_font(self): """ Sets the font to the default fixed-width font for this platform. """ if sys.platform == 'win32': # Consolas ships with Vista/Win7, fallback to Courier if needed fallback = 'Courier' elif sys.platform == 'darwin': # OSX always has Monaco fallback = 'Monaco' else: # Monospace should always exist fallback = 'Monospace' font = get_font(self.font_family, fallback) if self.font_size: font.setPointSize(self.font_size) else: font.setPointSize(QtWidgets.QApplication.instance().font().pointSize()) font.setStyleHint(QtGui.QFont.TypeWriter) self._set_font(font) def change_font_size(self, delta): """Change the font size by the specified amount (in points). """ font = self.font size = max(font.pointSize() + delta, 1) # minimum 1 point font.setPointSize(size) self._set_font(font) def _increase_font_size(self): self.change_font_size(1) def _decrease_font_size(self): self.change_font_size(-1) def select_all_smart(self): """ Select current cell, or, if already selected, the whole document. """ c = self._get_cursor() sel_range = c.selectionStart(), c.selectionEnd() c.clearSelection() c.setPosition(self._get_prompt_cursor().position()) c.setPosition(self._get_end_pos(), mode=QtGui.QTextCursor.KeepAnchor) new_sel_range = c.selectionStart(), c.selectionEnd() if sel_range == new_sel_range: # cell already selected, expand selection to whole document self.select_document() else: # set cell selection as active selection self._control.setTextCursor(c) def select_document(self): """ Selects all the text in the buffer. """ self._control.selectAll() def _get_tab_width(self): """ The width (in terms of space characters) for tab characters. """ return self._tab_width def _set_tab_width(self, tab_width): """ Sets the width (in terms of space characters) for tab characters. """ self._control.setTabStopWidth(tab_width * self._get_font_width()) self._tab_width = tab_width tab_width = property(_get_tab_width, _set_tab_width) def undo(self): """ Undo the last operation. If there is no operation to undo, nothing happens. """ self._control.undo() #--------------------------------------------------------------------------- # 'ConsoleWidget' abstract interface #--------------------------------------------------------------------------- def _is_complete(self, source, interactive): """ Returns whether 'source' can be executed. When triggered by an Enter/Return key press, 'interactive' is True; otherwise, it is False. """ raise NotImplementedError def _execute(self, source, hidden): """ Execute 'source'. If 'hidden', do not show any output. """ raise NotImplementedError def _prompt_started_hook(self): """ Called immediately after a new prompt is displayed. """ pass def _prompt_finished_hook(self): """ Called immediately after a prompt is finished, i.e. when some input will be processed and a new prompt displayed. """ pass def _up_pressed(self, shift_modifier): """ Called when the up key is pressed. Returns whether to continue processing the event. """ return True def _down_pressed(self, shift_modifier): """ Called when the down key is pressed. Returns whether to continue processing the event. """ return True def _tab_pressed(self): """ Called when the tab key is pressed. Returns whether to continue processing the event. """ return True #-------------------------------------------------------------------------- # 'ConsoleWidget' protected interface #-------------------------------------------------------------------------- def _append_custom(self, insert, input, before_prompt=False, *args, **kwargs): """ A low-level method for appending content to the end of the buffer. If 'before_prompt' is enabled, the content will be inserted before the current prompt, if there is one. """ # Determine where to insert the content. cursor = self._control.textCursor() if before_prompt and (self._reading or not self._executing): self._flush_pending_stream() cursor._insert_mode=True cursor.setPosition(self._append_before_prompt_pos) else: if insert != self._insert_plain_text: self._flush_pending_stream() cursor.movePosition(QtGui.QTextCursor.End) # Perform the insertion. result = insert(cursor, input, *args, **kwargs) return result def _append_block(self, block_format=None, before_prompt=False): """ Appends an new QTextBlock to the end of the console buffer. """ self._append_custom(self._insert_block, block_format, before_prompt) def _append_html(self, html, before_prompt=False): """ Appends HTML at the end of the console buffer. """ self._append_custom(self._insert_html, html, before_prompt) def _append_html_fetching_plain_text(self, html, before_prompt=False): """ Appends HTML, then returns the plain text version of it. """ return self._append_custom(self._insert_html_fetching_plain_text, html, before_prompt) def _append_plain_text(self, text, before_prompt=False): """ Appends plain text, processing ANSI codes if enabled. """ self._append_custom(self._insert_plain_text, text, before_prompt) def _cancel_completion(self): """ If text completion is progress, cancel it. """ self._completion_widget.cancel_completion() def _clear_temporary_buffer(self): """ Clears the "temporary text" buffer, i.e. all the text following the prompt region. """ # Select and remove all text below the input buffer. cursor = self._get_prompt_cursor() prompt = self._continuation_prompt.lstrip() if(self._temp_buffer_filled): self._temp_buffer_filled = False while cursor.movePosition(QtGui.QTextCursor.NextBlock): temp_cursor = QtGui.QTextCursor(cursor) temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor) text = temp_cursor.selection().toPlainText().lstrip() if not text.startswith(prompt): break else: # We've reached the end of the input buffer and no text follows. return cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline. cursor.movePosition(QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor) cursor.removeSelectedText() # After doing this, we have no choice but to clear the undo/redo # history. Otherwise, the text is not "temporary" at all, because it # can be recalled with undo/redo. Unfortunately, Qt does not expose # fine-grained control to the undo/redo system. if self._control.isUndoRedoEnabled(): self._control.setUndoRedoEnabled(False) self._control.setUndoRedoEnabled(True) def _complete_with_items(self, cursor, items): """ Performs completion with 'items' at the specified cursor location. """ self._cancel_completion() if len(items) == 1: cursor.setPosition(self._control.textCursor().position(), QtGui.QTextCursor.KeepAnchor) cursor.insertText(items[0]) elif len(items) > 1: current_pos = self._control.textCursor().position() prefix = os.path.commonprefix(items) if prefix: cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor) cursor.insertText(prefix) current_pos = cursor.position() self._completion_widget.show_items(cursor, items, prefix_length=len(prefix)) def _fill_temporary_buffer(self, cursor, text, html=False): """fill the area below the active editting zone with text""" current_pos = self._control.textCursor().position() cursor.beginEditBlock() self._append_plain_text('\n') self._page(text, html=html) cursor.endEditBlock() cursor.setPosition(current_pos) self._control.moveCursor(QtGui.QTextCursor.End) self._control.setTextCursor(cursor) self._temp_buffer_filled = True def _context_menu_make(self, pos): """ Creates a context menu for the given QPoint (in widget coordinates). """ menu = QtWidgets.QMenu(self) self.cut_action = menu.addAction('Cut', self.cut) self.cut_action.setEnabled(self.can_cut()) self.cut_action.setShortcut(QtGui.QKeySequence.Cut) self.copy_action = menu.addAction('Copy', self.copy) self.copy_action.setEnabled(self.can_copy()) self.copy_action.setShortcut(QtGui.QKeySequence.Copy) self.paste_action = menu.addAction('Paste', self.paste) self.paste_action.setEnabled(self.can_paste()) self.paste_action.setShortcut(QtGui.QKeySequence.Paste) anchor = self._control.anchorAt(pos) if anchor: menu.addSeparator() self.copy_link_action = menu.addAction( 'Copy Link Address', lambda: self.copy_anchor(anchor=anchor)) self.open_link_action = menu.addAction( 'Open Link', lambda: self.open_anchor(anchor=anchor)) menu.addSeparator() menu.addAction(self.select_all_action) menu.addSeparator() menu.addAction(self.export_action) menu.addAction(self.print_action) return menu def _control_key_down(self, modifiers, include_command=False): """ Given a KeyboardModifiers flags object, return whether the Control key is down. Parameters ---------- include_command : bool, optional (default True) Whether to treat the Command key as a (mutually exclusive) synonym for Control when in Mac OS. """ # Note that on Mac OS, ControlModifier corresponds to the Command key # while MetaModifier corresponds to the Control key. if sys.platform == 'darwin': down = include_command and (modifiers & QtCore.Qt.ControlModifier) return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier) else: return bool(modifiers & QtCore.Qt.ControlModifier) def _create_control(self): """ Creates and connects the underlying text widget. """ # Create the underlying control. if self.custom_control: control = self.custom_control() elif self.kind == 'plain': control = QtWidgets.QPlainTextEdit() elif self.kind == 'rich': control = QtWidgets.QTextEdit() control.setAcceptRichText(False) control.setMouseTracking(True) # Prevent the widget from handling drops, as we already provide # the logic in this class. control.setAcceptDrops(False) # Install event filters. The filter on the viewport is needed for # mouse events. control.installEventFilter(self) control.viewport().installEventFilter(self) # Connect signals. control.customContextMenuRequested.connect( self._custom_context_menu_requested) control.copyAvailable.connect(self.copy_available) control.redoAvailable.connect(self.redo_available) control.undoAvailable.connect(self.undo_available) # Hijack the document size change signal to prevent Qt from adjusting # the viewport's scrollbar. We are relying on an implementation detail # of Q(Plain)TextEdit here, which is potentially dangerous, but without # this functionality we cannot create a nice terminal interface. layout = control.document().documentLayout() layout.documentSizeChanged.disconnect() layout.documentSizeChanged.connect(self._adjust_scrollbars) # Configure the scrollbar policy if self.scrollbar_visibility: scrollbar_policy = QtCore.Qt.ScrollBarAlwaysOn else : scrollbar_policy = QtCore.Qt.ScrollBarAlwaysOff # Configure the control. control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True) control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) control.setReadOnly(True) control.setUndoRedoEnabled(False) control.setVerticalScrollBarPolicy(scrollbar_policy) return control def _create_page_control(self): """ Creates and connects the underlying paging widget. """ if self.custom_page_control: control = self.custom_page_control() elif self.kind == 'plain': control = QtWidgets.QPlainTextEdit() elif self.kind == 'rich': control = QtWidgets.QTextEdit() control.installEventFilter(self) viewport = control.viewport() viewport.installEventFilter(self) control.setReadOnly(True) control.setUndoRedoEnabled(False) # Configure the scrollbar policy if self.scrollbar_visibility: scrollbar_policy = QtCore.Qt.ScrollBarAlwaysOn else : scrollbar_policy = QtCore.Qt.ScrollBarAlwaysOff control.setVerticalScrollBarPolicy(scrollbar_policy) return control def _event_filter_console_keypress(self, event): """ Filter key events for the underlying text widget to create a console-like interface. """ intercepted = False cursor = self._control.textCursor() position = cursor.position() key = event.key() ctrl_down = self._control_key_down(event.modifiers()) alt_down = event.modifiers() & QtCore.Qt.AltModifier shift_down = event.modifiers() & QtCore.Qt.ShiftModifier cmd_down = ( sys.platform == "darwin" and self._control_key_down(event.modifiers(), include_command=True) ) if cmd_down: if key == QtCore.Qt.Key_Left: key = QtCore.Qt.Key_Home elif key == QtCore.Qt.Key_Right: key = QtCore.Qt.Key_End elif key == QtCore.Qt.Key_Up: ctrl_down = True key = QtCore.Qt.Key_Home elif key == QtCore.Qt.Key_Down: ctrl_down = True key = QtCore.Qt.Key_End #------ Special modifier logic ----------------------------------------- if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): intercepted = True # Special handling when tab completing in text mode. self._cancel_completion() if self._in_buffer(position): # Special handling when a reading a line of raw input. if self._reading: self._append_plain_text('\n') self._reading = False if self._reading_callback: self._reading_callback() # If the input buffer is a single line or there is only # whitespace after the cursor, execute. Otherwise, split the # line with a continuation prompt. elif not self._executing: cursor.movePosition(QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor) at_end = len(cursor.selectedText().strip()) == 0 single_line = (self._get_end_cursor().blockNumber() == self._get_prompt_cursor().blockNumber()) if (at_end or shift_down or single_line) and not ctrl_down: self.execute(interactive = not shift_down) else: # Do this inside an edit block for clean undo/redo. pos = self._get_input_buffer_cursor_pos() def callback(complete, indent): try: cursor.beginEditBlock() cursor.setPosition(position) cursor.insertText('\n') self._insert_continuation_prompt(cursor) if indent: cursor.insertText(indent) finally: cursor.endEditBlock() # Ensure that the whole input buffer is visible. # FIXME: This will not be usable if the input buffer is # taller than the console widget. self._control.moveCursor(QtGui.QTextCursor.End) self._control.setTextCursor(cursor) self._register_is_complete_callback( self._get_input_buffer()[:pos], callback) #------ Control/Cmd modifier ------------------------------------------- elif ctrl_down: if key == QtCore.Qt.Key_G: self._keyboard_quit() intercepted = True elif key == QtCore.Qt.Key_K: if self._in_buffer(position): cursor.clearSelection() cursor.movePosition(QtGui.QTextCursor.EndOfLine, QtGui.QTextCursor.KeepAnchor) if not cursor.hasSelection(): # Line deletion (remove continuation prompt) cursor.movePosition(QtGui.QTextCursor.NextBlock, QtGui.QTextCursor.KeepAnchor) cursor.movePosition(QtGui.QTextCursor.Right, QtGui.QTextCursor.KeepAnchor, len(self._continuation_prompt)) self._kill_ring.kill_cursor(cursor) self._set_cursor(cursor) intercepted = True elif key == QtCore.Qt.Key_L: self.prompt_to_top() intercepted = True elif key == QtCore.Qt.Key_O: if self._page_control and self._page_control.isVisible(): self._page_control.setFocus() intercepted = True elif key == QtCore.Qt.Key_U: if self._in_buffer(position): cursor.clearSelection() start_line = cursor.blockNumber() if start_line == self._get_prompt_cursor().blockNumber(): offset = len(self._prompt) else: offset = len(self._continuation_prompt) cursor.movePosition(QtGui.QTextCursor.StartOfBlock, QtGui.QTextCursor.KeepAnchor) cursor.movePosition(QtGui.QTextCursor.Right, QtGui.QTextCursor.KeepAnchor, offset) self._kill_ring.kill_cursor(cursor) self._set_cursor(cursor) intercepted = True elif key == QtCore.Qt.Key_Y: self._keep_cursor_in_buffer() self._kill_ring.yank() intercepted = True elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete): if key == QtCore.Qt.Key_Backspace: cursor = self._get_word_start_cursor(position) else: # key == QtCore.Qt.Key_Delete cursor = self._get_word_end_cursor(position) cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor) self._kill_ring.kill_cursor(cursor) intercepted = True elif key == QtCore.Qt.Key_D: if len(self.input_buffer) == 0 and not self._executing: self.exit_requested.emit(self) # if executing and input buffer empty elif len(self._get_input_buffer(force=True)) == 0: # input a EOT ansi control character self._control.textCursor().insertText(chr(4)) new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_Return, QtCore.Qt.NoModifier) QtWidgets.QApplication.instance().sendEvent(self._control, new_event) intercepted = True else: new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_Delete, QtCore.Qt.NoModifier) QtWidgets.QApplication.instance().sendEvent(self._control, new_event) intercepted = True elif key == QtCore.Qt.Key_Down: self._scroll_to_end() elif key == QtCore.Qt.Key_Up: self._control.verticalScrollBar().setValue(0) #------ Alt modifier --------------------------------------------------- elif alt_down: if key == QtCore.Qt.Key_B: self._set_cursor(self._get_word_start_cursor(position)) intercepted = True elif key == QtCore.Qt.Key_F: self._set_cursor(self._get_word_end_cursor(position)) intercepted = True elif key == QtCore.Qt.Key_Y: self._kill_ring.rotate() intercepted = True elif key == QtCore.Qt.Key_Backspace: cursor = self._get_word_start_cursor(position) cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor) self._kill_ring.kill_cursor(cursor) intercepted = True elif key == QtCore.Qt.Key_D: cursor = self._get_word_end_cursor(position) cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor) self._kill_ring.kill_cursor(cursor) intercepted = True elif key == QtCore.Qt.Key_Delete: intercepted = True elif key == QtCore.Qt.Key_Greater: self._control.moveCursor(QtGui.QTextCursor.End) intercepted = True elif key == QtCore.Qt.Key_Less: self._control.setTextCursor(self._get_prompt_cursor()) intercepted = True #------ No modifiers --------------------------------------------------- else: self._trigger_is_complete_callback() if shift_down: anchormode = QtGui.QTextCursor.KeepAnchor else: anchormode = QtGui.QTextCursor.MoveAnchor if key == QtCore.Qt.Key_Escape: self._keyboard_quit() intercepted = True elif key == QtCore.Qt.Key_Up and not shift_down: if self._reading or not self._up_pressed(shift_down): intercepted = True else: prompt_line = self._get_prompt_cursor().blockNumber() intercepted = cursor.blockNumber() <= prompt_line elif key == QtCore.Qt.Key_Down and not shift_down: if self._reading or not self._down_pressed(shift_down): intercepted = True else: end_line = self._get_end_cursor().blockNumber() intercepted = cursor.blockNumber() == end_line elif key == QtCore.Qt.Key_Tab: if not self._reading: if self._tab_pressed(): self._indent(dedent=False) intercepted = True elif key == QtCore.Qt.Key_Backtab: self._indent(dedent=True) intercepted = True elif key == QtCore.Qt.Key_Left and not shift_down: # Move to the previous line line, col = cursor.blockNumber(), cursor.columnNumber() if line > self._get_prompt_cursor().blockNumber() and \ col == len(self._continuation_prompt): self._control.moveCursor(QtGui.QTextCursor.PreviousBlock, mode=anchormode) self._control.moveCursor(QtGui.QTextCursor.EndOfBlock, mode=anchormode) intercepted = True # Regular left movement else: intercepted = not self._in_buffer(position - 1) elif key == QtCore.Qt.Key_Right and not shift_down: #original_block_number = cursor.blockNumber() if position == self._get_line_end_pos(): cursor.movePosition(QtGui.QTextCursor.NextBlock, mode=anchormode) cursor.movePosition(QtGui.QTextCursor.Right, mode=anchormode, n=len(self._continuation_prompt)) self._control.setTextCursor(cursor) else: self._control.moveCursor(QtGui.QTextCursor.Right, mode=anchormode) intercepted = True elif key == QtCore.Qt.Key_Home: start_pos = self._get_line_start_pos() c = self._get_cursor() spaces = self._get_leading_spaces() if (c.position() > start_pos + spaces or c.columnNumber() == len(self._continuation_prompt)): start_pos += spaces # Beginning of text if shift_down and self._in_buffer(position): if c.selectedText(): sel_max = max(c.selectionStart(), c.selectionEnd()) cursor.setPosition(sel_max, QtGui.QTextCursor.MoveAnchor) cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor) else: cursor.setPosition(start_pos) self._set_cursor(cursor) intercepted = True elif key == QtCore.Qt.Key_Backspace: # Line deletion (remove continuation prompt) line, col = cursor.blockNumber(), cursor.columnNumber() if not self._reading and \ col == len(self._continuation_prompt) and \ line > self._get_prompt_cursor().blockNumber(): cursor.beginEditBlock() cursor.movePosition(QtGui.QTextCursor.StartOfBlock, QtGui.QTextCursor.KeepAnchor) cursor.removeSelectedText() cursor.deletePreviousChar() cursor.endEditBlock() intercepted = True # Regular backwards deletion else: anchor = cursor.anchor() if anchor == position: intercepted = not self._in_buffer(position - 1) else: intercepted = not self._in_buffer(min(anchor, position)) elif key == QtCore.Qt.Key_Delete: # Line deletion (remove continuation prompt) if not self._reading and self._in_buffer(position) and \ cursor.atBlockEnd() and not cursor.hasSelection(): cursor.movePosition(QtGui.QTextCursor.NextBlock, QtGui.QTextCursor.KeepAnchor) cursor.movePosition(QtGui.QTextCursor.Right, QtGui.QTextCursor.KeepAnchor, len(self._continuation_prompt)) cursor.removeSelectedText() intercepted = True # Regular forwards deletion: else: anchor = cursor.anchor() intercepted = (not self._in_buffer(anchor) or not self._in_buffer(position)) #------ Special sequences ---------------------------------------------- if not intercepted: if event.matches(QtGui.QKeySequence.Copy): self.copy() intercepted = True elif event.matches(QtGui.QKeySequence.Cut): self.cut() intercepted = True elif event.matches(QtGui.QKeySequence.Paste): self.paste() intercepted = True # Don't move the cursor if Control/Cmd is pressed to allow copy-paste # using the keyboard in any part of the buffer. Also, permit scrolling # with Page Up/Down keys. Finally, if we're executing, don't move the # cursor (if even this made sense, we can't guarantee that the prompt # position is still valid due to text truncation). if not (self._control_key_down(event.modifiers(), include_command=True) or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown) or (self._executing and not self._reading) or (event.text() == "" and not (not shift_down and key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down)))): self._keep_cursor_in_buffer() return intercepted def _event_filter_page_keypress(self, event): """ Filter key events for the paging widget to create console-like interface. """ key = event.key() ctrl_down = self._control_key_down(event.modifiers()) alt_down = event.modifiers() & QtCore.Qt.AltModifier if ctrl_down: if key == QtCore.Qt.Key_O: self._control.setFocus() return True elif alt_down: if key == QtCore.Qt.Key_Greater: self._page_control.moveCursor(QtGui.QTextCursor.End) return True elif key == QtCore.Qt.Key_Less: self._page_control.moveCursor(QtGui.QTextCursor.Start) return True elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape): if self._splitter: self._page_control.hide() self._control.setFocus() else: self.layout().setCurrentWidget(self._control) # re-enable buffer truncation after paging self._control.document().setMaximumBlockCount(self.buffer_size) return True elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return, QtCore.Qt.Key_Tab): new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_PageDown, QtCore.Qt.NoModifier) QtWidgets.QApplication.instance().sendEvent(self._page_control, new_event) return True elif key == QtCore.Qt.Key_Backspace: new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_PageUp, QtCore.Qt.NoModifier) QtWidgets.QApplication.instance().sendEvent(self._page_control, new_event) return True # vi/less -like key bindings elif key == QtCore.Qt.Key_J: new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_Down, QtCore.Qt.NoModifier) QtWidgets.QApplication.instance().sendEvent(self._page_control, new_event) return True # vi/less -like key bindings elif key == QtCore.Qt.Key_K: new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_Up, QtCore.Qt.NoModifier) QtWidgets.QApplication.instance().sendEvent(self._page_control, new_event) return True return False def _on_flush_pending_stream_timer(self): """ Flush the pending stream output and change the prompt position appropriately. """ cursor = self._control.textCursor() cursor.movePosition(QtGui.QTextCursor.End) self._flush_pending_stream() cursor.movePosition(QtGui.QTextCursor.End) def _flush_pending_stream(self): """ Flush out pending text into the widget. """ text = self._pending_insert_text self._pending_insert_text = [] buffer_size = self._control.document().maximumBlockCount() if buffer_size > 0: text = self._get_last_lines_from_list(text, buffer_size) text = ''.join(text) t = time.time() self._insert_plain_text(self._get_end_cursor(), text, flush=True) # Set the flush interval to equal the maximum time to update text. self._pending_text_flush_interval.setInterval( int(max(100, (time.time() - t) * 1000)) ) def _get_cursor(self): """ Get a cursor at the current insert position. """ return self._control.textCursor() def _get_end_cursor(self): """ Get a cursor at the last character of the current cell. """ cursor = self._control.textCursor() cursor.movePosition(QtGui.QTextCursor.End) return cursor def _get_end_pos(self): """ Get the position of the last character of the current cell. """ return self._get_end_cursor().position() def _get_line_start_cursor(self): """ Get a cursor at the first character of the current line. """ cursor = self._control.textCursor() start_line = cursor.blockNumber() if start_line == self._get_prompt_cursor().blockNumber(): cursor.setPosition(self._prompt_pos) else: cursor.movePosition(QtGui.QTextCursor.StartOfLine) cursor.setPosition(cursor.position() + len(self._continuation_prompt)) return cursor def _get_line_start_pos(self): """ Get the position of the first character of the current line. """ return self._get_line_start_cursor().position() def _get_line_end_cursor(self): """ Get a cursor at the last character of the current line. """ cursor = self._control.textCursor() cursor.movePosition(QtGui.QTextCursor.EndOfLine) return cursor def _get_line_end_pos(self): """ Get the position of the last character of the current line. """ return self._get_line_end_cursor().position() def _get_input_buffer_cursor_column(self): """ Get the column of the cursor in the input buffer, excluding the contribution by the prompt, or -1 if there is no such column. """ prompt = self._get_input_buffer_cursor_prompt() if prompt is None: return -1 else: cursor = self._control.textCursor() return cursor.columnNumber() - len(prompt) def _get_input_buffer_cursor_line(self): """ Get the text of the line of the input buffer that contains the cursor, or None if there is no such line. """ prompt = self._get_input_buffer_cursor_prompt() if prompt is None: return None else: cursor = self._control.textCursor() text = cursor.block().text() return text[len(prompt):] def _get_input_buffer_cursor_pos(self): """Get the cursor position within the input buffer.""" cursor = self._control.textCursor() cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor) input_buffer = cursor.selection().toPlainText() # Don't count continuation prompts return len(input_buffer.replace('\n' + self._continuation_prompt, '\n')) def _get_input_buffer_cursor_prompt(self): """ Returns the (plain text) prompt for line of the input buffer that contains the cursor, or None if there is no such line. """ if self._executing: return None cursor = self._control.textCursor() if cursor.position() >= self._prompt_pos: if cursor.blockNumber() == self._get_prompt_cursor().blockNumber(): return self._prompt else: return self._continuation_prompt else: return None def _get_last_lines(self, text, num_lines, return_count=False): """ Get the last specified number of lines of text (like `tail -n`). If return_count is True, returns a tuple of clipped text and the number of lines in the clipped text. """ pos = len(text) if pos < num_lines: if return_count: return text, text.count('\n') if return_count else text else: return text i = 0 while i < num_lines: pos = text.rfind('\n', None, pos) if pos == -1: pos = None break i += 1 if return_count: return text[pos:], i else: return text[pos:] def _get_last_lines_from_list(self, text_list, num_lines): """ Get the list of text clipped to last specified lines. """ ret = [] lines_pending = num_lines for text in reversed(text_list): text, lines_added = self._get_last_lines(text, lines_pending, return_count=True) ret.append(text) lines_pending -= lines_added if lines_pending <= 0: break return ret[::-1] def _get_leading_spaces(self): """ Get the number of leading spaces of the current line. """ cursor = self._get_cursor() start_line = cursor.blockNumber() if start_line == self._get_prompt_cursor().blockNumber(): # first line offset = len(self._prompt) else: # continuation offset = len(self._continuation_prompt) cursor.select(QtGui.QTextCursor.LineUnderCursor) text = cursor.selectedText()[offset:] return len(text) - len(text.lstrip()) @property def _prompt_pos(self): """ Find the position in the text right after the prompt. """ return min(self._prompt_cursor.position() + 1, self._get_end_pos()) @property def _append_before_prompt_pos(self): """ Find the position in the text right before the prompt. """ return min(self._append_before_prompt_cursor.position(), self._get_end_pos()) def _get_prompt_cursor(self): """ Get a cursor at the prompt position of the current cell. """ cursor = self._control.textCursor() cursor.setPosition(self._prompt_pos) return cursor def _get_selection_cursor(self, start, end): """ Get a cursor with text selected between the positions 'start' and 'end'. """ cursor = self._control.textCursor() cursor.setPosition(start) cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor) return cursor def _get_word_start_cursor(self, position): """ Find the start of the word to the left the given position. If a sequence of non-word characters precedes the first word, skip over them. (This emulates the behavior of bash, emacs, etc.) """ document = self._control.document() cursor = self._control.textCursor() line_start_pos = self._get_line_start_pos() if position == self._prompt_pos: return cursor elif position == line_start_pos: # Cursor is at the beginning of a line, move to the last # non-whitespace character of the previous line cursor = self._control.textCursor() cursor.setPosition(position) cursor.movePosition(QtGui.QTextCursor.PreviousBlock) cursor.movePosition(QtGui.QTextCursor.EndOfBlock) position = cursor.position() while ( position >= self._prompt_pos and is_whitespace(document.characterAt(position)) ): position -= 1 cursor.setPosition(position + 1) else: position -= 1 # Find the last alphanumeric char, but don't move across lines while ( position >= self._prompt_pos and position >= line_start_pos and not is_letter_or_number(document.characterAt(position)) ): position -= 1 # Find the first alphanumeric char, but don't move across lines while ( position >= self._prompt_pos and position >= line_start_pos and is_letter_or_number(document.characterAt(position)) ): position -= 1 cursor.setPosition(position + 1) return cursor def _get_word_end_cursor(self, position): """ Find the end of the word to the right the given position. If a sequence of non-word characters precedes the first word, skip over them. (This emulates the behavior of bash, emacs, etc.) """ document = self._control.document() cursor = self._control.textCursor() end_pos = self._get_end_pos() line_end_pos = self._get_line_end_pos() if position == end_pos: # Cursor is at the very end of the buffer return cursor elif position == line_end_pos: # Cursor is at the end of a line, move to the first # non-whitespace character of the next line cursor = self._control.textCursor() cursor.setPosition(position) cursor.movePosition(QtGui.QTextCursor.NextBlock) position = cursor.position() + len(self._continuation_prompt) while ( position < end_pos and is_whitespace(document.characterAt(position)) ): position += 1 cursor.setPosition(position) else: if is_whitespace(document.characterAt(position)): # The next character is whitespace. If this is part of # indentation whitespace, skip to the first non-whitespace # character. is_indentation_whitespace = True back_pos = position - 1 line_start_pos = self._get_line_start_pos() while back_pos >= line_start_pos: if not is_whitespace(document.characterAt(back_pos)): is_indentation_whitespace = False break back_pos -= 1 if is_indentation_whitespace: # Skip to the first non-whitespace character while ( position < end_pos and position < line_end_pos and is_whitespace(document.characterAt(position)) ): position += 1 cursor.setPosition(position) return cursor while ( position < end_pos and position < line_end_pos and not is_letter_or_number(document.characterAt(position)) ): position += 1 while ( position < end_pos and position < line_end_pos and is_letter_or_number(document.characterAt(position)) ): position += 1 cursor.setPosition(position) return cursor def _indent(self, dedent=True): """ Indent/Dedent current line or current text selection. """ num_newlines = self._get_cursor().selectedText().count("\u2029") save_cur = self._get_cursor() cur = self._get_cursor() # move to first line of selection, if present cur.setPosition(cur.selectionStart()) self._control.setTextCursor(cur) spaces = self._get_leading_spaces() # calculate number of spaces neded to align/indent to 4-space multiple step = self._tab_width - (spaces % self._tab_width) # insertText shouldn't replace if selection is active cur.clearSelection() # indent all lines in selection (ir just current) by `step` for _ in range(num_newlines+1): # update underlying cursor for _get_line_start_pos self._control.setTextCursor(cur) # move to first non-ws char on line cur.setPosition(self._get_line_start_pos()) if dedent: spaces = min(step, self._get_leading_spaces()) safe_step = spaces % self._tab_width cur.movePosition(QtGui.QTextCursor.Right, QtGui.QTextCursor.KeepAnchor, min(spaces, safe_step if safe_step != 0 else self._tab_width)) cur.removeSelectedText() else: cur.insertText(' '*step) cur.movePosition(QtGui.QTextCursor.Down) # restore cursor self._control.setTextCursor(save_cur) def _insert_continuation_prompt(self, cursor, indent=''): """ Inserts new continuation prompt using the specified cursor. """ if self._continuation_prompt_html is None: self._insert_plain_text(cursor, self._continuation_prompt) else: self._continuation_prompt = self._insert_html_fetching_plain_text( cursor, self._continuation_prompt_html) if indent: cursor.insertText(indent) def _insert_block(self, cursor, block_format=None): """ Inserts an empty QTextBlock using the specified cursor. """ if block_format is None: block_format = QtGui.QTextBlockFormat() cursor.insertBlock(block_format) def _insert_html(self, cursor, html): """ Inserts HTML using the specified cursor in such a way that future formatting is unaffected. """ cursor.beginEditBlock() cursor.insertHtml(html) # After inserting HTML, the text document "remembers" it's in "html # mode", which means that subsequent calls adding plain text will result # in unwanted formatting, lost tab characters, etc. The following code # hacks around this behavior, which I consider to be a bug in Qt, by # (crudely) resetting the document's style state. cursor.movePosition(QtGui.QTextCursor.Left, QtGui.QTextCursor.KeepAnchor) if cursor.selection().toPlainText() == ' ': cursor.removeSelectedText() else: cursor.movePosition(QtGui.QTextCursor.Right) cursor.insertText(' ', QtGui.QTextCharFormat()) cursor.endEditBlock() def _insert_html_fetching_plain_text(self, cursor, html): """ Inserts HTML using the specified cursor, then returns its plain text version. """ cursor.beginEditBlock() cursor.removeSelectedText() start = cursor.position() self._insert_html(cursor, html) end = cursor.position() cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor) text = cursor.selection().toPlainText() cursor.setPosition(end) cursor.endEditBlock() return text def _viewport_at_end(self): """Check if the viewport is at the end of the document.""" viewport = self._control.viewport() end_scroll_pos = self._control.cursorForPosition( QtCore.QPoint(viewport.width() - 1, viewport.height() - 1) ).position() end_doc_pos = self._get_end_pos() return end_doc_pos - end_scroll_pos <= 1 def _scroll_to_end(self): """Scroll to the end of the document.""" end_scroll = (self._control.verticalScrollBar().maximum() - self._control.verticalScrollBar().pageStep()) # Only scroll down if end_scroll > self._control.verticalScrollBar().value(): self._control.verticalScrollBar().setValue(end_scroll) def _insert_plain_text(self, cursor, text, flush=False): """ Inserts plain text using the specified cursor, processing ANSI codes if enabled. """ should_autoscroll = self._viewport_at_end() # maximumBlockCount() can be different from self.buffer_size in # case input prompt is active. buffer_size = self._control.document().maximumBlockCount() if (self._executing and not flush and self._pending_text_flush_interval.isActive() and cursor.position() == self._get_end_pos()): # Queue the text to insert in case it is being inserted at end self._pending_insert_text.append(text) if buffer_size > 0: self._pending_insert_text = self._get_last_lines_from_list( self._pending_insert_text, buffer_size) return if self._executing and not self._pending_text_flush_interval.isActive(): self._pending_text_flush_interval.start() # Clip the text to last `buffer_size` lines. if buffer_size > 0: text = self._get_last_lines(text, buffer_size) cursor.beginEditBlock() if self.ansi_codes: for substring in self._ansi_processor.split_string(text): for act in self._ansi_processor.actions: # Unlike real terminal emulators, we don't distinguish # between the screen and the scrollback buffer. A screen # erase request clears everything. if act.action == 'erase': remove = False fill = False if act.area == 'screen': cursor.select(QtGui.QTextCursor.Document) remove = True if act.area == 'line': if act.erase_to == 'all': cursor.select(QtGui.QTextCursor.LineUnderCursor) remove = True elif act.erase_to == 'start': cursor.movePosition( QtGui.QTextCursor.StartOfLine, QtGui.QTextCursor.KeepAnchor) remove = True fill = True elif act.erase_to == 'end': cursor.movePosition( QtGui.QTextCursor.EndOfLine, QtGui.QTextCursor.KeepAnchor) remove = True if remove: nspace=cursor.selectionEnd()-cursor.selectionStart() if fill else 0 cursor.removeSelectedText() if nspace>0: cursor.insertText(' '*nspace) # replace text by space, to keep cursor position as specified # Simulate a form feed by scrolling just past the last line. elif act.action == 'scroll' and act.unit == 'page': cursor.insertText('\n') cursor.endEditBlock() self._set_top_cursor(cursor) cursor.joinPreviousEditBlock() cursor.deletePreviousChar() if os.name == 'nt': cursor.select(QtGui.QTextCursor.Document) cursor.removeSelectedText() elif act.action == 'carriage-return': cursor.movePosition( QtGui.QTextCursor.StartOfLine, QtGui.QTextCursor.MoveAnchor) elif act.action == 'beep': QtWidgets.QApplication.instance().beep() elif act.action == 'backspace': if not cursor.atBlockStart(): cursor.movePosition( QtGui.QTextCursor.PreviousCharacter, QtGui.QTextCursor.MoveAnchor) elif act.action == 'newline': cursor.movePosition(QtGui.QTextCursor.EndOfLine) # simulate replacement mode if substring is not None: format = self._ansi_processor.get_format() if not (hasattr(cursor,'_insert_mode') and cursor._insert_mode): pos = cursor.position() cursor2 = QtGui.QTextCursor(cursor) # self._get_line_end_pos() is the previous line, don't use it cursor2.movePosition(QtGui.QTextCursor.EndOfLine) remain = cursor2.position() - pos # number of characters until end of line n=len(substring) swallow = min(n, remain) # number of character to swallow cursor.setPosition(pos+swallow,QtGui.QTextCursor.KeepAnchor) cursor.insertText(substring,format) else: cursor.insertText(text) cursor.endEditBlock() if should_autoscroll: self._scroll_to_end() def _insert_plain_text_into_buffer(self, cursor, text): """ Inserts text into the input buffer using the specified cursor (which must be in the input buffer), ensuring that continuation prompts are inserted as necessary. """ lines = text.splitlines(True) if lines: if lines[-1].endswith('\n'): # If the text ends with a newline, add a blank line so a new # continuation prompt is produced. lines.append('') cursor.beginEditBlock() cursor.insertText(lines[0]) for line in lines[1:]: if self._continuation_prompt_html is None: cursor.insertText(self._continuation_prompt) else: self._continuation_prompt = \ self._insert_html_fetching_plain_text( cursor, self._continuation_prompt_html) cursor.insertText(line) cursor.endEditBlock() def _in_buffer(self, position): """ Returns whether the specified position is inside the editing region. """ return position == self._move_position_in_buffer(position) def _move_position_in_buffer(self, position): """ Return the next position in buffer. """ cursor = self._control.textCursor() cursor.setPosition(position) line = cursor.blockNumber() prompt_line = self._get_prompt_cursor().blockNumber() if line == prompt_line: if position >= self._prompt_pos: return position return self._prompt_pos if line > prompt_line: cursor.movePosition(QtGui.QTextCursor.StartOfBlock) prompt_pos = cursor.position() + len(self._continuation_prompt) if position >= prompt_pos: return position return prompt_pos return self._prompt_pos def _keep_cursor_in_buffer(self): """ Ensures that the cursor is inside the editing region. Returns whether the cursor was moved. """ cursor = self._control.textCursor() endpos = cursor.selectionEnd() if endpos < self._prompt_pos: cursor.setPosition(endpos) line = cursor.blockNumber() prompt_line = self._get_prompt_cursor().blockNumber() if line == prompt_line: # Cursor is on prompt line, move to start of buffer cursor.setPosition(self._prompt_pos) else: # Cursor is not in buffer, move to the end cursor.movePosition(QtGui.QTextCursor.End) self._control.setTextCursor(cursor) return True startpos = cursor.selectionStart() new_endpos = self._move_position_in_buffer(endpos) new_startpos = self._move_position_in_buffer(startpos) if new_endpos == endpos and new_startpos == startpos: return False cursor.setPosition(new_startpos) cursor.setPosition(new_endpos, QtGui.QTextCursor.KeepAnchor) self._control.setTextCursor(cursor) return True def _keyboard_quit(self): """ Cancels the current editing task ala Ctrl-G in Emacs. """ if self._temp_buffer_filled : self._cancel_completion() self._clear_temporary_buffer() else: self.input_buffer = '' def _page(self, text, html=False): """ Displays text using the pager if it exceeds the height of the viewport. Parameters ---------- html : bool, optional (default False) If set, the text will be interpreted as HTML instead of plain text. """ line_height = QtGui.QFontMetrics(self.font).height() minlines = self._control.viewport().height() / line_height if self.paging != 'none' and \ re.match("(?:[^\n]*\n){%i}" % minlines, text): if self.paging == 'custom': self.custom_page_requested.emit(text) else: # disable buffer truncation during paging self._control.document().setMaximumBlockCount(0) self._page_control.clear() cursor = self._page_control.textCursor() if html: self._insert_html(cursor, text) else: self._insert_plain_text(cursor, text) self._page_control.moveCursor(QtGui.QTextCursor.Start) self._page_control.viewport().resize(self._control.size()) if self._splitter: self._page_control.show() self._page_control.setFocus() else: self.layout().setCurrentWidget(self._page_control) elif html: self._append_html(text) else: self._append_plain_text(text) def _set_paging(self, paging): """ Change the pager to `paging` style. Parameters ---------- paging : string Either "hsplit", "vsplit", or "inside" """ if self._splitter is None: raise NotImplementedError("""can only switch if --paging=hsplit or --paging=vsplit is used.""") if paging == 'hsplit': self._splitter.setOrientation(QtCore.Qt.Horizontal) elif paging == 'vsplit': self._splitter.setOrientation(QtCore.Qt.Vertical) elif paging == 'inside': raise NotImplementedError("""switching to 'inside' paging not supported yet.""") else: raise ValueError("unknown paging method '%s'" % paging) self.paging = paging def _prompt_finished(self): """ Called immediately after a prompt is finished, i.e. when some input will be processed and a new prompt displayed. """ self._control.setReadOnly(True) self._prompt_finished_hook() def _prompt_started(self): """ Called immediately after a new prompt is displayed. """ # Temporarily disable the maximum block count to permit undo/redo and # to ensure that the prompt position does not change due to truncation. self._control.document().setMaximumBlockCount(0) self._control.setUndoRedoEnabled(True) # Work around bug in QPlainTextEdit: input method is not re-enabled # when read-only is disabled. self._control.setReadOnly(False) self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True) if not self._reading: self._executing = False self._prompt_started_hook() # If the input buffer has changed while executing, load it. if self._input_buffer_pending: self.input_buffer = self._input_buffer_pending self._input_buffer_pending = '' self._control.moveCursor(QtGui.QTextCursor.End) def _readline(self, prompt='', callback=None, password=False): """ Reads one line of input from the user. Parameters ---------- prompt : str, optional The prompt to print before reading the line. callback : callable, optional A callback to execute with the read line. If not specified, input is read *synchronously* and this method does not return until it has been read. Returns ------- If a callback is specified, returns nothing. Otherwise, returns the input string with the trailing newline stripped. """ if self._reading: raise RuntimeError('Cannot read a line. Widget is already reading.') if not callback and not self.isVisible(): # If the user cannot see the widget, this function cannot return. raise RuntimeError('Cannot synchronously read a line if the widget ' 'is not visible!') self._reading = True if password: self._show_prompt('Warning: QtConsole does not support password mode, '\ 'the text you type will be visible.', newline=True) if 'ipdb' not in prompt.lower(): # This is a prompt that asks for input from the user. self._show_prompt(prompt, newline=False, separator=False) else: self._show_prompt(prompt, newline=False) if callback is None: self._reading_callback = None while self._reading: QtCore.QCoreApplication.processEvents() return self._get_input_buffer(force=True).rstrip('\n') else: self._reading_callback = lambda: \ callback(self._get_input_buffer(force=True).rstrip('\n')) def _set_continuation_prompt(self, prompt, html=False): """ Sets the continuation prompt. Parameters ---------- prompt : str The prompt to show when more input is needed. html : bool, optional (default False) If set, the prompt will be inserted as formatted HTML. Otherwise, the prompt will be treated as plain text, though ANSI color codes will be handled. """ if html: self._continuation_prompt_html = prompt else: self._continuation_prompt = prompt self._continuation_prompt_html = None def _set_cursor(self, cursor): """ Convenience method to set the current cursor. """ self._control.setTextCursor(cursor) def _set_top_cursor(self, cursor): """ Scrolls the viewport so that the specified cursor is at the top. """ scrollbar = self._control.verticalScrollBar() scrollbar.setValue(scrollbar.maximum()) original_cursor = self._control.textCursor() self._control.setTextCursor(cursor) self._control.ensureCursorVisible() self._control.setTextCursor(original_cursor) def _show_prompt(self, prompt=None, html=False, newline=True, separator=True): """ Writes a new prompt at the end of the buffer. Parameters ---------- prompt : str, optional The prompt to show. If not specified, the previous prompt is used. html : bool, optional (default False) Only relevant when a prompt is specified. If set, the prompt will be inserted as formatted HTML. Otherwise, the prompt will be treated as plain text, though ANSI color codes will be handled. newline : bool, optional (default True) If set, a new line will be written before showing the prompt if there is not already a newline at the end of the buffer. separator : bool, optional (default True) If set, a separator will be written before the prompt. """ self._flush_pending_stream() # This is necessary to solve out-of-order insertion of mixed stdin and # stdout stream texts. # Fixes spyder-ide/spyder#17710 if sys.platform == 'darwin': # Although this makes our tests hang on Mac, users confirmed that # it's needed on that platform too. # Fixes spyder-ide/spyder#19888 if not os.environ.get('QTCONSOLE_TESTING'): QtCore.QCoreApplication.processEvents() else: QtCore.QCoreApplication.processEvents() cursor = self._get_end_cursor() # Save the current position to support _append*(before_prompt=True). # We can't leave the cursor at the end of the document though, because # that would cause any further additions to move the cursor. Therefore, # we move it back one place and move it forward again at the end of # this method. However, we only do this if the cursor isn't already # at the start of the text. if cursor.position() == 0: move_forward = False else: move_forward = True self._append_before_prompt_cursor.setPosition(cursor.position() - 1) # Insert a preliminary newline, if necessary. if newline and cursor.position() > 0: cursor.movePosition(QtGui.QTextCursor.Left, QtGui.QTextCursor.KeepAnchor) if cursor.selection().toPlainText() != '\n': self._append_block() # Write the prompt. if separator: self._append_plain_text(self._prompt_sep) if prompt is None: if self._prompt_html is None: self._append_plain_text(self._prompt) else: self._append_html(self._prompt_html) else: if html: self._prompt = self._append_html_fetching_plain_text(prompt) self._prompt_html = prompt else: self._append_plain_text(prompt) self._prompt = prompt self._prompt_html = None self._flush_pending_stream() self._prompt_cursor.setPosition(self._get_end_pos() - 1) if move_forward: self._append_before_prompt_cursor.setPosition( self._append_before_prompt_cursor.position() + 1) self._prompt_started() #------ Signal handlers ---------------------------------------------------- def _adjust_scrollbars(self): """ Expands the vertical scrollbar beyond the range set by Qt. """ # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp # and qtextedit.cpp. document = self._control.document() scrollbar = self._control.verticalScrollBar() viewport_height = self._control.viewport().height() if isinstance(self._control, QtWidgets.QPlainTextEdit): maximum = max(0, document.lineCount() - 1) step = viewport_height / self._control.fontMetrics().lineSpacing() else: # QTextEdit does not do line-based layout and blocks will not in # general have the same height. Therefore it does not make sense to # attempt to scroll in line height increments. maximum = document.size().height() step = viewport_height diff = maximum - scrollbar.maximum() scrollbar.setRange(0, round(maximum)) scrollbar.setPageStep(round(step)) # Compensate for undesirable scrolling that occurs automatically due to # maximumBlockCount() text truncation. if diff < 0 and document.blockCount() == document.maximumBlockCount(): scrollbar.setValue(round(scrollbar.value() + diff)) def _custom_context_menu_requested(self, pos): """ Shows a context menu at the given QPoint (in widget coordinates). """ menu = self._context_menu_make(pos) menu.exec_(self._control.mapToGlobal(pos)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1714872940.0 qtconsole-5.5.2/qtconsole/frontend_widget.py0000664000175000017500000010400300000000000022066 0ustar00carloscarlos00000000000000"""Frontend widget for the Qt Console""" # Copyright (c) Jupyter Development Team # Distributed under the terms of the Modified BSD License. from collections import namedtuple import sys import uuid import re from qtpy import QtCore, QtGui, QtWidgets from qtconsole.base_frontend_mixin import BaseFrontendMixin from traitlets import Any, Bool, Instance, Unicode, DottedObjectName, default from .bracket_matcher import BracketMatcher from .call_tip_widget import CallTipWidget from .history_console_widget import HistoryConsoleWidget from .pygments_highlighter import PygmentsHighlighter from .util import import_item class FrontendHighlighter(PygmentsHighlighter): """ A PygmentsHighlighter that understands and ignores prompts. """ def __init__(self, frontend, lexer=None): super().__init__(frontend._control.document(), lexer=lexer) self._current_offset = 0 self._frontend = frontend self.highlighting_on = False self._classic_prompt_re = re.compile( r'^(%s)?([ \t]*>>> |^[ \t]*\.\.\. )' % re.escape(frontend.other_output_prefix) ) self._ipy_prompt_re = re.compile( r'^(%s)?([ \t]*In \[\d+\]: |[ \t]*\ \ \ \.\.\.+: )' % re.escape(frontend.other_output_prefix) ) def transform_classic_prompt(self, line): """Handle inputs that start with '>>> ' syntax.""" if not line or line.isspace(): return line m = self._classic_prompt_re.match(line) if m: return line[len(m.group(0)):] else: return line def transform_ipy_prompt(self, line): """Handle inputs that start classic IPython prompt syntax.""" if not line or line.isspace(): return line m = self._ipy_prompt_re.match(line) if m: return line[len(m.group(0)):] else: return line def highlightBlock(self, string): """ Highlight a block of text. Reimplemented to highlight selectively. """ if not hasattr(self, 'highlighting_on') or not self.highlighting_on: return # The input to this function is a unicode string that may contain # paragraph break characters, non-breaking spaces, etc. Here we acquire # the string as plain text so we can compare it. current_block = self.currentBlock() string = current_block.text() # QTextBlock::text() can still return non-breaking spaces # for the continuation prompt string = string.replace("\xa0", " ") # Only highlight if we can identify a prompt, but make sure not to # highlight the prompt. without_prompt = self.transform_ipy_prompt(string) diff = len(string) - len(without_prompt) if diff > 0: self._current_offset = diff super().highlightBlock(without_prompt) def rehighlightBlock(self, block): """ Reimplemented to temporarily enable highlighting if disabled. """ old = self.highlighting_on self.highlighting_on = True super().rehighlightBlock(block) self.highlighting_on = old def setFormat(self, start, count, format): """ Reimplemented to highlight selectively. """ start += self._current_offset super().setFormat(start, count, format) class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): """ A Qt frontend for a generic Python kernel. """ # The text to show when the kernel is (re)started. banner = Unicode(config=True) kernel_banner = Unicode() # Whether to show the banner _display_banner = Bool(False) # An option and corresponding signal for overriding the default kernel # interrupt behavior. custom_interrupt = Bool(False) custom_interrupt_requested = QtCore.Signal() # An option and corresponding signals for overriding the default kernel # restart behavior. custom_restart = Bool(False) custom_restart_kernel_died = QtCore.Signal(float) custom_restart_requested = QtCore.Signal() # Whether to automatically show calltips on open-parentheses. enable_calltips = Bool(True, config=True, help="Whether to draw information calltips on open-parentheses.") clear_on_kernel_restart = Bool(True, config=True, help="Whether to clear the console when the kernel is restarted") confirm_restart = Bool(True, config=True, help="Whether to ask for user confirmation when restarting kernel") lexer_class = DottedObjectName(config=True, help="The pygments lexer class to use." ) def _lexer_class_changed(self, name, old, new): lexer_class = import_item(new) self.lexer = lexer_class() def _lexer_class_default(self): return 'pygments.lexers.Python3Lexer' lexer = Any() def _lexer_default(self): lexer_class = import_item(self.lexer_class) return lexer_class() # Emitted when a user visible 'execute_request' has been submitted to the # kernel from the FrontendWidget. Contains the code to be executed. executing = QtCore.Signal(object) # Emitted when a user-visible 'execute_reply' has been received from the # kernel and processed by the FrontendWidget. Contains the response message. executed = QtCore.Signal(object) # Emitted when an exit request has been received from the kernel. exit_requested = QtCore.Signal(object) _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos']) _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'code', 'pos']) _ExecutionRequest = namedtuple( '_ExecutionRequest', ['id', 'kind', 'hidden']) _local_kernel = False _highlighter = Instance(FrontendHighlighter, allow_none=True) # ------------------------------------------------------------------------- # 'Object' interface # ------------------------------------------------------------------------- def __init__(self, local_kernel=_local_kernel, *args, **kw): super().__init__(*args, **kw) # FrontendWidget protected variables. self._bracket_matcher = BracketMatcher(self._control) self._call_tip_widget = CallTipWidget(self._control) self._copy_raw_action = QtWidgets.QAction('Copy (Raw Text)', None) self._highlighter = FrontendHighlighter(self, lexer=self.lexer) self._kernel_manager = None self._kernel_client = None self._request_info = {} self._request_info['execute'] = {} self._callback_dict = {} self._display_banner = True # Configure the ConsoleWidget. self.tab_width = 4 self._set_continuation_prompt('... ') # Configure the CallTipWidget. self._call_tip_widget.setFont(self.font) self.font_changed.connect(self._call_tip_widget.setFont) # Configure actions. action = self._copy_raw_action action.setEnabled(False) action.setShortcut(QtGui.QKeySequence("Ctrl+Shift+C")) action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.copy_raw) self.copy_available.connect(action.setEnabled) self.addAction(action) # Connect signal handlers. document = self._control.document() document.contentsChange.connect(self._document_contents_change) # Set flag for whether we are connected via localhost. self._local_kernel = local_kernel # Whether or not a clear_output call is pending new output. self._pending_clearoutput = False #--------------------------------------------------------------------------- # 'ConsoleWidget' public interface #--------------------------------------------------------------------------- def copy(self): """ Copy the currently selected text to the clipboard, removing prompts. """ if self._page_control is not None and self._page_control.hasFocus(): self._page_control.copy() elif self._control.hasFocus(): text = self._control.textCursor().selection().toPlainText() if text: first_line_selection, *remaining_lines = text.splitlines() # Get preceding text cursor = self._control.textCursor() cursor.setPosition(cursor.selectionStart()) cursor.setPosition(cursor.block().position(), QtGui.QTextCursor.KeepAnchor) preceding_text = cursor.selection().toPlainText() def remove_prompts(line): """Remove all prompts from line.""" line = self._highlighter.transform_classic_prompt(line) return self._highlighter.transform_ipy_prompt(line) # Get first line promp len first_line = preceding_text + first_line_selection len_with_prompt = len(first_line) first_line = remove_prompts(first_line) prompt_len = len_with_prompt - len(first_line) # Remove not selected part if prompt_len < len(preceding_text): first_line = first_line[len(preceding_text) - prompt_len:] # Remove partial prompt last line if len(remaining_lines) > 0 and remaining_lines[-1]: cursor = self._control.textCursor() cursor.setPosition(cursor.selectionEnd()) block = cursor.block() start_pos = block.position() length = block.length() cursor.setPosition(start_pos) cursor.setPosition(start_pos + length - 1, QtGui.QTextCursor.KeepAnchor) last_line_full = cursor.selection().toPlainText() prompt_len = ( len(last_line_full) - len(remove_prompts(last_line_full))) if len(remaining_lines[-1]) < prompt_len: # This is a partial prompt remaining_lines[-1] = "" # Remove prompts for other lines. remaining_lines = map(remove_prompts, remaining_lines) text = '\n'.join([first_line, *remaining_lines]) # Needed to prevent errors when copying the prompt. # See issue 264 try: was_newline = text[-1] == '\n' except IndexError: was_newline = False if was_newline: # user doesn't need newline text = text[:-1] QtWidgets.QApplication.clipboard().setText(text) else: self.log.debug("frontend widget : unknown copy target") #--------------------------------------------------------------------------- # 'ConsoleWidget' abstract interface #--------------------------------------------------------------------------- def _execute(self, source, hidden): """ Execute 'source'. If 'hidden', do not show any output. See parent class :meth:`execute` docstring for full details. """ msg_id = self.kernel_client.execute(source, hidden) self._request_info['execute'][msg_id] = self._ExecutionRequest( msg_id, 'user', hidden) if not hidden: self.executing.emit(source) def _prompt_started_hook(self): """ Called immediately after a new prompt is displayed. """ if not self._reading: self._highlighter.highlighting_on = True def _prompt_finished_hook(self): """ Called immediately after a prompt is finished, i.e. when some input will be processed and a new prompt displayed. """ if not self._reading: self._highlighter.highlighting_on = False def _tab_pressed(self): """ Called when the tab key is pressed. Returns whether to continue processing the event. """ # Perform tab completion if: # 1) The cursor is in the input buffer. # 2) There is a non-whitespace character before the cursor. # 3) There is no active selection. text = self._get_input_buffer_cursor_line() if text is None: return False non_ws_before = bool(text[:self._get_input_buffer_cursor_column()].strip()) complete = non_ws_before and self._get_cursor().selectedText() == '' if complete: self._complete() return not complete #--------------------------------------------------------------------------- # 'ConsoleWidget' protected interface #--------------------------------------------------------------------------- def _context_menu_make(self, pos): """ Reimplemented to add an action for raw copy. """ menu = super()._context_menu_make(pos) for before_action in menu.actions(): if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \ QtGui.QKeySequence.ExactMatch: menu.insertAction(before_action, self._copy_raw_action) break return menu def request_interrupt_kernel(self): if self._executing: self.interrupt_kernel() def request_restart_kernel(self): message = 'Are you sure you want to restart the kernel?' self.restart_kernel(message, now=False) def _event_filter_console_keypress(self, event): """ Reimplemented for execution interruption and smart backspace. """ key = event.key() if self._control_key_down(event.modifiers(), include_command=False): if key == QtCore.Qt.Key_C and self._executing: # If text is selected, the user probably wants to copy it. if self.can_copy() and event.matches(QtGui.QKeySequence.Copy): self.copy() else: self.request_interrupt_kernel() return True elif key == QtCore.Qt.Key_Period: self.request_restart_kernel() return True elif not event.modifiers() & QtCore.Qt.AltModifier: # Smart backspace: remove four characters in one backspace if: # 1) everything left of the cursor is whitespace # 2) the four characters immediately left of the cursor are spaces if key == QtCore.Qt.Key_Backspace: col = self._get_input_buffer_cursor_column() cursor = self._control.textCursor() if col > 3 and not cursor.hasSelection(): text = self._get_input_buffer_cursor_line()[:col] if text.endswith(' ') and not text.strip(): cursor.movePosition(QtGui.QTextCursor.Left, QtGui.QTextCursor.KeepAnchor, 4) cursor.removeSelectedText() return True return super()._event_filter_console_keypress(event) #--------------------------------------------------------------------------- # 'BaseFrontendMixin' abstract interface #--------------------------------------------------------------------------- def _handle_clear_output(self, msg): """Handle clear output messages.""" if self.include_output(msg): wait = msg['content'].get('wait', True) if wait: self._pending_clearoutput = True else: self.clear_output() def _silent_exec_callback(self, expr, callback): """Silently execute `expr` in the kernel and call `callback` with reply the `expr` is evaluated silently in the kernel (without) output in the frontend. Call `callback` with the `repr `_ as first argument Parameters ---------- expr : string valid string to be executed by the kernel. callback : function function accepting one argument, as a string. The string will be the `repr` of the result of evaluating `expr` The `callback` is called with the `repr()` of the result of `expr` as first argument. To get the object, do `eval()` on the passed value. See Also -------- _handle_exec_callback : private method, deal with calling callback with reply """ # generate uuid, which would be used as an indication of whether or # not the unique request originated from here (can use msg id ?) local_uuid = str(uuid.uuid1()) msg_id = self.kernel_client.execute('', silent=True, user_expressions={ local_uuid:expr }) self._callback_dict[local_uuid] = callback self._request_info['execute'][msg_id] = self._ExecutionRequest( msg_id, 'silent_exec_callback', False) def _handle_exec_callback(self, msg): """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback`` Parameters ---------- msg : raw message send by the kernel containing an `user_expressions` and having a 'silent_exec_callback' kind. Notes ----- This function will look for a `callback` associated with the corresponding message id. Association has been made by `_silent_exec_callback`. `callback` is then called with the `repr()` of the value of corresponding `user_expressions` as argument. `callback` is then removed from the known list so that any message coming again with the same id won't trigger it. """ user_exp = msg['content'].get('user_expressions') if not user_exp: return for expression in user_exp: if expression in self._callback_dict: self._callback_dict.pop(expression)(user_exp[expression]) def _handle_execute_reply(self, msg): """ Handles replies for code execution. """ self.log.debug("execute_reply: %s", msg.get('content', '')) msg_id = msg['parent_header']['msg_id'] info = self._request_info['execute'].get(msg_id) # unset reading flag, because if execute finished, raw_input can't # still be pending. self._reading = False # Note: If info is NoneType, this is ignored if not info or info.hidden: return if info.kind == 'user': # Make sure that all output from the SUB channel has been processed # before writing a new prompt. if not self.kernel_client.iopub_channel.closed(): self.kernel_client.iopub_channel.flush() # Reset the ANSI style information to prevent bad text in stdout # from messing up our colors. We're not a true terminal so we're # allowed to do this. if self.ansi_codes: self._ansi_processor.reset_sgr() content = msg['content'] status = content['status'] if status == 'ok': self._process_execute_ok(msg) elif status == 'aborted': self._process_execute_abort(msg) self._show_interpreter_prompt_for_reply(msg) self.executed.emit(msg) self._request_info['execute'].pop(msg_id) elif info.kind == 'silent_exec_callback': self._handle_exec_callback(msg) self._request_info['execute'].pop(msg_id) else: raise RuntimeError("Unknown handler for %s" % info.kind) def _handle_error(self, msg): """ Handle error messages. """ self._process_execute_error(msg) def _handle_input_request(self, msg): """ Handle requests for raw_input. """ self.log.debug("input: %s", msg.get('content', '')) msg_id = msg['parent_header']['msg_id'] info = self._request_info['execute'].get(msg_id) if info and info.hidden: raise RuntimeError('Request for raw input during hidden execution.') # Make sure that all output from the SUB channel has been processed # before entering readline mode. if not self.kernel_client.iopub_channel.closed(): self.kernel_client.iopub_channel.flush() def callback(line): self._finalize_input_request() self.kernel_client.input(line) if self._reading: self.log.debug("Got second input request, assuming first was interrupted.") self._reading = False self._readline(msg['content']['prompt'], callback=callback, password=msg['content']['password']) def _kernel_restarted_message(self, died=True): msg = "Kernel died, restarting" if died else "Kernel restarting" self._append_html("
%s

" % msg, before_prompt=False ) def _handle_kernel_died(self, since_last_heartbeat): """Handle the kernel's death (if we do not own the kernel). """ self.log.warning("kernel died: %s", since_last_heartbeat) if self.custom_restart: self.custom_restart_kernel_died.emit(since_last_heartbeat) else: self._kernel_restarted_message(died=True) self.reset() def _handle_kernel_restarted(self, died=True): """Notice that the autorestarter restarted the kernel. There's nothing to do but show a message. """ self.log.warning("kernel restarted") self._kernel_restarted_message(died=died) # This resets the autorestart counter so that the kernel can be # auto-restarted before the next time it's polled to see if it's alive. if self.kernel_manager: self.kernel_manager.reset_autorestart_count() self.reset() def _handle_inspect_reply(self, rep): """Handle replies for call tips.""" self.log.debug("oinfo: %s", rep.get('content', '')) cursor = self._get_cursor() info = self._request_info.get('call_tip') if info and info.id == rep['parent_header']['msg_id'] and \ info.pos == cursor.position(): content = rep['content'] if content.get('status') == 'ok' and content.get('found', False): self._call_tip_widget.show_inspect_data(content) def _handle_execute_result(self, msg): """ Handle display hook output. """ self.log.debug("execute_result: %s", msg.get('content', '')) if self.include_output(msg): self.flush_clearoutput() text = msg['content']['data'] self._append_plain_text(text + '\n', before_prompt=True) def _handle_stream(self, msg): """ Handle stdout, stderr, and stdin. """ self.log.debug("stream: %s", msg.get('content', '')) if self.include_output(msg): self.flush_clearoutput() self.append_stream(msg['content']['text']) def _handle_shutdown_reply(self, msg): """ Handle shutdown signal, only if from other console. """ self.log.debug("shutdown: %s", msg.get('content', '')) restart = msg.get('content', {}).get('restart', False) if msg['parent_header']: msg_id = msg['parent_header']['msg_id'] info = self._request_info['execute'].get(msg_id) if info and info.hidden: return if not self.from_here(msg): # got shutdown reply, request came from session other than ours if restart: # someone restarted the kernel, handle it self._handle_kernel_restarted(died=False) else: # kernel was shutdown permanently # this triggers exit_requested if the kernel was local, # and a dialog if the kernel was remote, # so we don't suddenly clear the qtconsole without asking. if self._local_kernel: self.exit_requested.emit(self) else: title = self.window().windowTitle() reply = QtWidgets.QMessageBox.question(self, title, "Kernel has been shutdown permanently. " "Close the Console?", QtWidgets.QMessageBox.Yes,QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: self.exit_requested.emit(self) def _handle_status(self, msg): """Handle status message""" # This is where a busy/idle indicator would be triggered, # when we make one. state = msg['content'].get('execution_state', '') if state == 'starting': # kernel started while we were running if self._executing: self._handle_kernel_restarted(died=True) elif state == 'idle': pass elif state == 'busy': pass def _started_channels(self): """ Called when the KernelManager channels have started listening or when the frontend is assigned an already listening KernelManager. """ self.reset(clear=True) #--------------------------------------------------------------------------- # 'FrontendWidget' public interface #--------------------------------------------------------------------------- def copy_raw(self): """ Copy the currently selected text to the clipboard without attempting to remove prompts or otherwise alter the text. """ self._control.copy() def interrupt_kernel(self): """ Attempts to interrupt the running kernel. Also unsets _reading flag, to avoid runtime errors if raw_input is called again. """ if self.custom_interrupt: self._reading = False self.custom_interrupt_requested.emit() elif self.kernel_manager: self._reading = False self.kernel_manager.interrupt_kernel() else: self._append_plain_text('Cannot interrupt a kernel I did not start.\n') def reset(self, clear=False): """ Resets the widget to its initial state if ``clear`` parameter is True, otherwise prints a visual indication of the fact that the kernel restarted, but does not clear the traces from previous usage of the kernel before it was restarted. With ``clear=True``, it is similar to ``%clear``, but also re-writes the banner and aborts execution if necessary. """ if self._executing: self._executing = False self._request_info['execute'] = {} self._reading = False self._highlighter.highlighting_on = False if clear: self._control.clear() if self._display_banner: self._append_plain_text(self.banner) if self.kernel_banner: self._append_plain_text(self.kernel_banner) # update output marker for stdout/stderr, so that startup # messages appear after banner: self._show_interpreter_prompt() def restart_kernel(self, message, now=False): """ Attempts to restart the running kernel. """ # FIXME: now should be configurable via a checkbox in the dialog. Right # now at least the heartbeat path sets it to True and the manual restart # to False. But those should just be the pre-selected states of a # checkbox that the user could override if so desired. But I don't know # enough Qt to go implementing the checkbox now. if self.custom_restart: self.custom_restart_requested.emit() return if self.kernel_manager: # Pause the heart beat channel to prevent further warnings. self.kernel_client.hb_channel.pause() # Prompt the user to restart the kernel. Un-pause the heartbeat if # they decline. (If they accept, the heartbeat will be un-paused # automatically when the kernel is restarted.) if self.confirm_restart: buttons = QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No result = QtWidgets.QMessageBox.question(self, 'Restart kernel?', message, buttons) do_restart = result == QtWidgets.QMessageBox.Yes else: # confirm_restart is False, so we don't need to ask user # anything, just do the restart do_restart = True if do_restart: try: self.kernel_manager.restart_kernel(now=now) except RuntimeError as e: self._append_plain_text( 'Error restarting kernel: %s\n' % e, before_prompt=True ) else: self._append_html("
Restarting kernel...\n

", before_prompt=True, ) else: self.kernel_client.hb_channel.unpause() else: self._append_plain_text( 'Cannot restart a Kernel I did not start\n', before_prompt=True ) def append_stream(self, text): """Appends text to the output stream.""" self._append_plain_text(text, before_prompt=True) def flush_clearoutput(self): """If a clearoutput is pending, execute it.""" if self._pending_clearoutput: self._pending_clearoutput = False self.clear_output() def clear_output(self): """Clears the current line of output.""" cursor = self._control.textCursor() cursor.beginEditBlock() cursor.movePosition(QtGui.QTextCursor.StartOfLine, QtGui.QTextCursor.KeepAnchor) cursor.insertText('') cursor.endEditBlock() #--------------------------------------------------------------------------- # 'FrontendWidget' protected interface #--------------------------------------------------------------------------- def _auto_call_tip(self): """Trigger call tip automatically on open parenthesis Call tips can be requested explcitly with `_call_tip`. """ cursor = self._get_cursor() cursor.movePosition(QtGui.QTextCursor.Left) if cursor.document().characterAt(cursor.position()) == '(': # trigger auto call tip on open paren self._call_tip() def _call_tip(self): """Shows a call tip, if appropriate, at the current cursor location.""" # Decide if it makes sense to show a call tip if not self.enable_calltips or not self.kernel_client.shell_channel.is_alive(): return False cursor_pos = self._get_input_buffer_cursor_pos() code = self.input_buffer # Send the metadata request to the kernel msg_id = self.kernel_client.inspect(code, cursor_pos) pos = self._get_cursor().position() self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos) return True def _complete(self): """ Performs completion at the current cursor location. """ code = self.input_buffer cursor_pos = self._get_input_buffer_cursor_pos() # Send the completion request to the kernel msg_id = self.kernel_client.complete(code=code, cursor_pos=cursor_pos) info = self._CompletionRequest(msg_id, code, cursor_pos) self._request_info['complete'] = info def _process_execute_abort(self, msg): """ Process a reply for an aborted execution request. """ self._append_plain_text("ERROR: execution aborted\n") def _process_execute_error(self, msg): """ Process a reply for an execution request that resulted in an error. """ content = msg['content'] # If a SystemExit is passed along, this means exit() was called - also # all the ipython %exit magic syntax of '-k' to be used to keep # the kernel running if content['ename']=='SystemExit': keepkernel = content['evalue']=='-k' or content['evalue']=='True' self._keep_kernel_on_exit = keepkernel self.exit_requested.emit(self) else: traceback = ''.join(content['traceback']) self._append_plain_text(traceback) def _process_execute_ok(self, msg): """ Process a reply for a successful execution request. """ payload = msg['content'].get('payload', []) for item in payload: if not self._process_execute_payload(item): warning = 'Warning: received unknown payload of type %s' print(warning % repr(item['source'])) def _process_execute_payload(self, item): """ Process a single payload item from the list of payload items in an execution reply. Returns whether the payload was handled. """ # The basic FrontendWidget doesn't handle payloads, as they are a # mechanism for going beyond the standard Python interpreter model. return False def _show_interpreter_prompt(self): """ Shows a prompt for the interpreter. """ self._show_prompt('>>> ') def _show_interpreter_prompt_for_reply(self, msg): """ Shows a prompt for the interpreter given an 'execute_reply' message. """ self._show_interpreter_prompt() #------ Signal handlers ---------------------------------------------------- def _document_contents_change(self, position, removed, added): """ Called whenever the document's content changes. Display a call tip if appropriate. """ # Calculate where the cursor should be *after* the change: position += added if position == self._get_cursor().position(): self._auto_call_tip() #------ Trait default initializers ----------------------------------------- @default('banner') def _banner_default(self): """ Returns the standard Python banner. """ banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \ '"license" for more information.' return banner % (sys.version, sys.platform) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1615943229.0 qtconsole-5.5.2/qtconsole/history_console_widget.py0000664000175000017500000002333300000000000023500 0ustar00carloscarlos00000000000000# Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. from qtpy import QtGui from traitlets import Bool from .console_widget import ConsoleWidget class HistoryConsoleWidget(ConsoleWidget): """ A ConsoleWidget that keeps a history of the commands that have been executed and provides a readline-esque interface to this history. """ #------ Configuration ------------------------------------------------------ # If enabled, the input buffer will become "locked" to history movement when # an edit is made to a multi-line input buffer. To override the lock, use # Shift in conjunction with the standard history cycling keys. history_lock = Bool(False, config=True) #--------------------------------------------------------------------------- # 'object' interface #--------------------------------------------------------------------------- def __init__(self, *args, **kw): super().__init__(*args, **kw) # HistoryConsoleWidget protected variables. self._history = [] self._history_edits = {} self._history_index = 0 self._history_prefix = '' #--------------------------------------------------------------------------- # 'ConsoleWidget' public interface #--------------------------------------------------------------------------- def do_execute(self, source, complete, indent): """ Reimplemented to the store history. """ history = self.input_buffer if source is None else source super().do_execute(source, complete, indent) if complete: # Save the command unless it was an empty string or was identical # to the previous command. history = history.rstrip() if history and (not self._history or self._history[-1] != history): self._history.append(history) # Emulate readline: reset all history edits. self._history_edits = {} # Move the history index to the most recent item. self._history_index = len(self._history) #--------------------------------------------------------------------------- # 'ConsoleWidget' abstract interface #--------------------------------------------------------------------------- def _up_pressed(self, shift_modifier): """ Called when the up key is pressed. Returns whether to continue processing the event. """ prompt_cursor = self._get_prompt_cursor() if self._get_cursor().blockNumber() == prompt_cursor.blockNumber(): # Bail out if we're locked. if self._history_locked() and not shift_modifier: return False # Set a search prefix based on the cursor position. pos = self._get_input_buffer_cursor_pos() input_buffer = self.input_buffer # use the *shortest* of the cursor column and the history prefix # to determine if the prefix has changed n = min(pos, len(self._history_prefix)) # prefix changed, restart search from the beginning if (self._history_prefix[:n] != input_buffer[:n]): self._history_index = len(self._history) # the only time we shouldn't set the history prefix # to the line up to the cursor is if we are already # in a simple scroll (no prefix), # and the cursor is at the end of the first line # check if we are at the end of the first line c = self._get_cursor() current_pos = c.position() c.movePosition(QtGui.QTextCursor.EndOfBlock) at_eol = (c.position() == current_pos) if self._history_index == len(self._history) or \ not (self._history_prefix == '' and at_eol) or \ not (self._get_edited_history(self._history_index)[:pos] == input_buffer[:pos]): self._history_prefix = input_buffer[:pos] # Perform the search. self.history_previous(self._history_prefix, as_prefix=not shift_modifier) # Go to the first line of the prompt for seemless history scrolling. # Emulate readline: keep the cursor position fixed for a prefix # search. cursor = self._get_prompt_cursor() if self._history_prefix: cursor.movePosition(QtGui.QTextCursor.Right, n=len(self._history_prefix)) else: cursor.movePosition(QtGui.QTextCursor.EndOfBlock) self._set_cursor(cursor) return False return True def _down_pressed(self, shift_modifier): """ Called when the down key is pressed. Returns whether to continue processing the event. """ end_cursor = self._get_end_cursor() if self._get_cursor().blockNumber() == end_cursor.blockNumber(): # Bail out if we're locked. if self._history_locked() and not shift_modifier: return False # Perform the search. replaced = self.history_next(self._history_prefix, as_prefix=not shift_modifier) # Emulate readline: keep the cursor position fixed for a prefix # search. (We don't need to move the cursor to the end of the buffer # in the other case because this happens automatically when the # input buffer is set.) if self._history_prefix and replaced: cursor = self._get_prompt_cursor() cursor.movePosition(QtGui.QTextCursor.Right, n=len(self._history_prefix)) self._set_cursor(cursor) return False return True #--------------------------------------------------------------------------- # 'HistoryConsoleWidget' public interface #--------------------------------------------------------------------------- def history_previous(self, substring='', as_prefix=True): """ If possible, set the input buffer to a previous history item. Parameters ---------- substring : str, optional If specified, search for an item with this substring. as_prefix : bool, optional If True, the substring must match at the beginning (default). Returns ------- Whether the input buffer was changed. """ index = self._history_index replace = False while index > 0: index -= 1 history = self._get_edited_history(index) if history == self.input_buffer: continue if (as_prefix and history.startswith(substring)) \ or (not as_prefix and substring in history): replace = True break if replace: self._store_edits() self._history_index = index self.input_buffer = history return replace def history_next(self, substring='', as_prefix=True): """ If possible, set the input buffer to a subsequent history item. Parameters ---------- substring : str, optional If specified, search for an item with this substring. as_prefix : bool, optional If True, the substring must match at the beginning (default). Returns ------- Whether the input buffer was changed. """ index = self._history_index replace = False while index < len(self._history): index += 1 history = self._get_edited_history(index) if history == self.input_buffer: continue if (as_prefix and history.startswith(substring)) \ or (not as_prefix and substring in history): replace = True break if replace: self._store_edits() self._history_index = index self.input_buffer = history return replace def history_tail(self, n=10): """ Get the local history list. Parameters ---------- n : int The (maximum) number of history items to get. """ return self._history[-n:] #--------------------------------------------------------------------------- # 'HistoryConsoleWidget' protected interface #--------------------------------------------------------------------------- def _history_locked(self): """ Returns whether history movement is locked. """ return (self.history_lock and (self._get_edited_history(self._history_index) != self.input_buffer) and (self._get_prompt_cursor().blockNumber() != self._get_end_cursor().blockNumber())) def _get_edited_history(self, index): """ Retrieves a history item, possibly with temporary edits. """ if index in self._history_edits: return self._history_edits[index] elif index == len(self._history): return str() return self._history[index] def _set_history(self, history): """ Replace the current history with a sequence of history items. """ self._history = list(history) self._history_edits = {} self._history_index = len(self._history) def _store_edits(self): """ If there are edits to the current input buffer, store them. """ current = self.input_buffer if self._history_index == len(self._history) or \ self._history[self._history_index] != current: self._history_edits[self._history_index] = current ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1683308066.0 qtconsole-5.5.2/qtconsole/inprocess.py0000664000175000017500000000547200000000000020723 0ustar00carloscarlos00000000000000""" Defines an in-process KernelManager with signals and slots. """ from qtpy import QtCore from ipykernel.inprocess import ( InProcessHBChannel, InProcessKernelClient, InProcessKernelManager, ) from ipykernel.inprocess.channels import InProcessChannel from traitlets import Type from .util import SuperQObject from .kernel_mixins import ( QtKernelClientMixin, QtKernelManagerMixin, ) from .rich_jupyter_widget import RichJupyterWidget class QtInProcessChannel(SuperQObject, InProcessChannel): # Emitted when the channel is started. started = QtCore.Signal() # Emitted when the channel is stopped. stopped = QtCore.Signal() # Emitted when any message is received. message_received = QtCore.Signal(object) def start(self): """ Reimplemented to emit signal. """ super().start() self.started.emit() def stop(self): """ Reimplemented to emit signal. """ super().stop() self.stopped.emit() def call_handlers_later(self, *args, **kwds): """ Call the message handlers later. """ do_later = lambda: self.call_handlers(*args, **kwds) QtCore.QTimer.singleShot(0, do_later) def call_handlers(self, msg): self.message_received.emit(msg) def process_events(self): """ Process any pending GUI events. """ QtCore.QCoreApplication.instance().processEvents() def flush(self, timeout=1.0): """ Reimplemented to ensure that signals are dispatched immediately. """ super().flush() self.process_events() def closed(self): """ Function to ensure compatibility with the QtZMQSocketChannel.""" return False class QtInProcessHBChannel(SuperQObject, InProcessHBChannel): # This signal will never be fired, but it needs to exist kernel_died = QtCore.Signal() class QtInProcessKernelClient(QtKernelClientMixin, InProcessKernelClient): """ An in-process KernelManager with signals and slots. """ iopub_channel_class = Type(QtInProcessChannel) shell_channel_class = Type(QtInProcessChannel) stdin_channel_class = Type(QtInProcessChannel) hb_channel_class = Type(QtInProcessHBChannel) class QtInProcessKernelManager(QtKernelManagerMixin, InProcessKernelManager): client_class = __module__ + '.QtInProcessKernelClient' class QtInProcessRichJupyterWidget(RichJupyterWidget): """ An in-process Jupyter Widget that enables multiline editing """ def _is_complete(self, source, interactive=True): shell = self.kernel_manager.kernel.shell status, indent_spaces = \ shell.input_transformer_manager.check_complete(source) if indent_spaces is None: indent = '' else: indent = ' ' * indent_spaces return status != 'incomplete', indent ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603554021.0 qtconsole-5.5.2/qtconsole/ipython_widget.py0000664000175000017500000000025100000000000021741 0ustar00carloscarlos00000000000000import warnings warnings.warn("qtconsole.ipython_widget is deprecated; " "use qtconsole.jupyter_widget", DeprecationWarning) from .jupyter_widget import * ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1699226777.0 qtconsole-5.5.2/qtconsole/jupyter_widget.py0000664000175000017500000006064200000000000021763 0ustar00carloscarlos00000000000000"""A FrontendWidget that emulates a repl for a Jupyter kernel. This supports the additional functionality provided by Jupyter kernel. """ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. from collections import namedtuple from subprocess import Popen import sys import time from warnings import warn from qtpy import QtCore, QtGui from IPython.lib.lexers import IPythonLexer, IPython3Lexer from pygments.lexers import get_lexer_by_name from pygments.util import ClassNotFound from qtconsole import __version__ from traitlets import Bool, Unicode, observe, default from .frontend_widget import FrontendWidget from . import styles #----------------------------------------------------------------------------- # Constants #----------------------------------------------------------------------------- # Default strings to build and display input and output prompts (and separators # in between) default_in_prompt = 'In [%i]: ' default_out_prompt = 'Out[%i]: ' default_input_sep = '\n' default_output_sep = '' default_output_sep2 = '' # Base path for most payload sources. zmq_shell_source = 'ipykernel.zmqshell.ZMQInteractiveShell' if sys.platform.startswith('win'): default_editor = 'notepad' else: default_editor = '' #----------------------------------------------------------------------------- # JupyterWidget class #----------------------------------------------------------------------------- class IPythonWidget(FrontendWidget): """Dummy class for config inheritance. Destroyed below.""" class JupyterWidget(IPythonWidget): """A FrontendWidget for a Jupyter kernel.""" # If set, the 'custom_edit_requested(str, int)' signal will be emitted when # an editor is needed for a file. This overrides 'editor' and 'editor_line' # settings. custom_edit = Bool(False) custom_edit_requested = QtCore.Signal(object, object) editor = Unicode(default_editor, config=True, help=""" A command for invoking a GUI text editor. If the string contains a {filename} format specifier, it will be used. Otherwise, the filename will be appended to the end the command. To use a terminal text editor, the command should launch a new terminal, e.g. ``"gnome-terminal -- vim"``. """) editor_line = Unicode(config=True, help=""" The editor command to use when a specific line number is requested. The string should contain two format specifiers: {line} and {filename}. If this parameter is not specified, the line number option to the %edit magic will be ignored. """) style_sheet = Unicode(config=True, help=""" A CSS stylesheet. The stylesheet can contain classes for: 1. Qt: QPlainTextEdit, QFrame, QWidget, etc 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter) 3. QtConsole: .error, .in-prompt, .out-prompt, etc """) syntax_style = Unicode(config=True, help=""" If not empty, use this Pygments style for syntax highlighting. Otherwise, the style sheet is queried for Pygments style information. """) # Prompts. in_prompt = Unicode(default_in_prompt, config=True) out_prompt = Unicode(default_out_prompt, config=True) input_sep = Unicode(default_input_sep, config=True) output_sep = Unicode(default_output_sep, config=True) output_sep2 = Unicode(default_output_sep2, config=True) # JupyterWidget protected class variables. _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number']) _payload_source_edit = 'edit_magic' _payload_source_exit = 'ask_exit' _payload_source_next_input = 'set_next_input' _payload_source_page = 'page' _retrying_history_request = False _starting = False #--------------------------------------------------------------------------- # 'object' interface #--------------------------------------------------------------------------- def __init__(self, *args, **kw): super().__init__(*args, **kw) # JupyterWidget protected variables. self._payload_handlers = { self._payload_source_edit : self._handle_payload_edit, self._payload_source_exit : self._handle_payload_exit, self._payload_source_page : self._handle_payload_page, self._payload_source_next_input : self._handle_payload_next_input } self._previous_prompt_obj = None self._keep_kernel_on_exit = None # Initialize widget styling. if self.style_sheet: self._style_sheet_changed() self._syntax_style_changed() else: self.set_default_style() # Initialize language name. self.language_name = None self._prompt_requested = False #--------------------------------------------------------------------------- # 'BaseFrontendMixin' abstract interface # # For JupyterWidget, override FrontendWidget methods which implement the # BaseFrontend Mixin abstract interface #--------------------------------------------------------------------------- def _handle_complete_reply(self, rep): """Support Jupyter's improved completion machinery. """ self.log.debug("complete: %s", rep.get('content', '')) cursor = self._get_cursor() info = self._request_info.get('complete') if (info and info.id == rep['parent_header']['msg_id'] and info.pos == self._get_input_buffer_cursor_pos() and info.code == self.input_buffer): content = rep['content'] matches = content['matches'] start = content['cursor_start'] end = content['cursor_end'] start = max(start, 0) end = max(end, start) # Move the control's cursor to the desired end point cursor_pos = self._get_input_buffer_cursor_pos() if end < cursor_pos: cursor.movePosition(QtGui.QTextCursor.Left, n=(cursor_pos - end)) elif end > cursor_pos: cursor.movePosition(QtGui.QTextCursor.Right, n=(end - cursor_pos)) # This line actually applies the move to control's cursor self._control.setTextCursor(cursor) offset = end - start # Move the local cursor object to the start of the match and # complete. cursor.movePosition(QtGui.QTextCursor.Left, n=offset) self._complete_with_items(cursor, matches) def _handle_execute_reply(self, msg): """Support prompt requests. """ msg_id = msg['parent_header'].get('msg_id') info = self._request_info['execute'].get(msg_id) if info and info.kind == 'prompt': self._prompt_requested = False content = msg['content'] if content['status'] == 'aborted': self._show_interpreter_prompt() else: number = content['execution_count'] + 1 self._show_interpreter_prompt(number) self._request_info['execute'].pop(msg_id) else: super()._handle_execute_reply(msg) def _handle_history_reply(self, msg): """ Handle history tail replies, which are only supported by Jupyter kernels. """ content = msg['content'] if 'history' not in content: self.log.error("History request failed: %r"%content) if content.get('status', '') == 'aborted' and \ not self._retrying_history_request: # a *different* action caused this request to be aborted, so # we should try again. self.log.error("Retrying aborted history request") # prevent multiple retries of aborted requests: self._retrying_history_request = True # wait out the kernel's queue flush, which is currently timed at 0.1s time.sleep(0.25) self.kernel_client.history(hist_access_type='tail',n=1000) else: self._retrying_history_request = False return # reset retry flag self._retrying_history_request = False history_items = content['history'] self.log.debug("Received history reply with %i entries", len(history_items)) items = [] last_cell = "" for _, _, cell in history_items: cell = cell.rstrip() if cell != last_cell: items.append(cell) last_cell = cell self._set_history(items) def _insert_other_input(self, cursor, content, remote=True): """Insert function for input from other frontends""" n = content.get('execution_count', 0) prompt = self._make_in_prompt(n, remote=remote) cont_prompt = self._make_continuation_prompt(self._prompt, remote=remote) cursor.insertText('\n') for i, line in enumerate(content['code'].strip().split('\n')): if i == 0: self._insert_html(cursor, prompt) else: self._insert_html(cursor, cont_prompt) self._insert_plain_text(cursor, line + '\n') # Update current prompt number self._update_prompt(n + 1) def _handle_execute_input(self, msg): """Handle an execute_input message""" self.log.debug("execute_input: %s", msg.get('content', '')) if self.include_output(msg): self._append_custom( self._insert_other_input, msg['content'], before_prompt=True) elif not self._prompt: self._append_custom( self._insert_other_input, msg['content'], before_prompt=True, remote=False) def _handle_execute_result(self, msg): """Handle an execute_result message""" self.log.debug("execute_result: %s", msg.get('content', '')) if self.include_output(msg): self.flush_clearoutput() content = msg['content'] prompt_number = content.get('execution_count', 0) data = content['data'] if 'text/plain' in data: self._append_plain_text(self.output_sep, before_prompt=True) self._append_html( self._make_out_prompt(prompt_number, remote=not self.from_here(msg)), before_prompt=True ) text = data['text/plain'] # If the repr is multiline, make sure we start on a new line, # so that its lines are aligned. if "\n" in text and not self.output_sep.endswith("\n"): self._append_plain_text('\n', before_prompt=True) self._append_plain_text(text + self.output_sep2, before_prompt=True) if not self.from_here(msg): self._append_plain_text('\n', before_prompt=True) def _handle_display_data(self, msg): """The base handler for the ``display_data`` message.""" # For now, we don't display data from other frontends, but we # eventually will as this allows all frontends to monitor the display # data. But we need to figure out how to handle this in the GUI. if self.include_output(msg): self.flush_clearoutput() data = msg['content']['data'] # In the regular JupyterWidget, we simply print the plain text # representation. if 'text/plain' in data: text = data['text/plain'] self._append_plain_text(text, True) # This newline seems to be needed for text and html output. self._append_plain_text('\n', True) def _handle_kernel_info_reply(self, rep): """Handle kernel info replies.""" content = rep['content'] self.language_name = content['language_info']['name'] pygments_lexer = content['language_info'].get('pygments_lexer', '') try: # Other kernels with pygments_lexer info will have to be # added here by hand. if pygments_lexer == 'ipython3': lexer = IPython3Lexer() elif pygments_lexer == 'ipython2': lexer = IPythonLexer() else: lexer = get_lexer_by_name(self.language_name) self._highlighter._lexer = lexer except ClassNotFound: pass self.kernel_banner = content.get('banner', '') if self._starting: # finish handling started channels self._starting = False super()._started_channels() def _started_channels(self): """Make a history request""" self._starting = True self.kernel_client.kernel_info() self.kernel_client.history(hist_access_type='tail', n=1000) #--------------------------------------------------------------------------- # 'FrontendWidget' protected interface #--------------------------------------------------------------------------- def _process_execute_error(self, msg): """Handle an execute_error message""" self.log.debug("execute_error: %s", msg.get('content', '')) content = msg['content'] traceback = '\n'.join(content['traceback']) + '\n' if False: # FIXME: For now, tracebacks come as plain text, so we can't # use the html renderer yet. Once we refactor ultratb to # produce properly styled tracebacks, this branch should be the # default traceback = traceback.replace(' ', ' ') traceback = traceback.replace('\n', '
') ename = content['ename'] ename_styled = '%s' % ename traceback = traceback.replace(ename, ename_styled) self._append_html(traceback) else: # This is the fallback for now, using plain text with ansi # escapes self._append_plain_text(traceback, before_prompt=not self.from_here(msg)) def _process_execute_payload(self, item): """ Reimplemented to dispatch payloads to handler methods. """ handler = self._payload_handlers.get(item['source']) if handler is None: # We have no handler for this type of payload, simply ignore it return False else: handler(item) return True def _show_interpreter_prompt(self, number=None): """ Reimplemented for IPython-style prompts. """ # If a number was not specified, make a prompt number request. if number is None: if self._prompt_requested: # Already asked for prompt, avoid multiple prompts. return self._prompt_requested = True msg_id = self.kernel_client.execute('', silent=True) info = self._ExecutionRequest(msg_id, 'prompt', False) self._request_info['execute'][msg_id] = info return # Show a new prompt and save information about it so that it can be # updated later if the prompt number turns out to be wrong. self._prompt_sep = self.input_sep self._show_prompt(self._make_in_prompt(number), html=True) block = self._control.document().lastBlock() length = len(self._prompt) self._previous_prompt_obj = self._PromptBlock(block, length, number) # Update continuation prompt to reflect (possibly) new prompt length. self._set_continuation_prompt( self._make_continuation_prompt(self._prompt), html=True) def _update_prompt(self, new_prompt_number): """Replace the last displayed prompt with a new one.""" if self._previous_prompt_obj is None: return block = self._previous_prompt_obj.block # Make sure the prompt block has not been erased. if block.isValid() and block.text(): # Remove the old prompt and insert a new prompt. cursor = QtGui.QTextCursor(block) cursor.movePosition(QtGui.QTextCursor.Right, QtGui.QTextCursor.KeepAnchor, self._previous_prompt_obj.length) prompt = self._make_in_prompt(new_prompt_number) self._prompt = self._insert_html_fetching_plain_text( cursor, prompt) # When the HTML is inserted, Qt blows away the syntax # highlighting for the line, so we need to rehighlight it. self._highlighter.rehighlightBlock(cursor.block()) # Update the prompt cursor self._prompt_cursor.setPosition(cursor.position() - 1) # Store the updated prompt. block = self._control.document().lastBlock() length = len(self._prompt) self._previous_prompt_obj = self._PromptBlock(block, length, new_prompt_number) def _show_interpreter_prompt_for_reply(self, msg): """ Reimplemented for IPython-style prompts. """ # Update the old prompt number if necessary. content = msg['content'] # abort replies do not have any keys: if content['status'] == 'aborted': if self._previous_prompt_obj: previous_prompt_number = self._previous_prompt_obj.number else: previous_prompt_number = 0 else: previous_prompt_number = content['execution_count'] if self._previous_prompt_obj and \ self._previous_prompt_obj.number != previous_prompt_number: self._update_prompt(previous_prompt_number) self._previous_prompt_obj = None # Show a new prompt with the kernel's estimated prompt number. self._show_interpreter_prompt(previous_prompt_number + 1) #--------------------------------------------------------------------------- # 'JupyterWidget' interface #--------------------------------------------------------------------------- def set_default_style(self, colors='lightbg'): """ Sets the widget style to the class defaults. Parameters ---------- colors : str, optional (default lightbg) Whether to use the default light background or dark background or B&W style. """ colors = colors.lower() if colors=='lightbg': self.style_sheet = styles.default_light_style_sheet self.syntax_style = styles.default_light_syntax_style elif colors=='linux': self.style_sheet = styles.default_dark_style_sheet self.syntax_style = styles.default_dark_syntax_style elif colors=='nocolor': self.style_sheet = styles.default_bw_style_sheet self.syntax_style = styles.default_bw_syntax_style else: raise KeyError("No such color scheme: %s"%colors) #--------------------------------------------------------------------------- # 'JupyterWidget' protected interface #--------------------------------------------------------------------------- def _edit(self, filename, line=None): """ Opens a Python script for editing. Parameters ---------- filename : str A path to a local system file. line : int, optional A line of interest in the file. """ if self.custom_edit: self.custom_edit_requested.emit(filename, line) elif not self.editor: self._append_plain_text('No default editor available.\n' 'Specify a GUI text editor in the `JupyterWidget.editor` ' 'configurable to enable the %edit magic') else: try: filename = '"%s"' % filename if line and self.editor_line: command = self.editor_line.format(filename=filename, line=line) else: try: command = self.editor.format() except KeyError: command = self.editor.format(filename=filename) else: command += ' ' + filename except KeyError: self._append_plain_text('Invalid editor command.\n') else: try: Popen(command, shell=True) except OSError: msg = 'Opening editor with command "%s" failed.\n' self._append_plain_text(msg % command) def _make_in_prompt(self, number, remote=False): """ Given a prompt number, returns an HTML In prompt. """ try: body = self.in_prompt % number except TypeError: # allow in_prompt to leave out number, e.g. '>>> ' from xml.sax.saxutils import escape body = escape(self.in_prompt) if remote: body = self.other_output_prefix + body return '%s' % body def _make_continuation_prompt(self, prompt, remote=False): """ Given a plain text version of an In prompt, returns an HTML continuation prompt. """ end_chars = '...: ' space_count = len(prompt.lstrip('\n')) - len(end_chars) if remote: space_count += len(self.other_output_prefix.rsplit('\n')[-1]) body = ' ' * space_count + end_chars return '%s' % body def _make_out_prompt(self, number, remote=False): """ Given a prompt number, returns an HTML Out prompt. """ try: body = self.out_prompt % number except TypeError: # allow out_prompt to leave out number, e.g. '<<< ' from xml.sax.saxutils import escape body = escape(self.out_prompt) if remote: body = self.other_output_prefix + body return '%s' % body #------ Payload handlers -------------------------------------------------- # Payload handlers with a generic interface: each takes the opaque payload # dict, unpacks it and calls the underlying functions with the necessary # arguments. def _handle_payload_edit(self, item): self._edit(item['filename'], item['line_number']) def _handle_payload_exit(self, item): self._keep_kernel_on_exit = item['keepkernel'] self.exit_requested.emit(self) def _handle_payload_next_input(self, item): self.input_buffer = item['text'] def _handle_payload_page(self, item): # Since the plain text widget supports only a very small subset of HTML # and we have no control over the HTML source, we only page HTML # payloads in the rich text widget. data = item['data'] if 'text/html' in data and self.kind == 'rich': self._page(data['text/html'], html=True) else: self._page(data['text/plain'], html=False) #------ Trait change handlers -------------------------------------------- @observe('style_sheet') def _style_sheet_changed(self, changed=None): """ Set the style sheets of the underlying widgets. """ self.setStyleSheet(self.style_sheet) if self._control is not None: self._control.document().setDefaultStyleSheet(self.style_sheet) if self._page_control is not None: self._page_control.document().setDefaultStyleSheet(self.style_sheet) @observe('syntax_style') def _syntax_style_changed(self, changed=None): """ Set the style for the syntax highlighter. """ if self._highlighter is None: # ignore premature calls return if self.syntax_style: self._highlighter.set_style(self.syntax_style) self._ansi_processor.set_background_color(self.syntax_style) else: self._highlighter.set_style_sheet(self.style_sheet) #------ Trait default initializers ----------------------------------------- @default('banner') def _banner_default(self): return "Jupyter QtConsole {version}\n".format(version=__version__) # Clobber IPythonWidget above: class IPythonWidget(JupyterWidget): """Deprecated class; use JupyterWidget.""" def __init__(self, *a, **kw): warn("IPythonWidget is deprecated; use JupyterWidget", DeprecationWarning) super().__init__(*a, **kw) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1615943229.0 qtconsole-5.5.2/qtconsole/kernel_mixins.py0000664000175000017500000000342500000000000021561 0ustar00carloscarlos00000000000000"""Defines a KernelManager that provides signals and slots.""" # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. from qtpy import QtCore from traitlets import HasTraits, Type from .util import MetaQObjectHasTraits, SuperQObject from .comms import CommManager class QtKernelRestarterMixin(MetaQObjectHasTraits('NewBase', (HasTraits, SuperQObject), {})): _timer = None class QtKernelManagerMixin(MetaQObjectHasTraits('NewBase', (HasTraits, SuperQObject), {})): """ A KernelClient that provides signals and slots. """ kernel_restarted = QtCore.Signal() class QtKernelClientMixin(MetaQObjectHasTraits('NewBase', (HasTraits, SuperQObject), {})): """ A KernelClient that provides signals and slots. """ # Emitted when the kernel client has started listening. started_channels = QtCore.Signal() # Emitted when the kernel client has stopped listening. stopped_channels = QtCore.Signal() #--------------------------------------------------------------------------- # 'KernelClient' interface #--------------------------------------------------------------------------- def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.comm_manager = None #------ Channel management ------------------------------------------------- def start_channels(self, *args, **kw): """ Reimplemented to emit signal. """ super().start_channels(*args, **kw) self.started_channels.emit() self.comm_manager = CommManager(parent=self, kernel_client=self) def stop_channels(self): """ Reimplemented to emit signal. """ super().stop_channels() self.stopped_channels.emit() self.comm_manager = None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1615943229.0 qtconsole-5.5.2/qtconsole/kill_ring.py0000664000175000017500000000732100000000000020663 0ustar00carloscarlos00000000000000""" A generic Emacs-style kill ring, as well as a Qt-specific version. """ #----------------------------------------------------------------------------- # Imports #----------------------------------------------------------------------------- # System library imports from qtpy import QtCore, QtWidgets, QtGui #----------------------------------------------------------------------------- # Classes #----------------------------------------------------------------------------- class KillRing(object): """ A generic Emacs-style kill ring. """ def __init__(self): self.clear() def clear(self): """ Clears the kill ring. """ self._index = -1 self._ring = [] def kill(self, text): """ Adds some killed text to the ring. """ self._ring.append(text) def yank(self): """ Yank back the most recently killed text. Returns ------- A text string or None. """ self._index = len(self._ring) return self.rotate() def rotate(self): """ Rotate the kill ring, then yank back the new top. Returns ------- A text string or None. """ self._index -= 1 if self._index >= 0: return self._ring[self._index] return None class QtKillRing(QtCore.QObject): """ A kill ring attached to Q[Plain]TextEdit. """ #-------------------------------------------------------------------------- # QtKillRing interface #-------------------------------------------------------------------------- def __init__(self, text_edit): """ Create a kill ring attached to the specified Qt text edit. """ assert isinstance(text_edit, (QtWidgets.QTextEdit, QtWidgets.QPlainTextEdit)) super().__init__() self._ring = KillRing() self._prev_yank = None self._skip_cursor = False self._text_edit = text_edit text_edit.cursorPositionChanged.connect(self._cursor_position_changed) def clear(self): """ Clears the kill ring. """ self._ring.clear() self._prev_yank = None def kill(self, text): """ Adds some killed text to the ring. """ self._ring.kill(text) def kill_cursor(self, cursor): """ Kills the text selected by the give cursor. """ text = cursor.selectedText() if text: cursor.removeSelectedText() self.kill(text) def yank(self): """ Yank back the most recently killed text. """ text = self._ring.yank() if text: self._skip_cursor = True cursor = self._text_edit.textCursor() cursor.insertText(text) self._prev_yank = text def rotate(self): """ Rotate the kill ring, then yank back the new top. """ if self._prev_yank: text = self._ring.rotate() if text: self._skip_cursor = True cursor = self._text_edit.textCursor() cursor.movePosition(QtGui.QTextCursor.Left, QtGui.QTextCursor.KeepAnchor, n = len(self._prev_yank)) cursor.insertText(text) self._prev_yank = text #-------------------------------------------------------------------------- # Protected interface #-------------------------------------------------------------------------- #------ Signal handlers ---------------------------------------------------- def _cursor_position_changed(self): if self._skip_cursor: self._skip_cursor = False else: self._prev_yank = None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1653842606.0 qtconsole-5.5.2/qtconsole/mainwindow.py0000664000175000017500000011300200000000000021057 0ustar00carloscarlos00000000000000"""The Qt MainWindow for the QtConsole This is a tabbed pseudo-terminal of Jupyter sessions, with a menu bar for common actions. """ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import sys import webbrowser from functools import partial from threading import Thread from jupyter_core.paths import jupyter_runtime_dir from pygments.styles import get_all_styles from qtpy import QtGui, QtCore, QtWidgets from qtconsole import styles from qtconsole.jupyter_widget import JupyterWidget from qtconsole.usage import gui_reference def background(f): """call a function in a simple thread, to prevent blocking""" t = Thread(target=f) t.start() return t class MainWindow(QtWidgets.QMainWindow): #--------------------------------------------------------------------------- # 'object' interface #--------------------------------------------------------------------------- def __init__(self, app, confirm_exit=True, new_frontend_factory=None, slave_frontend_factory=None, connection_frontend_factory=None, parent=None ): """ Create a tabbed MainWindow for managing FrontendWidgets Parameters ---------- app : reference to QApplication parent confirm_exit : bool, optional Whether we should prompt on close of tabs new_frontend_factory : callable A callable that returns a new JupyterWidget instance, attached to its own running kernel. slave_frontend_factory : callable A callable that takes an existing JupyterWidget, and returns a new JupyterWidget instance, attached to the same kernel. """ super().__init__(parent=parent) self._kernel_counter = 0 self._external_kernel_counter = 0 self._app = app self.confirm_exit = confirm_exit self.new_frontend_factory = new_frontend_factory self.slave_frontend_factory = slave_frontend_factory self.connection_frontend_factory = connection_frontend_factory self.tab_widget = QtWidgets.QTabWidget(self) self.tab_widget.setDocumentMode(True) self.tab_widget.setTabsClosable(True) self.tab_widget.tabCloseRequested[int].connect(self.close_tab) self.setCentralWidget(self.tab_widget) # hide tab bar at first, since we have no tabs: self.tab_widget.tabBar().setVisible(False) # prevent focus in tab bar self.tab_widget.setFocusPolicy(QtCore.Qt.NoFocus) def update_tab_bar_visibility(self): """ update visibility of the tabBar depending of the number of tab 0 or 1 tab, tabBar hidden 2+ tabs, tabBar visible send a self.close if number of tab ==0 need to be called explicitly, or be connected to tabInserted/tabRemoved """ if self.tab_widget.count() <= 1: self.tab_widget.tabBar().setVisible(False) else: self.tab_widget.tabBar().setVisible(True) if self.tab_widget.count()==0 : self.close() @property def next_kernel_id(self): """constantly increasing counter for kernel IDs""" c = self._kernel_counter self._kernel_counter += 1 return c @property def next_external_kernel_id(self): """constantly increasing counter for external kernel IDs""" c = self._external_kernel_counter self._external_kernel_counter += 1 return c @property def active_frontend(self): return self.tab_widget.currentWidget() def create_tab_with_new_frontend(self): """create a new frontend and attach it to a new tab""" widget = self.new_frontend_factory() self.add_tab_with_frontend(widget) def set_window_title(self): """Set the title of the console window""" old_title = self.windowTitle() title, ok = QtWidgets.QInputDialog.getText(self, "Rename Window", "New title:", text=old_title) if ok: self.setWindowTitle(title) def create_tab_with_existing_kernel(self): """create a new frontend attached to an external kernel in a new tab""" connection_file, file_type = QtWidgets.QFileDialog.getOpenFileName(self, "Connect to Existing Kernel", jupyter_runtime_dir(), "Connection file (*.json)") if not connection_file: return widget = self.connection_frontend_factory(connection_file) name = "external {}".format(self.next_external_kernel_id) self.add_tab_with_frontend(widget, name=name) def create_tab_with_current_kernel(self): """create a new frontend attached to the same kernel as the current tab""" current_widget = self.tab_widget.currentWidget() current_widget_index = self.tab_widget.indexOf(current_widget) current_widget_name = self.tab_widget.tabText(current_widget_index) widget = self.slave_frontend_factory(current_widget) if 'slave' in current_widget_name: # don't keep stacking slaves name = current_widget_name else: name = '(%s) slave' % current_widget_name self.add_tab_with_frontend(widget,name=name) def set_tab_title(self): """Set the title of the current tab""" old_title = self.tab_widget.tabText(self.tab_widget.currentIndex()) title, ok = QtWidgets.QInputDialog.getText(self, "Rename Tab", "New title:", text=old_title) if ok: self.tab_widget.setTabText(self.tab_widget.currentIndex(), title) def close_tab(self,current_tab): """ Called when you need to try to close a tab. It takes the number of the tab to be closed as argument, or a reference to the widget inside this tab """ # let's be sure "tab" and "closing widget" are respectively the index # of the tab to close and a reference to the frontend to close if type(current_tab) is not int : current_tab = self.tab_widget.indexOf(current_tab) closing_widget=self.tab_widget.widget(current_tab) # when trying to be closed, widget might re-send a request to be # closed again, but will be deleted when event will be processed. So # need to check that widget still exists and skip if not. One example # of this is when 'exit' is sent in a slave tab. 'exit' will be # re-sent by this function on the master widget, which ask all slave # widgets to exit if closing_widget is None: return #get a list of all slave widgets on the same kernel. slave_tabs = self.find_slave_widgets(closing_widget) keepkernel = None #Use the prompt by default if hasattr(closing_widget,'_keep_kernel_on_exit'): #set by exit magic keepkernel = closing_widget._keep_kernel_on_exit # If signal sent by exit magic (_keep_kernel_on_exit, exist and not None) # we set local slave tabs._hidden to True to avoid prompting for kernel # restart when they get the signal. and then "forward" the 'exit' # to the main window if keepkernel is not None: for tab in slave_tabs: tab._hidden = True if closing_widget in slave_tabs: try : self.find_master_tab(closing_widget).execute('exit') except AttributeError: self.log.info("Master already closed or not local, closing only current tab") self.tab_widget.removeTab(current_tab) self.update_tab_bar_visibility() return kernel_client = closing_widget.kernel_client kernel_manager = closing_widget.kernel_manager if keepkernel is None and not closing_widget._confirm_exit: # don't prompt, just terminate the kernel if we own it # or leave it alone if we don't keepkernel = closing_widget._existing if keepkernel is None: #show prompt if kernel_client and kernel_client.channels_running: title = self.window().windowTitle() cancel = QtWidgets.QMessageBox.Cancel okay = QtWidgets.QMessageBox.Ok if closing_widget._may_close: msg = "You are closing the tab : "+'"'+self.tab_widget.tabText(current_tab)+'"' info = "Would you like to quit the Kernel and close all attached Consoles as well?" justthis = QtWidgets.QPushButton("&No, just this Tab", self) justthis.setShortcut('N') closeall = QtWidgets.QPushButton("&Yes, close all", self) closeall.setShortcut('Y') # allow ctrl-d ctrl-d exit, like in terminal closeall.setShortcut('Ctrl+D') box = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Question, title, msg) box.setInformativeText(info) box.addButton(cancel) box.addButton(justthis, QtWidgets.QMessageBox.NoRole) box.addButton(closeall, QtWidgets.QMessageBox.YesRole) box.setDefaultButton(closeall) box.setEscapeButton(cancel) pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64))) box.setIconPixmap(pixmap) reply = box.exec_() if reply == 1: # close All for slave in slave_tabs: background(slave.kernel_client.stop_channels) self.tab_widget.removeTab(self.tab_widget.indexOf(slave)) kernel_manager.shutdown_kernel() self.tab_widget.removeTab(current_tab) background(kernel_client.stop_channels) elif reply == 0: # close Console if not closing_widget._existing: # Have kernel: don't quit, just close the tab closing_widget.execute("exit True") self.tab_widget.removeTab(current_tab) background(kernel_client.stop_channels) else: reply = QtWidgets.QMessageBox.question(self, title, "Are you sure you want to close this Console?"+ "\nThe Kernel and other Consoles will remain active.", okay|cancel, defaultButton=okay ) if reply == okay: self.tab_widget.removeTab(current_tab) elif keepkernel: #close console but leave kernel running (no prompt) self.tab_widget.removeTab(current_tab) background(kernel_client.stop_channels) else: #close console and kernel (no prompt) self.tab_widget.removeTab(current_tab) if kernel_client and kernel_client.channels_running: for slave in slave_tabs: background(slave.kernel_client.stop_channels) self.tab_widget.removeTab(self.tab_widget.indexOf(slave)) if kernel_manager: kernel_manager.shutdown_kernel() background(kernel_client.stop_channels) self.update_tab_bar_visibility() def add_tab_with_frontend(self,frontend,name=None): """ insert a tab with a given frontend in the tab bar, and give it a name """ if not name: name = 'kernel %i' % self.next_kernel_id self.tab_widget.addTab(frontend,name) self.update_tab_bar_visibility() self.make_frontend_visible(frontend) frontend.exit_requested.connect(self.close_tab) def next_tab(self): self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()+1)) def prev_tab(self): self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()-1)) def make_frontend_visible(self,frontend): widget_index=self.tab_widget.indexOf(frontend) if widget_index > 0 : self.tab_widget.setCurrentIndex(widget_index) def find_master_tab(self,tab,as_list=False): """ Try to return the frontend that owns the kernel attached to the given widget/tab. Only finds frontend owned by the current application. Selection based on port of the kernel might be inaccurate if several kernel on different ip use same port number. This function does the conversion tabNumber/widget if needed. Might return None if no master widget (non local kernel) Will crash if more than 1 masterWidget When asList set to True, always return a list of widget(s) owning the kernel. The list might be empty or containing several Widget. """ #convert from/to int/richIpythonWidget if needed if isinstance(tab, int): tab = self.tab_widget.widget(tab) km=tab.kernel_client #build list of all widgets widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())] # widget that are candidate to be the owner of the kernel does have all the same port of the curent widget # And should have a _may_close attribute filtered_widget_list = [ widget for widget in widget_list if widget.kernel_client.connection_file == km.connection_file and hasattr(widget,'_may_close') ] # the master widget is the one that may close the kernel master_widget= [ widget for widget in filtered_widget_list if widget._may_close] if as_list: return master_widget assert(len(master_widget)<=1 ) if len(master_widget)==0: return None return master_widget[0] def find_slave_widgets(self,tab): """return all the frontends that do not own the kernel attached to the given widget/tab. Only find frontends owned by the current application. Selection based on connection file of the kernel. This function does the conversion tabNumber/widget if needed. """ #convert from/to int/richIpythonWidget if needed if isinstance(tab, int): tab = self.tab_widget.widget(tab) km=tab.kernel_client #build list of all widgets widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())] # widget that are candidate not to be the owner of the kernel does have all the same port of the curent widget filtered_widget_list = ( widget for widget in widget_list if widget.kernel_client.connection_file == km.connection_file) # Get a list of all widget owning the same kernel and removed it from # the previous cadidate. (better using sets ?) master_widget_list = self.find_master_tab(tab, as_list=True) slave_list = [widget for widget in filtered_widget_list if widget not in master_widget_list] return slave_list # Populate the menu bar with common actions and shortcuts def add_menu_action(self, menu, action, defer_shortcut=False): """Add action to menu as well as self So that when the menu bar is invisible, its actions are still available. If defer_shortcut is True, set the shortcut context to widget-only, where it will avoid conflict with shortcuts already bound to the widgets themselves. """ menu.addAction(action) self.addAction(action) if defer_shortcut: action.setShortcutContext(QtCore.Qt.WidgetShortcut) def init_menu_bar(self): #create menu in the order they should appear in the menu bar self.init_file_menu() self.init_edit_menu() self.init_view_menu() self.init_kernel_menu() self.init_window_menu() self.init_help_menu() def init_file_menu(self): self.file_menu = self.menuBar().addMenu("&File") self.new_kernel_tab_act = QtWidgets.QAction("New Tab with &New kernel", self, shortcut="Ctrl+T", triggered=self.create_tab_with_new_frontend) self.add_menu_action(self.file_menu, self.new_kernel_tab_act) self.slave_kernel_tab_act = QtWidgets.QAction("New Tab with Sa&me kernel", self, shortcut="Ctrl+Shift+T", triggered=self.create_tab_with_current_kernel) self.add_menu_action(self.file_menu, self.slave_kernel_tab_act) self.existing_kernel_tab_act = QtWidgets.QAction("New Tab with &Existing kernel", self, shortcut="Alt+T", triggered=self.create_tab_with_existing_kernel) self.add_menu_action(self.file_menu, self.existing_kernel_tab_act) self.file_menu.addSeparator() self.close_action=QtWidgets.QAction("&Close Tab", self, shortcut=QtGui.QKeySequence.Close, triggered=self.close_active_frontend ) self.add_menu_action(self.file_menu, self.close_action) self.export_action=QtWidgets.QAction("&Save to HTML/XHTML", self, shortcut=QtGui.QKeySequence.Save, triggered=self.export_action_active_frontend ) self.add_menu_action(self.file_menu, self.export_action, True) self.file_menu.addSeparator() printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print) if printkey.matches("Ctrl+P") and sys.platform != 'darwin': # Only override the default if there is a collision. # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX. printkey = "Ctrl+Shift+P" self.print_action = QtWidgets.QAction("&Print", self, shortcut=printkey, triggered=self.print_action_active_frontend) self.add_menu_action(self.file_menu, self.print_action, True) if sys.platform != 'darwin': # OSX always has Quit in the Application menu, only add it # to the File menu elsewhere. self.file_menu.addSeparator() self.quit_action = QtWidgets.QAction("&Quit", self, shortcut=QtGui.QKeySequence.Quit, triggered=self.close, ) self.add_menu_action(self.file_menu, self.quit_action) def init_edit_menu(self): self.edit_menu = self.menuBar().addMenu("&Edit") self.undo_action = QtWidgets.QAction("&Undo", self, shortcut=QtGui.QKeySequence.Undo, statusTip="Undo last action if possible", triggered=self.undo_active_frontend ) self.add_menu_action(self.edit_menu, self.undo_action) self.redo_action = QtWidgets.QAction("&Redo", self, shortcut=QtGui.QKeySequence.Redo, statusTip="Redo last action if possible", triggered=self.redo_active_frontend) self.add_menu_action(self.edit_menu, self.redo_action) self.edit_menu.addSeparator() self.cut_action = QtWidgets.QAction("&Cut", self, shortcut=QtGui.QKeySequence.Cut, triggered=self.cut_active_frontend ) self.add_menu_action(self.edit_menu, self.cut_action, True) self.copy_action = QtWidgets.QAction("&Copy", self, shortcut=QtGui.QKeySequence.Copy, triggered=self.copy_active_frontend ) self.add_menu_action(self.edit_menu, self.copy_action, True) self.copy_raw_action = QtWidgets.QAction("Copy (&Raw Text)", self, shortcut="Ctrl+Shift+C", triggered=self.copy_raw_active_frontend ) self.add_menu_action(self.edit_menu, self.copy_raw_action, True) self.paste_action = QtWidgets.QAction("&Paste", self, shortcut=QtGui.QKeySequence.Paste, triggered=self.paste_active_frontend ) self.add_menu_action(self.edit_menu, self.paste_action, True) self.edit_menu.addSeparator() selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll) if selectall.matches("Ctrl+A") and sys.platform != 'darwin': # Only override the default if there is a collision. # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX. selectall = "Ctrl+Shift+A" self.select_all_action = QtWidgets.QAction("Select Cell/&All", self, shortcut=selectall, triggered=self.select_all_active_frontend ) self.add_menu_action(self.edit_menu, self.select_all_action, True) def init_view_menu(self): self.view_menu = self.menuBar().addMenu("&View") if sys.platform != 'darwin': # disable on OSX, where there is always a menu bar self.toggle_menu_bar_act = QtWidgets.QAction("Toggle &Menu Bar", self, shortcut="Ctrl+Shift+M", statusTip="Toggle visibility of menubar", triggered=self.toggle_menu_bar) self.add_menu_action(self.view_menu, self.toggle_menu_bar_act) fs_key = "Ctrl+Meta+F" if sys.platform == 'darwin' else "F11" self.full_screen_act = QtWidgets.QAction("&Full Screen", self, shortcut=fs_key, statusTip="Toggle between Fullscreen and Normal Size", triggered=self.toggleFullScreen) self.add_menu_action(self.view_menu, self.full_screen_act) self.view_menu.addSeparator() self.increase_font_size = QtWidgets.QAction("Zoom &In", self, shortcut=QtGui.QKeySequence.ZoomIn, triggered=self.increase_font_size_active_frontend ) self.add_menu_action(self.view_menu, self.increase_font_size, True) self.decrease_font_size = QtWidgets.QAction("Zoom &Out", self, shortcut=QtGui.QKeySequence.ZoomOut, triggered=self.decrease_font_size_active_frontend ) self.add_menu_action(self.view_menu, self.decrease_font_size, True) self.reset_font_size = QtWidgets.QAction("Zoom &Reset", self, shortcut="Ctrl+0", triggered=self.reset_font_size_active_frontend ) self.add_menu_action(self.view_menu, self.reset_font_size, True) self.view_menu.addSeparator() self.clear_action = QtWidgets.QAction("&Clear Screen", self, shortcut='Ctrl+L', statusTip="Clear the console", triggered=self.clear_active_frontend) self.add_menu_action(self.view_menu, self.clear_action) self.completion_menu = self.view_menu.addMenu("&Completion type") completion_group = QtWidgets.QActionGroup(self) active_frontend_completion = self.active_frontend.gui_completion ncurses_completion_action = QtWidgets.QAction( "&ncurses", self, triggered=lambda: self.set_completion_widget_active_frontend( 'ncurses')) ncurses_completion_action.setCheckable(True) ncurses_completion_action.setChecked( active_frontend_completion == 'ncurses') droplist_completion_action = QtWidgets.QAction( "&droplist", self, triggered=lambda: self.set_completion_widget_active_frontend( 'droplist')) droplist_completion_action.setCheckable(True) droplist_completion_action.setChecked( active_frontend_completion == 'droplist') plain_commpletion_action = QtWidgets.QAction( "&plain", self, triggered=lambda: self.set_completion_widget_active_frontend( 'plain')) plain_commpletion_action.setCheckable(True) plain_commpletion_action.setChecked( active_frontend_completion == 'plain') completion_group.addAction(ncurses_completion_action) completion_group.addAction(droplist_completion_action) completion_group.addAction(plain_commpletion_action) self.completion_menu.addAction(ncurses_completion_action) self.completion_menu.addAction(droplist_completion_action) self.completion_menu.addAction(plain_commpletion_action) self.completion_menu.setDefaultAction(ncurses_completion_action) self.pager_menu = self.view_menu.addMenu("&Pager") hsplit_action = QtWidgets.QAction(".. &Horizontal Split", self, triggered=lambda: self.set_paging_active_frontend('hsplit')) vsplit_action = QtWidgets.QAction(" : &Vertical Split", self, triggered=lambda: self.set_paging_active_frontend('vsplit')) inside_action = QtWidgets.QAction(" &Inside Pager", self, triggered=lambda: self.set_paging_active_frontend('inside')) self.pager_menu.addAction(hsplit_action) self.pager_menu.addAction(vsplit_action) self.pager_menu.addAction(inside_action) available_syntax_styles = self.get_available_syntax_styles() if len(available_syntax_styles) > 0: self.syntax_style_menu = self.view_menu.addMenu("&Syntax Style") style_group = QtWidgets.QActionGroup(self) for style in available_syntax_styles: action = QtWidgets.QAction("{}".format(style), self) action.triggered.connect(partial(self.set_syntax_style, style)) action.setCheckable(True) style_group.addAction(action) self.syntax_style_menu.addAction(action) if style == 'default': action.setChecked(True) self.syntax_style_menu.setDefaultAction(action) def init_kernel_menu(self): self.kernel_menu = self.menuBar().addMenu("&Kernel") # Qt on OSX maps Ctrl to Cmd, and Meta to Ctrl # keep the signal shortcuts to ctrl, rather than # platform-default like we do elsewhere. ctrl = "Meta" if sys.platform == 'darwin' else "Ctrl" self.interrupt_kernel_action = QtWidgets.QAction("&Interrupt current Kernel", self, triggered=self.interrupt_kernel_active_frontend, shortcut=ctrl+"+C", ) self.add_menu_action(self.kernel_menu, self.interrupt_kernel_action) self.restart_kernel_action = QtWidgets.QAction("&Restart current Kernel", self, triggered=self.restart_kernel_active_frontend, shortcut=ctrl+"+.", ) self.add_menu_action(self.kernel_menu, self.restart_kernel_action) self.kernel_menu.addSeparator() self.confirm_restart_kernel_action = QtWidgets.QAction("&Confirm kernel restart", self, checkable=True, checked=self.active_frontend.confirm_restart, triggered=self.toggle_confirm_restart_active_frontend ) self.add_menu_action(self.kernel_menu, self.confirm_restart_kernel_action) self.tab_widget.currentChanged.connect(self.update_restart_checkbox) def init_window_menu(self): self.window_menu = self.menuBar().addMenu("&Window") if sys.platform == 'darwin': # add min/maximize actions to OSX, which lacks default bindings. self.minimizeAct = QtWidgets.QAction("Mini&mize", self, shortcut="Ctrl+m", statusTip="Minimize the window/Restore Normal Size", triggered=self.toggleMinimized) # maximize is called 'Zoom' on OSX for some reason self.maximizeAct = QtWidgets.QAction("&Zoom", self, shortcut="Ctrl+Shift+M", statusTip="Maximize the window/Restore Normal Size", triggered=self.toggleMaximized) self.add_menu_action(self.window_menu, self.minimizeAct) self.add_menu_action(self.window_menu, self.maximizeAct) self.window_menu.addSeparator() prev_key = "Ctrl+Alt+Left" if sys.platform == 'darwin' else "Ctrl+PgUp" self.prev_tab_act = QtWidgets.QAction("Pre&vious Tab", self, shortcut=prev_key, statusTip="Select previous tab", triggered=self.prev_tab) self.add_menu_action(self.window_menu, self.prev_tab_act) next_key = "Ctrl+Alt+Right" if sys.platform == 'darwin' else "Ctrl+PgDown" self.next_tab_act = QtWidgets.QAction("Ne&xt Tab", self, shortcut=next_key, statusTip="Select next tab", triggered=self.next_tab) self.add_menu_action(self.window_menu, self.next_tab_act) self.rename_window_act = QtWidgets.QAction("Rename &Window", self, shortcut="Alt+R", statusTip="Rename window", triggered=self.set_window_title) self.add_menu_action(self.window_menu, self.rename_window_act) self.rename_current_tab_act = QtWidgets.QAction("&Rename Current Tab", self, shortcut="Ctrl+R", statusTip="Rename current tab", triggered=self.set_tab_title) self.add_menu_action(self.window_menu, self.rename_current_tab_act) def init_help_menu(self): # please keep the Help menu in Mac Os even if empty. It will # automatically contain a search field to search inside menus and # please keep it spelled in English, as long as Qt Doesn't support # a QAction.MenuRole like HelpMenuRole otherwise it will lose # this search field functionality self.help_menu = self.menuBar().addMenu("&Help") # Help Menu self.help_action = QtWidgets.QAction("Show &QtConsole help", self, triggered=self._show_help) self.online_help_action = QtWidgets.QAction("Open online &help", self, triggered=self._open_online_help) self.add_menu_action(self.help_menu, self.help_action) self.add_menu_action(self.help_menu, self.online_help_action) def _set_active_frontend_focus(self): # this is a hack, self.active_frontend._control seems to be # a private member. Unfortunately this is the only method # to set focus reliably QtCore.QTimer.singleShot(200, self.active_frontend._control.setFocus) # minimize/maximize/fullscreen actions: def toggle_menu_bar(self): menu_bar = self.menuBar() if menu_bar.isVisible(): menu_bar.setVisible(False) else: menu_bar.setVisible(True) def toggleMinimized(self): if not self.isMinimized(): self.showMinimized() else: self.showNormal() def _show_help(self): self.active_frontend._page(gui_reference) def _open_online_help(self): webbrowser.open("https://qtconsole.readthedocs.io", new=1, autoraise=True) def toggleMaximized(self): if not self.isMaximized(): self.showMaximized() else: self.showNormal() # Min/Max imizing while in full screen give a bug # when going out of full screen, at least on OSX def toggleFullScreen(self): if not self.isFullScreen(): self.showFullScreen() if sys.platform == 'darwin': self.maximizeAct.setEnabled(False) self.minimizeAct.setEnabled(False) else: self.showNormal() if sys.platform == 'darwin': self.maximizeAct.setEnabled(True) self.minimizeAct.setEnabled(True) def set_paging_active_frontend(self, paging): self.active_frontend._set_paging(paging) def set_completion_widget_active_frontend(self, gui_completion): self.active_frontend._set_completion_widget(gui_completion) def get_available_syntax_styles(self): """Get a list with the syntax styles available.""" styles = list(get_all_styles()) return sorted(styles) def set_syntax_style(self, syntax_style): """Set up syntax style for the current console.""" if syntax_style=='bw': colors='nocolor' elif styles.dark_style(syntax_style): colors='linux' else: colors='lightbg' self.active_frontend.syntax_style = syntax_style style_sheet = styles.sheet_from_template(syntax_style, colors) self.active_frontend.style_sheet = style_sheet self.active_frontend._syntax_style_changed() self.active_frontend._style_sheet_changed() self.active_frontend.reset(clear=True) self.active_frontend._execute("%colors linux", True) def close_active_frontend(self): self.close_tab(self.active_frontend) def restart_kernel_active_frontend(self): self.active_frontend.request_restart_kernel() def interrupt_kernel_active_frontend(self): self.active_frontend.request_interrupt_kernel() def toggle_confirm_restart_active_frontend(self): widget = self.active_frontend widget.confirm_restart = not widget.confirm_restart self.confirm_restart_kernel_action.setChecked(widget.confirm_restart) def update_restart_checkbox(self): if self.active_frontend is None: return widget = self.active_frontend self.confirm_restart_kernel_action.setChecked(widget.confirm_restart) def clear_active_frontend(self): self.active_frontend.clear() def cut_active_frontend(self): widget = self.active_frontend if widget.can_cut(): widget.cut() def copy_active_frontend(self): widget = self.active_frontend widget.copy() def copy_raw_active_frontend(self): self.active_frontend._copy_raw_action.trigger() def paste_active_frontend(self): widget = self.active_frontend if widget.can_paste(): widget.paste() def undo_active_frontend(self): self.active_frontend.undo() def redo_active_frontend(self): self.active_frontend.redo() def print_action_active_frontend(self): self.active_frontend.print_action.trigger() def export_action_active_frontend(self): self.active_frontend.export_action.trigger() def select_all_active_frontend(self): self.active_frontend.select_all_action.trigger() def increase_font_size_active_frontend(self): self.active_frontend.increase_font_size.trigger() def decrease_font_size_active_frontend(self): self.active_frontend.decrease_font_size.trigger() def reset_font_size_active_frontend(self): self.active_frontend.reset_font_size.trigger() #--------------------------------------------------------------------------- # QWidget interface #--------------------------------------------------------------------------- def closeEvent(self, event): """ Forward the close event to every tabs contained by the windows """ if self.tab_widget.count() == 0: # no tabs, just close event.accept() return # Do Not loop on the widget count as it change while closing title = self.window().windowTitle() cancel = QtWidgets.QMessageBox.Cancel okay = QtWidgets.QMessageBox.Ok accept_role = QtWidgets.QMessageBox.AcceptRole if self.confirm_exit: if self.tab_widget.count() > 1: msg = "Close all tabs, stop all kernels, and Quit?" else: msg = "Close console, stop kernel, and Quit?" info = "Kernels not started here (e.g. notebooks) will be left alone." closeall = QtWidgets.QPushButton("&Quit", self) closeall.setShortcut('Q') box = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Question, title, msg) box.setInformativeText(info) box.addButton(cancel) box.addButton(closeall, QtWidgets.QMessageBox.YesRole) box.setDefaultButton(closeall) box.setEscapeButton(cancel) pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64))) box.setIconPixmap(pixmap) reply = box.exec_() else: reply = okay if reply == cancel: event.ignore() return if reply == okay or reply == accept_role: while self.tab_widget.count() >= 1: # prevent further confirmations: widget = self.active_frontend widget._confirm_exit = False self.close_tab(widget) event.accept() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1680040323.0 qtconsole-5.5.2/qtconsole/manager.py0000664000175000017500000000505200000000000020322 0ustar00carloscarlos00000000000000""" Defines a KernelClient that provides signals and slots. """ from qtpy import QtCore # Local imports from traitlets import Bool, DottedObjectName from jupyter_client import KernelManager from jupyter_client.restarter import KernelRestarter from .kernel_mixins import QtKernelManagerMixin, QtKernelRestarterMixin class QtKernelRestarter(KernelRestarter, QtKernelRestarterMixin): def start(self): if self._timer is None: self._timer = QtCore.QTimer() self._timer.timeout.connect(self.poll) self._timer.start(round(self.time_to_dead * 1000)) def stop(self): self._timer.stop() def poll(self): super().poll() def reset_count(self): self._restart_count = 0 class QtKernelManager(KernelManager, QtKernelManagerMixin): """A KernelManager with Qt signals for restart""" client_class = DottedObjectName('qtconsole.client.QtKernelClient') autorestart = Bool(True, config=True) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._is_restarting = False def start_restarter(self): """Start restarter mechanism.""" if self.autorestart and self.has_kernel: if self._restarter is None: self._restarter = QtKernelRestarter( kernel_manager=self, parent=self, log=self.log, ) self._restarter.add_callback(self._handle_kernel_restarting) self._restarter.start() def stop_restarter(self): """Stop restarter mechanism.""" if self.autorestart: if self._restarter is not None: self._restarter.stop() def post_start_kernel(self, **kw): """Kernel restarted.""" super().post_start_kernel(**kw) if self._is_restarting: self.kernel_restarted.emit() self._is_restarting = False def reset_autorestart_count(self): """Reset autorestart count.""" if self._restarter: self._restarter.reset_count() async def _async_post_start_kernel(self, **kw): """ This is necessary for Jupyter-client 8+ because `start_kernel` doesn't call `post_start_kernel` directly. """ await super()._async_post_start_kernel(**kw) if self._is_restarting: self.kernel_restarted.emit() self._is_restarting = False def _handle_kernel_restarting(self): """Kernel has died, and will be restarted.""" self._is_restarting = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1619977496.0 qtconsole-5.5.2/qtconsole/pygments_highlighter.py0000664000175000017500000002163700000000000023143 0ustar00carloscarlos00000000000000# Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. from qtpy import QtGui from qtconsole.qstringhelpers import qstring_length from pygments.formatters.html import HtmlFormatter from pygments.lexer import RegexLexer, _TokenType, Text, Error from pygments.lexers import Python3Lexer from pygments.styles import get_style_by_name def get_tokens_unprocessed(self, text, stack=('root',)): """ Split ``text`` into (tokentype, text) pairs. Monkeypatched to store the final stack on the object itself. The `text` parameter this gets passed is only the current line, so to highlight things like multiline strings correctly, we need to retrieve the state from the previous line (this is done in PygmentsHighlighter, below), and use it to continue processing the current line. """ pos = 0 tokendefs = self._tokens if hasattr(self, '_saved_state_stack'): statestack = list(self._saved_state_stack) else: statestack = list(stack) statetokens = tokendefs[statestack[-1]] while 1: for rexmatch, action, new_state in statetokens: m = rexmatch(text, pos) if m: if action is not None: if type(action) is _TokenType: yield pos, action, m.group() else: for item in action(self, m): yield item pos = m.end() if new_state is not None: # state transition if isinstance(new_state, tuple): for state in new_state: if state == '#pop': statestack.pop() elif state == '#push': statestack.append(statestack[-1]) else: statestack.append(state) elif isinstance(new_state, int): # pop del statestack[new_state:] elif new_state == '#push': statestack.append(statestack[-1]) else: assert False, "wrong state def: %r" % new_state statetokens = tokendefs[statestack[-1]] break else: try: if text[pos] == '\n': # at EOL, reset state to "root" pos += 1 statestack = ['root'] statetokens = tokendefs['root'] yield pos, Text, '\n' continue yield pos, Error, text[pos] pos += 1 except IndexError: break self._saved_state_stack = list(statestack) # Monkeypatch! from contextlib import contextmanager @contextmanager def _lexpatch(): try: orig = RegexLexer.get_tokens_unprocessed RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed yield finally: pass RegexLexer.get_tokens_unprocessed = orig class PygmentsBlockUserData(QtGui.QTextBlockUserData): """ Storage for the user data associated with each line. """ syntax_stack = ('root',) def __init__(self, **kwds): for key, value in kwds.items(): setattr(self, key, value) QtGui.QTextBlockUserData.__init__(self) def __repr__(self): attrs = ['syntax_stack'] kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr)) for attr in attrs ]) return 'PygmentsBlockUserData(%s)' % kwds class PygmentsHighlighter(QtGui.QSyntaxHighlighter): """ Syntax highlighter that uses Pygments for parsing. """ #--------------------------------------------------------------------------- # 'QSyntaxHighlighter' interface #--------------------------------------------------------------------------- def __init__(self, parent, lexer=None): super().__init__(parent) self._document = self.document() self._formatter = HtmlFormatter(nowrap=True) self.set_style('default') if lexer is not None: self._lexer = lexer else: self._lexer = Python3Lexer() def highlightBlock(self, string): """ Highlight a block of text. """ prev_data = self.currentBlock().previous().userData() with _lexpatch(): if prev_data is not None: self._lexer._saved_state_stack = prev_data.syntax_stack elif hasattr(self._lexer, "_saved_state_stack"): del self._lexer._saved_state_stack # Lex the text using Pygments index = 0 for token, text in self._lexer.get_tokens(string): length = qstring_length(text) self.setFormat(index, length, self._get_format(token)) index += length if hasattr(self._lexer, "_saved_state_stack"): data = PygmentsBlockUserData( syntax_stack=self._lexer._saved_state_stack ) self.currentBlock().setUserData(data) # Clean up for the next go-round. del self._lexer._saved_state_stack #--------------------------------------------------------------------------- # 'PygmentsHighlighter' interface #--------------------------------------------------------------------------- def set_style(self, style): """ Sets the style to the specified Pygments style. """ if isinstance(style, str): style = get_style_by_name(style) self._style = style self._clear_caches() def set_style_sheet(self, stylesheet): """ Sets a CSS stylesheet. The classes in the stylesheet should correspond to those generated by: pygmentize -S image/svg+xml ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603554021.0 qtconsole-5.5.2/qtconsole/rich_ipython_widget.py0000664000175000017500000000027000000000000022747 0ustar00carloscarlos00000000000000import warnings warnings.warn("qtconsole.rich_ipython_widget is deprecated; " "use qtconsole.rich_jupyter_widget", DeprecationWarning) from .rich_jupyter_widget import * ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1697726811.0 qtconsole-5.5.2/qtconsole/rich_jupyter_widget.py0000664000175000017500000004405600000000000022771 0ustar00carloscarlos00000000000000# Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. from base64 import b64decode import os import re from warnings import warn from qtpy import QtCore, QtGui, QtWidgets from traitlets import Bool from pygments.util import ClassNotFound from qtconsole.svg import save_svg, svg_to_clipboard, svg_to_image from .jupyter_widget import JupyterWidget from .styles import get_colors try: from IPython.lib.latextools import latex_to_png except ImportError: latex_to_png = None def _ensure_dir_exists(path, mode=0o755): """ensure that a directory exists If it doesn't exists, try to create it and protect against a race condition if another process is doing the same. The default permissions are 755, which differ from os.makedirs default of 777. """ if not os.path.exists(path): try: os.makedirs(path, mode=mode) except OSError as e: if e.errno != errno.EEXIST: raise elif not os.path.isdir(path): raise IOError("%r exists but is not a directory" % path) class LatexError(Exception): """Exception for Latex errors""" class RichIPythonWidget(JupyterWidget): """Dummy class for config inheritance. Destroyed below.""" class RichJupyterWidget(RichIPythonWidget): """ An JupyterWidget that supports rich text, including lists, images, and tables. Note that raw performance will be reduced compared to the plain text version. """ # RichJupyterWidget protected class variables. _payload_source_plot = 'ipykernel.pylab.backend_payload.add_plot_payload' _jpg_supported = Bool(False) # Used to determine whether a given html export attempt has already # displayed a warning about being unable to convert a png to svg. _svg_warning_displayed = False #--------------------------------------------------------------------------- # 'object' interface #--------------------------------------------------------------------------- def __init__(self, *args, **kw): """ Create a RichJupyterWidget. """ kw['kind'] = 'rich' super().__init__(*args, **kw) # Configure the ConsoleWidget HTML exporter for our formats. self._html_exporter.image_tag = self._get_image_tag # Dictionary for resolving document resource names to SVG data. self._name_to_svg_map = {} # Do we support jpg ? # it seems that sometime jpg support is a plugin of QT, so try to assume # it is not always supported. self._jpg_supported = 'jpeg' in QtGui.QImageReader.supportedImageFormats() #--------------------------------------------------------------------------- # 'ConsoleWidget' public interface overides #--------------------------------------------------------------------------- def export_html(self): """ Shows a dialog to export HTML/XML in various formats. Overridden in order to reset the _svg_warning_displayed flag prior to the export running. """ self._svg_warning_displayed = False super().export_html() #--------------------------------------------------------------------------- # 'ConsoleWidget' protected interface #--------------------------------------------------------------------------- def _context_menu_make(self, pos): """ Reimplemented to return a custom context menu for images. """ format = self._control.cursorForPosition(pos).charFormat() name = format.stringProperty(QtGui.QTextFormat.ImageName) if name: menu = QtWidgets.QMenu(self) menu.addAction('Copy Image', lambda: self._copy_image(name)) menu.addAction('Save Image As...', lambda: self._save_image(name)) menu.addSeparator() svg = self._name_to_svg_map.get(name, None) if svg is not None: menu.addSeparator() menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg)) menu.addAction('Save SVG As...', lambda: save_svg(svg, self._control)) else: menu = super()._context_menu_make(pos) return menu #--------------------------------------------------------------------------- # 'BaseFrontendMixin' abstract interface #--------------------------------------------------------------------------- def _pre_image_append(self, msg, prompt_number): """Append the Out[] prompt and make the output nicer Shared code for some the following if statement """ self._append_plain_text(self.output_sep, True) self._append_html(self._make_out_prompt(prompt_number), True) self._append_plain_text('\n', True) def _handle_execute_result(self, msg): """Overridden to handle rich data types, like SVG.""" self.log.debug("execute_result: %s", msg.get('content', '')) if self.include_output(msg): self.flush_clearoutput() content = msg['content'] prompt_number = content.get('execution_count', 0) data = content['data'] metadata = msg['content']['metadata'] if 'image/svg+xml' in data: self._pre_image_append(msg, prompt_number) self._append_svg(data['image/svg+xml'], True) self._append_html(self.output_sep2, True) elif 'image/png' in data: self._pre_image_append(msg, prompt_number) png = b64decode(data['image/png'].encode('ascii')) self._append_png(png, True, metadata=metadata.get('image/png', None)) self._append_html(self.output_sep2, True) elif 'image/jpeg' in data and self._jpg_supported: self._pre_image_append(msg, prompt_number) jpg = b64decode(data['image/jpeg'].encode('ascii')) self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None)) self._append_html(self.output_sep2, True) elif 'text/latex' in data: self._pre_image_append(msg, prompt_number) try: self._append_latex(data['text/latex'], True) except LatexError: return super()._handle_display_data(msg) self._append_html(self.output_sep2, True) else: # Default back to the plain text representation. return super()._handle_execute_result(msg) def _handle_display_data(self, msg): """Overridden to handle rich data types, like SVG.""" self.log.debug("display_data: %s", msg.get('content', '')) if self.include_output(msg): self.flush_clearoutput() data = msg['content']['data'] metadata = msg['content']['metadata'] # Try to use the svg or html representations. # FIXME: Is this the right ordering of things to try? self.log.debug("display: %s", msg.get('content', '')) if 'image/svg+xml' in data: svg = data['image/svg+xml'] self._append_svg(svg, True) elif 'image/png' in data: # PNG data is base64 encoded as it passes over the network # in a JSON structure so we decode it. png = b64decode(data['image/png'].encode('ascii')) self._append_png(png, True, metadata=metadata.get('image/png', None)) elif 'image/jpeg' in data and self._jpg_supported: jpg = b64decode(data['image/jpeg'].encode('ascii')) self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None)) elif 'text/latex' in data and latex_to_png: try: self._append_latex(data['text/latex'], True) except LatexError: return super()._handle_display_data(msg) else: # Default back to the plain text representation. return super()._handle_display_data(msg) #--------------------------------------------------------------------------- # 'RichJupyterWidget' protected interface #--------------------------------------------------------------------------- def _is_latex_math(self, latex): """ Determine if a Latex string is in math mode This is the only mode supported by qtconsole """ basic_envs = ['math', 'displaymath'] starable_envs = ['equation', 'eqnarray' 'multline', 'gather', 'align', 'flalign', 'alignat'] star_envs = [env + '*' for env in starable_envs] envs = basic_envs + starable_envs + star_envs env_syntax = [r'\begin{{{0}}} \end{{{0}}}'.format(env).split() for env in envs] math_syntax = [ (r'\[', r'\]'), (r'\(', r'\)'), ('$$', '$$'), ('$', '$'), ] for start, end in math_syntax + env_syntax: inner = latex[len(start):-len(end)] if start in inner or end in inner: return False if latex.startswith(start) and latex.endswith(end): return True return False def _get_color(self, color): """Get color from the current syntax style if loadable.""" try: return get_colors(self.syntax_style)[color] except ClassNotFound: # The syntax_style has been sideloaded (e.g. by spyder). # In this case the overloading class should override this method. return get_colors('default')[color] def _append_latex(self, latex, before_prompt=False, metadata=None): """ Append latex data to the widget.""" png = None if self._is_latex_math(latex): png = latex_to_png(latex, wrap=False, backend='dvipng', color=self._get_color('fgcolor')) # Matplotlib only supports strings enclosed in dollar signs if png is None and latex.startswith('$') and latex.endswith('$'): # To avoid long and ugly errors, like the one reported in # spyder-ide/spyder#7619 try: png = latex_to_png(latex, wrap=False, backend='matplotlib', color=self._get_color('fgcolor')) except Exception: pass if png: self._append_png(png, before_prompt, metadata) else: raise LatexError def _append_jpg(self, jpg, before_prompt=False, metadata=None): """ Append raw JPG data to the widget.""" self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata) def _append_png(self, png, before_prompt=False, metadata=None): """ Append raw PNG data to the widget. """ self._append_custom(self._insert_png, png, before_prompt, metadata=metadata) def _append_svg(self, svg, before_prompt=False): """ Append raw SVG data to the widget. """ self._append_custom(self._insert_svg, svg, before_prompt) def _add_image(self, image): """ Adds the specified QImage to the document and returns a QTextImageFormat that references it. """ document = self._control.document() name = str(image.cacheKey()) document.addResource(QtGui.QTextDocument.ImageResource, QtCore.QUrl(name), image) format = QtGui.QTextImageFormat() format.setName(name) return format def _copy_image(self, name): """ Copies the ImageResource with 'name' to the clipboard. """ image = self._get_image(name) QtWidgets.QApplication.clipboard().setImage(image) def _get_image(self, name): """ Returns the QImage stored as the ImageResource with 'name'. """ document = self._control.document() image = document.resource(QtGui.QTextDocument.ImageResource, QtCore.QUrl(name)) return image def _get_image_tag(self, match, path = None, format = "png"): """ Return (X)HTML mark-up for the image-tag given by match. Parameters ---------- match : re.SRE_Match A match to an HTML image tag as exported by Qt, with match.group("Name") containing the matched image ID. path : string|None, optional [default None] If not None, specifies a path to which supporting files may be written (e.g., for linked images). If None, all images are to be included inline. format : "png"|"svg"|"jpg", optional [default "png"] Format for returned or referenced images. """ if format in ("png","jpg"): try: image = self._get_image(match.group("name")) except KeyError: return "Couldn't find image %s" % match.group("name") if path is not None: _ensure_dir_exists(path) relpath = os.path.basename(path) if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format), "PNG"): return '' % (relpath, match.group("name"),format) else: return "Couldn't save image!" else: ba = QtCore.QByteArray() buffer_ = QtCore.QBuffer(ba) buffer_.open(QtCore.QIODevice.WriteOnly) image.save(buffer_, format.upper()) buffer_.close() return '' % ( format,re.sub(r'(.{60})',r'\1\n', str(ba.toBase64().data().decode()))) elif format == "svg": try: svg = str(self._name_to_svg_map[match.group("name")]) except KeyError: if not self._svg_warning_displayed: QtWidgets.QMessageBox.warning(self, 'Error converting PNG to SVG.', 'Cannot convert PNG images to SVG, export with PNG figures instead. ' 'If you want to export matplotlib figures as SVG, add ' 'to your ipython config:\n\n' '\tc.InlineBackend.figure_format = \'svg\'\n\n' 'And regenerate the figures.', QtWidgets.QMessageBox.Ok) self._svg_warning_displayed = True return ("Cannot convert PNG images to SVG. " "You must export this session with PNG images. " "If you want to export matplotlib figures as SVG, add to your config " "c.InlineBackend.figure_format = 'svg' " "and regenerate the figures.") # Not currently checking path, because it's tricky to find a # cross-browser way to embed external SVG images (e.g., via # object or embed tags). # Chop stand-alone header from matplotlib SVG offset = svg.find(" -1) return svg[offset:] else: return 'Unrecognized image format' def _insert_jpg(self, cursor, jpg, metadata=None): """ Insert raw PNG data into the widget.""" self._insert_img(cursor, jpg, 'jpg', metadata=metadata) def _insert_png(self, cursor, png, metadata=None): """ Insert raw PNG data into the widget. """ self._insert_img(cursor, png, 'png', metadata=metadata) def _insert_img(self, cursor, img, fmt, metadata=None): """ insert a raw image, jpg or png """ if metadata: width = metadata.get('width', None) height = metadata.get('height', None) else: width = height = None try: image = QtGui.QImage() image.loadFromData(img, fmt.upper()) if width and height: image = image.scaled(int(width), int(height), QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation) elif width and not height: image = image.scaledToWidth(int(width), QtCore.Qt.SmoothTransformation) elif height and not width: image = image.scaledToHeight(int(height), QtCore.Qt.SmoothTransformation) except ValueError: self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt) else: format = self._add_image(image) cursor.insertBlock() cursor.insertImage(format) cursor.insertBlock() def _insert_svg(self, cursor, svg): """ Insert raw SVG data into the widet. """ try: image = svg_to_image(svg) except ValueError: self._insert_plain_text(cursor, 'Received invalid SVG data.') else: format = self._add_image(image) self._name_to_svg_map[format.name()] = svg cursor.insertBlock() cursor.insertImage(format) cursor.insertBlock() def _save_image(self, name, format='PNG'): """ Shows a save dialog for the ImageResource with 'name'. """ dialog = QtWidgets.QFileDialog(self._control, 'Save Image') dialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave) dialog.setDefaultSuffix(format.lower()) dialog.setNameFilter('%s file (*.%s)' % (format, format.lower())) if dialog.exec_(): filename = dialog.selectedFiles()[0] image = self._get_image(name) image.save(filename, format) # Clobber RichIPythonWidget above: class RichIPythonWidget(RichJupyterWidget): """Deprecated class. Use RichJupyterWidget.""" def __init__(self, *a, **kw): warn("RichIPythonWidget is deprecated, use RichJupyterWidget", DeprecationWarning) super().__init__(*a, **kw) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1615943229.0 qtconsole-5.5.2/qtconsole/rich_text.py0000664000175000017500000002067600000000000020712 0ustar00carloscarlos00000000000000""" Defines classes and functions for working with Qt's rich text system. """ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import io import os import re from qtpy import QtWidgets #----------------------------------------------------------------------------- # Constants #----------------------------------------------------------------------------- # A regular expression for an HTML paragraph with no content. EMPTY_P_RE = re.compile(r']*>\s*

') # A regular expression for matching images in rich text HTML. # Note that this is overly restrictive, but Qt's output is predictable... IMG_RE = re.compile(r'') #----------------------------------------------------------------------------- # Classes #----------------------------------------------------------------------------- class HtmlExporter(object): """ A stateful HTML exporter for a Q(Plain)TextEdit. This class is designed for convenient user interaction. """ def __init__(self, control): """ Creates an HtmlExporter for the given Q(Plain)TextEdit. """ assert isinstance(control, (QtWidgets.QPlainTextEdit, QtWidgets.QTextEdit)) self.control = control self.filename = 'ipython.html' self.image_tag = None self.inline_png = None def export(self): """ Displays a dialog for exporting HTML generated by Qt's rich text system. Returns ------- The name of the file that was saved, or None if no file was saved. """ parent = self.control.window() dialog = QtWidgets.QFileDialog(parent, 'Save as...') dialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave) filters = [ 'HTML with PNG figures (*.html *.htm)', 'XHTML with inline SVG figures (*.xhtml *.xml)' ] dialog.setNameFilters(filters) if self.filename: dialog.selectFile(self.filename) root,ext = os.path.splitext(self.filename) if ext.lower() in ('.xml', '.xhtml'): dialog.selectNameFilter(filters[-1]) if dialog.exec_(): self.filename = dialog.selectedFiles()[0] choice = dialog.selectedNameFilter() html = self.control.document().toHtml() # Configure the exporter. if choice.startswith('XHTML'): exporter = export_xhtml else: # If there are PNGs, decide how to export them. inline = self.inline_png if inline is None and IMG_RE.search(html): dialog = QtWidgets.QDialog(parent) dialog.setWindowTitle('Save as...') layout = QtWidgets.QVBoxLayout(dialog) msg = "Exporting HTML with PNGs" info = "Would you like inline PNGs (single large html " \ "file) or external image files?" checkbox = QtWidgets.QCheckBox("&Don't ask again") checkbox.setShortcut('D') ib = QtWidgets.QPushButton("&Inline") ib.setShortcut('I') eb = QtWidgets.QPushButton("&External") eb.setShortcut('E') box = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Question, dialog.windowTitle(), msg) box.setInformativeText(info) box.addButton(ib, QtWidgets.QMessageBox.NoRole) box.addButton(eb, QtWidgets.QMessageBox.YesRole) layout.setSpacing(0) layout.addWidget(box) layout.addWidget(checkbox) dialog.setLayout(layout) dialog.show() reply = box.exec_() dialog.hide() inline = (reply == 0) if checkbox.checkState(): # Don't ask anymore; always use this choice. self.inline_png = inline exporter = lambda h, f, i: export_html(h, f, i, inline) # Perform the export! try: return exporter(html, self.filename, self.image_tag) except Exception as e: msg = "Error exporting HTML to %s\n" % self.filename + str(e) reply = QtWidgets.QMessageBox.warning(parent, 'Error', msg, QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) return None #----------------------------------------------------------------------------- # Functions #----------------------------------------------------------------------------- def export_html(html, filename, image_tag = None, inline = True): """ Export the contents of the ConsoleWidget as HTML. Parameters ---------- html : unicode, A Python unicode string containing the Qt HTML to export. filename : str The file to be saved. image_tag : callable, optional (default None) Used to convert images. See ``default_image_tag()`` for information. inline : bool, optional [default True] If True, include images as inline PNGs. Otherwise, include them as links to external PNG files, mimicking web browsers' "Web Page, Complete" behavior. """ if image_tag is None: image_tag = default_image_tag if inline: path = None else: root,ext = os.path.splitext(filename) path = root + "_files" if os.path.isfile(path): raise OSError("%s exists, but is not a directory." % path) with io.open(filename, 'w', encoding='utf-8') as f: html = fix_html(html) f.write(IMG_RE.sub(lambda x: image_tag(x, path = path, format = "png"), html)) def export_xhtml(html, filename, image_tag=None): """ Export the contents of the ConsoleWidget as XHTML with inline SVGs. Parameters ---------- html : unicode, A Python unicode string containing the Qt HTML to export. filename : str The file to be saved. image_tag : callable, optional (default None) Used to convert images. See ``default_image_tag()`` for information. """ if image_tag is None: image_tag = default_image_tag with io.open(filename, 'w', encoding='utf-8') as f: # Hack to make xhtml header -- note that we are not doing any check for # valid XML. offset = html.find("") assert offset > -1, 'Invalid HTML string: no tag.' html = ('\n'+ html[offset+6:]) html = fix_html(html) f.write(IMG_RE.sub(lambda x: image_tag(x, path = None, format = "svg"), html)) def default_image_tag(match, path = None, format = "png"): """ Return (X)HTML mark-up for the image-tag given by match. This default implementation merely removes the image, and exists mostly for documentation purposes. More information than is present in the Qt HTML is required to supply the images. Parameters ---------- match : re.SRE_Match A match to an HTML image tag as exported by Qt, with match.group("Name") containing the matched image ID. path : string|None, optional [default None] If not None, specifies a path to which supporting files may be written (e.g., for linked images). If None, all images are to be included inline. format : "png"|"svg", optional [default "png"] Format for returned or referenced images. """ return '' def fix_html(html): """ Transforms a Qt-generated HTML string into a standards-compliant one. Parameters ---------- html : unicode, A Python unicode string containing the Qt HTML. """ # A UTF-8 declaration is needed for proper rendering of some characters # (e.g., indented commands) when viewing exported HTML on a local system # (i.e., without seeing an encoding declaration in an HTTP header). # C.f. http://www.w3.org/International/O-charset for details. offset = html.find('') if offset > -1: html = (html[:offset+6]+ '\n\n'+ html[offset+6:]) # Replace empty paragraphs tags with line breaks. html = re.sub(EMPTY_P_RE, '
', html) return html ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603554021.0 qtconsole-5.5.2/qtconsole/styles.py0000664000175000017500000000733100000000000020235 0ustar00carloscarlos00000000000000""" Style utilities, templates, and defaults for syntax highlighting widgets. """ #----------------------------------------------------------------------------- # Imports #----------------------------------------------------------------------------- from colorsys import rgb_to_hls from pygments.styles import get_style_by_name from pygments.token import Token #----------------------------------------------------------------------------- # Constants #----------------------------------------------------------------------------- default_template = '''\ QPlainTextEdit, QTextEdit { background-color: %(bgcolor)s; background-clip: padding; color: %(fgcolor)s; selection-background-color: %(select)s; } .inverted { background-color: %(fgcolor)s; color: %(bgcolor)s; } .error { color: red; } .in-prompt-number { font-weight: bold; } .out-prompt-number { font-weight: bold; } ''' # The default light style sheet: black text on a white background. default_light_style_template = default_template + '''\ .in-prompt { color: navy; } .out-prompt { color: darkred; } ''' default_light_style_sheet = default_light_style_template%dict( bgcolor='white', fgcolor='black', select="#ccc") default_light_syntax_style = 'default' # The default dark style sheet: white text on a black background. default_dark_style_template = default_template + '''\ .in-prompt, .in-prompt-number { color: lime; } .out-prompt, .out-prompt-number { color: red; } ''' default_dark_style_sheet = default_dark_style_template%dict( bgcolor='black', fgcolor='white', select="#555") default_dark_syntax_style = 'monokai' # The default monochrome default_bw_style_sheet = default_template%dict( bgcolor='white', fgcolor='black', select="#ccc") default_bw_syntax_style = 'bw' def hex_to_rgb(color): """Convert a hex color to rgb integer tuple.""" if color.startswith('#'): color = color[1:] if len(color) == 3: color = ''.join([c*2 for c in color]) if len(color) != 6: return False try: r = int(color[:2],16) g = int(color[2:4],16) b = int(color[4:],16) except ValueError: return False else: return r,g,b def dark_color(color): """Check whether a color is 'dark'. Currently, this is simply whether the luminance is <50%""" rgb = hex_to_rgb(color) if rgb: return rgb_to_hls(*rgb)[1] < 128 else: # default to False return False def dark_style(stylename): """Guess whether the background of the style with name 'stylename' counts as 'dark'.""" return dark_color(get_style_by_name(stylename).background_color) def get_colors(stylename): """Construct the keys to be used building the base stylesheet from a templatee.""" style = get_style_by_name(stylename) fgcolor = style.style_for_token(Token.Text)['color'] or '' if len(fgcolor) in (3,6): # could be 'abcdef' or 'ace' hex, which needs '#' prefix try: int(fgcolor, 16) except TypeError: pass else: fgcolor = "#"+fgcolor return dict( bgcolor = style.background_color, select = style.highlight_color, fgcolor = fgcolor ) def sheet_from_template(name, colors='lightbg'): """Use one of the base templates, and set bg/fg/select colors.""" colors = colors.lower() if colors=='lightbg': return default_light_style_template%get_colors(name) elif colors=='linux': return default_dark_style_template%get_colors(name) elif colors=='nocolor': return default_bw_style_sheet else: raise KeyError("No such color scheme: %s"%colors) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1615943229.0 qtconsole-5.5.2/qtconsole/svg.py0000664000175000017500000000453200000000000017511 0ustar00carloscarlos00000000000000""" Defines utility functions for working with SVG documents in Qt. """ # System library imports. from qtpy import QtCore, QtGui, QtSvg, QtWidgets def save_svg(string, parent=None): """ Prompts the user to save an SVG document to disk. Parameters ---------- string : basestring A Python string containing a SVG document. parent : QWidget, optional The parent to use for the file dialog. Returns ------- The name of the file to which the document was saved, or None if the save was cancelled. """ if isinstance(string, str): string = string.encode('utf-8') dialog = QtWidgets.QFileDialog(parent, 'Save SVG Document') dialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave) dialog.setDefaultSuffix('svg') dialog.setNameFilter('SVG document (*.svg)') if dialog.exec_(): filename = dialog.selectedFiles()[0] f = open(filename, 'wb') try: f.write(string) finally: f.close() return filename return None def svg_to_clipboard(string): """ Copy a SVG document to the clipboard. Parameters ---------- string : basestring A Python string containing a SVG document. """ if isinstance(string, str): string = string.encode('utf-8') mime_data = QtCore.QMimeData() mime_data.setData('image/svg+xml', string) QtWidgets.QApplication.clipboard().setMimeData(mime_data) def svg_to_image(string, size=None): """ Convert a SVG document to a QImage. Parameters ---------- string : basestring A Python string containing a SVG document. size : QSize, optional The size of the image that is produced. If not specified, the SVG document's default size is used. Raises ------ ValueError If an invalid SVG string is provided. Returns ------- A QImage of format QImage.Format_ARGB32. """ if isinstance(string, str): string = string.encode('utf-8') renderer = QtSvg.QSvgRenderer(QtCore.QByteArray(string)) if not renderer.isValid(): raise ValueError('Invalid SVG data.') if size is None: size = renderer.defaultSize() image = QtGui.QImage(size, QtGui.QImage.Format_ARGB32) image.fill(0) painter = QtGui.QPainter(image) renderer.render(painter) return image ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1714924509.6896565 qtconsole-5.5.2/qtconsole/tests/0000775000175000017500000000000000000000000017476 5ustar00carloscarlos00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603554022.0 qtconsole-5.5.2/qtconsole/tests/__init__.py0000664000175000017500000000020400000000000021603 0ustar00carloscarlos00000000000000 import os import sys no_display = (sys.platform not in ('darwin', 'win32') and os.environ.get('DISPLAY', '') == '') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1680040323.0 qtconsole-5.5.2/qtconsole/tests/test_00_console_widget.py0000664000175000017500000006327600000000000024431 0ustar00carloscarlos00000000000000import os import unittest import sys from flaky import flaky import pytest from qtpy import QtCore, QtGui, QtWidgets from qtpy.QtTest import QTest from qtconsole.console_widget import ConsoleWidget from qtconsole.qtconsoleapp import JupyterQtConsoleApp from . import no_display from IPython.core.inputtransformer2 import TransformerManager SHELL_TIMEOUT = 20000 @pytest.fixture def qtconsole(qtbot): """Qtconsole fixture.""" # Create a console console = JupyterQtConsoleApp() console.initialize(argv=[]) console.window.confirm_exit = False console.window.show() yield console console.window.close() @flaky(max_runs=3) @pytest.mark.parametrize( "debug", [True, False]) def test_scroll(qtconsole, qtbot, debug): """ Make sure the scrolling works. """ window = qtconsole.window shell = window.active_frontend control = shell._control scroll_bar = control.verticalScrollBar() # Wait until the console is fully up qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) assert scroll_bar.value() == 0 # Define a function with loads of output # Check the outputs are working as well code = ["import time", "def print_numbers():", " for i in range(1000):", " print(i)", " time.sleep(.01)"] for line in code: qtbot.keyClicks(control, line) qtbot.keyClick(control, QtCore.Qt.Key_Enter) with qtbot.waitSignal(shell.executed): qtbot.keyClick(control, QtCore.Qt.Key_Enter, modifier=QtCore.Qt.ShiftModifier) def run_line(line, block=True): qtbot.keyClicks(control, line) if block: with qtbot.waitSignal(shell.executed): qtbot.keyClick(control, QtCore.Qt.Key_Enter, modifier=QtCore.Qt.ShiftModifier) else: qtbot.keyClick(control, QtCore.Qt.Key_Enter, modifier=QtCore.Qt.ShiftModifier) if debug: # Enter debug run_line('%debug print()', block=False) qtbot.keyClick(control, QtCore.Qt.Key_Enter) # redefine run_line def run_line(line, block=True): qtbot.keyClicks(control, '!' + line) qtbot.keyClick(control, QtCore.Qt.Key_Enter, modifier=QtCore.Qt.ShiftModifier) if block: qtbot.waitUntil( lambda: control.toPlainText().strip( ).split()[-1] == "ipdb>") prev_position = scroll_bar.value() # Create a bunch of inputs for i in range(20): run_line('a = 1') assert scroll_bar.value() > prev_position # Put the scroll bar higher and check it doesn't move prev_position = scroll_bar.value() + scroll_bar.pageStep() // 2 scroll_bar.setValue(prev_position) for i in range(2): run_line('a') assert scroll_bar.value() == prev_position # add more input and check it moved for i in range(10): run_line('a') assert scroll_bar.value() > prev_position prev_position = scroll_bar.value() # Run the printing function run_line('print_numbers()', block=False) qtbot.wait(1000) # Check everything advances assert scroll_bar.value() > prev_position # move up prev_position = scroll_bar.value() - scroll_bar.pageStep() scroll_bar.setValue(prev_position) qtbot.wait(1000) # Check position stayed the same assert scroll_bar.value() == prev_position # reset position prev_position = scroll_bar.maximum() - (scroll_bar.pageStep() * 8) // 10 scroll_bar.setValue(prev_position) qtbot.wait(1000) assert scroll_bar.value() > prev_position @flaky(max_runs=3) def test_input(qtconsole, qtbot): """ Test input function """ window = qtconsole.window shell = window.active_frontend control = shell._control # Wait until the console is fully up qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) with qtbot.waitSignal(shell.executed): shell.execute("import time") input_function = 'input' shell.execute("print(" + input_function + "('name: ')); time.sleep(3)") qtbot.waitUntil(lambda: control.toPlainText().split()[-1] == 'name:') qtbot.keyClicks(control, 'test') qtbot.keyClick(control, QtCore.Qt.Key_Enter) qtbot.waitUntil(lambda: not shell._reading) qtbot.keyClick(control, 'z', modifier=QtCore.Qt.ControlModifier) for i in range(10): qtbot.keyClick(control, QtCore.Qt.Key_Backspace) qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) assert 'name: test\ntest' in control.toPlainText() @flaky(max_runs=3) def test_debug(qtconsole, qtbot): """ Make sure the cursor works while debugging It might not because the console is "_executing" """ window = qtconsole.window shell = window.active_frontend control = shell._control # Wait until the console is fully up qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) # Enter execution code = "%debug range(1)" qtbot.keyClicks(control, code) qtbot.keyClick(control, QtCore.Qt.Key_Enter, modifier=QtCore.Qt.ShiftModifier) qtbot.waitUntil( lambda: control.toPlainText().strip().split()[-1] == "ipdb>", timeout=SHELL_TIMEOUT) # We should be able to move the cursor while debugging qtbot.keyClicks(control, "abd") qtbot.wait(100) qtbot.keyClick(control, QtCore.Qt.Key_Left) qtbot.keyClick(control, 'c') qtbot.wait(100) assert control.toPlainText().strip().split()[-1] == "abcd" @flaky(max_runs=15) def test_input_and_print(qtconsole, qtbot): """ Test that we print correctly mixed input and print statements. This is a regression test for spyder-ide/spyder#17710. """ window = qtconsole.window shell = window.active_frontend control = shell._control def wait_for_input(): qtbot.waitUntil( lambda: control.toPlainText().splitlines()[-1] == 'Write input: ' ) # Wait until the console is fully up qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) # Run a for loop with mixed input and print statements code = """ user_input = None while user_input != '': user_input = input('Write input: ') print('Input was entered!') """ shell.execute(code) wait_for_input() # Interact with the 'for' loop for a certain number of repetitions repetitions = 3 for _ in range(repetitions): qtbot.keyClicks(control, '1') qtbot.keyClick(control, QtCore.Qt.Key_Enter) wait_for_input() # Get out of the for loop qtbot.keyClick(control, QtCore.Qt.Key_Enter) qtbot.waitUntil(lambda: not shell._reading) qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) # Assert that printed correctly the expected output in the console. output = ( " ...: \n" + "Write input: 1\nInput was entered!\n" * repetitions + "Write input: \nInput was entered!\n" ) assert output in control.toPlainText() @flaky(max_runs=5) @pytest.mark.skipif(os.name == 'nt', reason="no SIGTERM on Windows") def test_restart_after_kill(qtconsole, qtbot): """ Test that the kernel correctly restarts after a kill. """ window = qtconsole.window shell = window.active_frontend control = shell._control def wait_for_restart(): qtbot.waitUntil( lambda: 'Kernel died, restarting' in control.toPlainText() ) # Wait until the console is fully up qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT) # This checks that we are able to restart the kernel even after the number # of consecutive auto-restarts is reached (which by default is five). for _ in range(10): # Clear console with qtbot.waitSignal(shell.executed): shell.execute('%clear') qtbot.wait(500) # Run some code that kills the kernel code = "import os, signal; os.kill(os.getpid(), signal.SIGTERM)" shell.execute(code) # Check that the restart message is printed qtbot.waitUntil( lambda: 'Kernel died, restarting' in control.toPlainText() ) # Check that a new prompt is available after the restart qtbot.waitUntil( lambda: control.toPlainText().splitlines()[-1] == 'In [1]: ' ) qtbot.wait(500) @pytest.mark.skipif(no_display, reason="Doesn't work without a display") class TestConsoleWidget(unittest.TestCase): @classmethod def setUpClass(cls): """ Create the application for the test case. """ cls._app = QtWidgets.QApplication.instance() if cls._app is None: cls._app = QtWidgets.QApplication([]) cls._app.setQuitOnLastWindowClosed(False) @classmethod def tearDownClass(cls): """ Exit the application. """ QtWidgets.QApplication.quit() def assert_text_equal(self, cursor, text): cursor.select(QtGui.QTextCursor.Document) selection = cursor.selectedText() self.assertEqual(selection, text) def test_special_characters(self): """ Are special characters displayed correctly? """ w = ConsoleWidget() cursor = w._get_prompt_cursor() test_inputs = ['xyz\b\b=\n', 'foo\b\nbar\n', 'foo\b\nbar\r\n', 'abc\rxyz\b\b='] expected_outputs = ['x=z\u2029', 'foo\u2029bar\u2029', 'foo\u2029bar\u2029', 'x=z'] for i, text in enumerate(test_inputs): w._insert_plain_text(cursor, text) self.assert_text_equal(cursor, expected_outputs[i]) # clear all the text cursor.insertText('') def test_erase_in_line(self): """ Do control sequences for clearing the line work? """ w = ConsoleWidget() cursor = w._get_prompt_cursor() test_inputs = ['Hello\x1b[1KBye', 'Hello\x1b[0KBye', 'Hello\r\x1b[0KBye', 'Hello\r\x1b[1KBye', 'Hello\r\x1b[2KBye', 'Hello\x1b[2K\rBye'] expected_outputs = [' Bye', 'HelloBye', 'Bye', 'Byelo', 'Bye', 'Bye'] for i, text in enumerate(test_inputs): w._insert_plain_text(cursor, text) self.assert_text_equal(cursor, expected_outputs[i]) # clear all the text cursor.insertText('') def test_link_handling(self): noButton = QtCore.Qt.NoButton noButtons = QtCore.Qt.NoButton noModifiers = QtCore.Qt.NoModifier MouseMove = QtCore.QEvent.MouseMove QMouseEvent = QtGui.QMouseEvent w = ConsoleWidget() cursor = w._get_prompt_cursor() w._insert_html(cursor, 'written in') obj = w._control tip = QtWidgets.QToolTip self.assertEqual(tip.text(), '') # should be somewhere else elsewhereEvent = QMouseEvent(MouseMove, QtCore.QPointF(50, 50), noButton, noButtons, noModifiers) w.eventFilter(obj, elsewhereEvent) self.assertEqual(tip.isVisible(), False) self.assertEqual(tip.text(), '') # should be over text overTextEvent = QMouseEvent(MouseMove, QtCore.QPointF(1, 5), noButton, noButtons, noModifiers) w.eventFilter(obj, overTextEvent) self.assertEqual(tip.isVisible(), True) self.assertEqual(tip.text(), "http://python.org") # should still be over text stillOverTextEvent = QMouseEvent(MouseMove, QtCore.QPointF(1, 5), noButton, noButtons, noModifiers) w.eventFilter(obj, stillOverTextEvent) self.assertEqual(tip.isVisible(), True) self.assertEqual(tip.text(), "http://python.org") def test_width_height(self): # width()/height() QWidget properties should not be overridden. w = ConsoleWidget() self.assertEqual(w.width(), QtWidgets.QWidget.width(w)) self.assertEqual(w.height(), QtWidgets.QWidget.height(w)) def test_prompt_cursors(self): """Test the cursors that keep track of where the prompt begins and ends""" w = ConsoleWidget() w._prompt = 'prompt>' doc = w._control.document() # Fill up the QTextEdit area with the maximum number of blocks doc.setMaximumBlockCount(10) for _ in range(9): w._append_plain_text('line\n') # Draw the prompt, this should cause the first lines to be deleted w._show_prompt() self.assertEqual(doc.blockCount(), 10) # _prompt_pos should be at the end of the document self.assertEqual(w._prompt_pos, w._get_end_pos()) # _append_before_prompt_pos should be at the beginning of the prompt self.assertEqual(w._append_before_prompt_pos, w._prompt_pos - len(w._prompt)) # insert some more text without drawing a new prompt w._append_plain_text('line\n') self.assertEqual(w._prompt_pos, w._get_end_pos() - len('line\n')) self.assertEqual(w._append_before_prompt_pos, w._prompt_pos - len(w._prompt)) # redraw the prompt w._show_prompt() self.assertEqual(w._prompt_pos, w._get_end_pos()) self.assertEqual(w._append_before_prompt_pos, w._prompt_pos - len(w._prompt)) # insert some text before the prompt w._append_plain_text('line', before_prompt=True) self.assertEqual(w._prompt_pos, w._get_end_pos()) self.assertEqual(w._append_before_prompt_pos, w._prompt_pos - len(w._prompt)) def test_select_all(self): w = ConsoleWidget() w._append_plain_text('Header\n') w._prompt = 'prompt>' w._show_prompt() control = w._control app = QtWidgets.QApplication.instance() cursor = w._get_cursor() w._insert_plain_text_into_buffer(cursor, "if:\n pass") cursor.clearSelection() control.setTextCursor(cursor) # "select all" action selects cell first w.select_all_smart() QTest.keyClick(control, QtCore.Qt.Key_C, QtCore.Qt.ControlModifier) copied = app.clipboard().text() self.assertEqual(copied, 'if:\n> pass') # # "select all" action triggered a second time selects whole document w.select_all_smart() QTest.keyClick(control, QtCore.Qt.Key_C, QtCore.Qt.ControlModifier) copied = app.clipboard().text() self.assertEqual(copied, 'Header\nprompt>if:\n> pass') @pytest.mark.skipif(sys.platform == 'darwin', reason="Fails on macOS") def test_keypresses(self): """Test the event handling code for keypresses.""" w = ConsoleWidget() w._append_plain_text('Header\n') w._prompt = 'prompt>' w._show_prompt() app = QtWidgets.QApplication.instance() control = w._control # Test setting the input buffer w._set_input_buffer('test input') self.assertEqual(w._get_input_buffer(), 'test input') # Ctrl+K kills input until EOL w._set_input_buffer('test input') c = control.textCursor() c.setPosition(c.position() - 3) control.setTextCursor(c) QTest.keyClick(control, QtCore.Qt.Key_K, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), 'test in') # Ctrl+V pastes w._set_input_buffer('test input ') app.clipboard().setText('pasted text') QTest.keyClick(control, QtCore.Qt.Key_V, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), 'test input pasted text') self.assertEqual(control.document().blockCount(), 2) # Paste should strip indentation w._set_input_buffer('test input ') app.clipboard().setText(' pasted text') QTest.keyClick(control, QtCore.Qt.Key_V, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), 'test input pasted text') self.assertEqual(control.document().blockCount(), 2) # Multiline paste, should also show continuation marks w._set_input_buffer('test input ') app.clipboard().setText('line1\nline2\nline3') QTest.keyClick(control, QtCore.Qt.Key_V, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), 'test input line1\nline2\nline3') self.assertEqual(control.document().blockCount(), 4) self.assertEqual(control.document().findBlockByNumber(1).text(), 'prompt>test input line1') self.assertEqual(control.document().findBlockByNumber(2).text(), '> line2') self.assertEqual(control.document().findBlockByNumber(3).text(), '> line3') # Multiline paste should strip indentation intelligently # in the case where pasted text has leading whitespace on first line # and we're pasting into indented position w._set_input_buffer(' ') app.clipboard().setText(' If 1:\n pass') QTest.keyClick(control, QtCore.Qt.Key_V, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), ' If 1:\n pass') # Ctrl+Backspace should intelligently remove the last word w._set_input_buffer("foo = ['foo', 'foo', 'foo', \n" " 'bar', 'bar', 'bar']") QTest.keyClick(control, QtCore.Qt.Key_Backspace, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), ("foo = ['foo', 'foo', 'foo', \n" " 'bar', 'bar', '")) QTest.keyClick(control, QtCore.Qt.Key_Backspace, QtCore.Qt.ControlModifier) QTest.keyClick(control, QtCore.Qt.Key_Backspace, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), ("foo = ['foo', 'foo', 'foo', \n" " '")) QTest.keyClick(control, QtCore.Qt.Key_Backspace, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), ("foo = ['foo', 'foo', 'foo', \n" "")) QTest.keyClick(control, QtCore.Qt.Key_Backspace, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), "foo = ['foo', 'foo', 'foo',") # Ctrl+Delete should intelligently remove the next word w._set_input_buffer("foo = ['foo', 'foo', 'foo', \n" " 'bar', 'bar', 'bar']") c = control.textCursor() c.setPosition(35) control.setTextCursor(c) QTest.keyClick(control, QtCore.Qt.Key_Delete, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), ("foo = ['foo', 'foo', ', \n" " 'bar', 'bar', 'bar']")) QTest.keyClick(control, QtCore.Qt.Key_Delete, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), ("foo = ['foo', 'foo', \n" " 'bar', 'bar', 'bar']")) QTest.keyClick(control, QtCore.Qt.Key_Delete, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), "foo = ['foo', 'foo', 'bar', 'bar', 'bar']") w._set_input_buffer("foo = ['foo', 'foo', 'foo', \n" " 'bar', 'bar', 'bar']") c = control.textCursor() c.setPosition(48) control.setTextCursor(c) QTest.keyClick(control, QtCore.Qt.Key_Delete, QtCore.Qt.ControlModifier) self.assertEqual(w._get_input_buffer(), ("foo = ['foo', 'foo', 'foo', \n" "'bar', 'bar', 'bar']")) # Left and right keys should respect the continuation prompt w._set_input_buffer("line 1\n" "line 2\n" "line 3") c = control.textCursor() c.setPosition(20) # End of line 1 control.setTextCursor(c) QTest.keyClick(control, QtCore.Qt.Key_Right) # Cursor should have moved after the continuation prompt self.assertEqual(control.textCursor().position(), 23) QTest.keyClick(control, QtCore.Qt.Key_Left) # Cursor should have moved to the end of the previous line self.assertEqual(control.textCursor().position(), 20) # TODO: many more keybindings def test_indent(self): """Test the event handling code for indent/dedent keypresses .""" w = ConsoleWidget() w._append_plain_text('Header\n') w._prompt = 'prompt>' w._show_prompt() control = w._control # TAB with multiline selection should block-indent w._set_input_buffer("") c = control.textCursor() pos=c.position() w._set_input_buffer("If 1:\n pass") c.setPosition(pos, QtGui.QTextCursor.KeepAnchor) control.setTextCursor(c) QTest.keyClick(control, QtCore.Qt.Key_Tab) self.assertEqual(w._get_input_buffer()," If 1:\n pass") # TAB with multiline selection, should block-indent to next multiple # of 4 spaces, if first line has 0 < indent < 4 w._set_input_buffer("") c = control.textCursor() pos=c.position() w._set_input_buffer(" If 2:\n pass") c.setPosition(pos, QtGui.QTextCursor.KeepAnchor) control.setTextCursor(c) QTest.keyClick(control, QtCore.Qt.Key_Tab) self.assertEqual(w._get_input_buffer()," If 2:\n pass") # Shift-TAB with multiline selection should block-dedent w._set_input_buffer("") c = control.textCursor() pos=c.position() w._set_input_buffer(" If 3:\n pass") c.setPosition(pos, QtGui.QTextCursor.KeepAnchor) control.setTextCursor(c) QTest.keyClick(control, QtCore.Qt.Key_Backtab) self.assertEqual(w._get_input_buffer(),"If 3:\n pass") def test_complete(self): class TestKernelClient(object): def is_complete(self, source): calls.append(source) return msg_id w = ConsoleWidget() cursor = w._get_prompt_cursor() w._execute = lambda *args: calls.append(args) w.kernel_client = TestKernelClient() msg_id = object() calls = [] # test incomplete statement (no _execute called, but indent added) w.execute("thing", interactive=True) self.assertEqual(calls, ["thing"]) calls = [] w._handle_is_complete_reply( dict(parent_header=dict(msg_id=msg_id), content=dict(status="incomplete", indent="!!!"))) self.assert_text_equal(cursor, "thing\u2029> !!!") self.assertEqual(calls, []) # test complete statement (_execute called) msg_id = object() w.execute("else", interactive=True) self.assertEqual(calls, ["else"]) calls = [] w._handle_is_complete_reply( dict(parent_header=dict(msg_id=msg_id), content=dict(status="complete", indent="###"))) self.assertEqual(calls, [("else", False)]) calls = [] self.assert_text_equal(cursor, "thing\u2029> !!!else\u2029") # test missing answer from is_complete msg_id = object() w.execute("done", interactive=True) self.assertEqual(calls, ["done"]) calls = [] self.assert_text_equal(cursor, "thing\u2029> !!!else\u2029") w._trigger_is_complete_callback() self.assert_text_equal(cursor, "thing\u2029> !!!else\u2029\u2029> ") # assert that late answer isn't destroying anything w._handle_is_complete_reply( dict(parent_header=dict(msg_id=msg_id), content=dict(status="complete", indent="###"))) self.assertEqual(calls, []) def test_complete_python(self): """Test that is_complete is working correctly for Python.""" # Kernel client to test the responses of is_complete class TestIPyKernelClient(object): def is_complete(self, source): tm = TransformerManager() check_complete = tm.check_complete(source) responses.append(check_complete) # Initialize widget responses = [] w = ConsoleWidget() w._append_plain_text('Header\n') w._prompt = 'prompt>' w._show_prompt() w.kernel_client = TestIPyKernelClient() # Execute incomplete statement inside a block code = '\n'.join(["if True:", " a = 1"]) w._set_input_buffer(code) w.execute(interactive=True) assert responses == [('incomplete', 4)] # Execute complete statement inside a block responses = [] code = '\n'.join(["if True:", " a = 1\n\n"]) w._set_input_buffer(code) w.execute(interactive=True) assert responses == [('complete', None)] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1636915703.0 qtconsole-5.5.2/qtconsole/tests/test_ansi_code_processor.py0000664000175000017500000001640600000000000025141 0ustar00carloscarlos00000000000000# Standard library imports import unittest # Local imports from qtconsole.ansi_code_processor import AnsiCodeProcessor class TestAnsiCodeProcessor(unittest.TestCase): def setUp(self): self.processor = AnsiCodeProcessor() def test_clear(self): """ Do control sequences for clearing the console work? """ string = '\x1b[2J\x1b[K' i = -1 for i, substring in enumerate(self.processor.split_string(string)): if i == 0: self.assertEqual(len(self.processor.actions), 1) action = self.processor.actions[0] self.assertEqual(action.action, 'erase') self.assertEqual(action.area, 'screen') self.assertEqual(action.erase_to, 'all') elif i == 1: self.assertEqual(len(self.processor.actions), 1) action = self.processor.actions[0] self.assertEqual(action.action, 'erase') self.assertEqual(action.area, 'line') self.assertEqual(action.erase_to, 'end') else: self.fail('Too many substrings.') self.assertEqual(i, 1, 'Too few substrings.') #test_erase_in_line() is in test_00_console_widget.py, because it needs the console def test_colors(self): """ Do basic controls sequences for colors work? """ string = 'first\x1b[34mblue\x1b[0mlast' i = -1 for i, substring in enumerate(self.processor.split_string(string)): if i == 0: self.assertEqual(substring, 'first') self.assertEqual(self.processor.foreground_color, None) elif i == 1: self.assertEqual(substring, 'blue') self.assertEqual(self.processor.foreground_color, 4) elif i == 2: self.assertEqual(substring, 'last') self.assertEqual(self.processor.foreground_color, None) else: self.fail('Too many substrings.') self.assertEqual(i, 2, 'Too few substrings.') def test_colors_xterm(self): """ Do xterm-specific control sequences for colors work? """ string = '\x1b]4;20;rgb:ff/ff/ff\x1b' \ '\x1b]4;25;rgbi:1.0/1.0/1.0\x1b' substrings = list(self.processor.split_string(string)) desired = { 20 : (255, 255, 255), 25 : (255, 255, 255) } self.assertEqual(self.processor.color_map, desired) string = '\x1b[38;5;20m\x1b[48;5;25m' substrings = list(self.processor.split_string(string)) self.assertEqual(self.processor.foreground_color, 20) self.assertEqual(self.processor.background_color, 25) def test_true_color(self): """Do 24bit True Color control sequences? """ string = '\x1b[38;2;255;100;0m\x1b[48;2;100;100;100m' substrings = list(self.processor.split_string(string)) self.assertEqual(self.processor.foreground_color, [255, 100, 0]) self.assertEqual(self.processor.background_color, [100, 100, 100]) def test_scroll(self): """ Do control sequences for scrolling the buffer work? """ string = '\x1b[5S\x1b[T' i = -1 for i, substring in enumerate(self.processor.split_string(string)): if i == 0: self.assertEqual(len(self.processor.actions), 1) action = self.processor.actions[0] self.assertEqual(action.action, 'scroll') self.assertEqual(action.dir, 'up') self.assertEqual(action.unit, 'line') self.assertEqual(action.count, 5) elif i == 1: self.assertEqual(len(self.processor.actions), 1) action = self.processor.actions[0] self.assertEqual(action.action, 'scroll') self.assertEqual(action.dir, 'down') self.assertEqual(action.unit, 'line') self.assertEqual(action.count, 1) else: self.fail('Too many substrings.') self.assertEqual(i, 1, 'Too few substrings.') def test_formfeed(self): """ Are formfeed characters processed correctly? """ string = '\f' # form feed self.assertEqual(list(self.processor.split_string(string)), ['']) self.assertEqual(len(self.processor.actions), 1) action = self.processor.actions[0] self.assertEqual(action.action, 'scroll') self.assertEqual(action.dir, 'down') self.assertEqual(action.unit, 'page') self.assertEqual(action.count, 1) def test_carriage_return(self): """ Are carriage return characters processed correctly? """ string = 'foo\rbar' # carriage return splits = [] actions = [] for split in self.processor.split_string(string): splits.append(split) actions.append([action.action for action in self.processor.actions]) self.assertEqual(splits, ['foo', None, 'bar']) self.assertEqual(actions, [[], ['carriage-return'], []]) def test_carriage_return_newline(self): """transform CRLF to LF""" string = 'foo\rbar\r\ncat\r\n\n' # carriage return and newline # only one CR action should occur, and '\r\n' should transform to '\n' splits = [] actions = [] for split in self.processor.split_string(string): splits.append(split) actions.append([action.action for action in self.processor.actions]) self.assertEqual(splits, ['foo', None, 'bar', '\r\n', 'cat', '\r\n', '\n']) self.assertEqual(actions, [[], ['carriage-return'], [], ['newline'], [], ['newline'], ['newline']]) def test_beep(self): """ Are beep characters processed correctly? """ string = 'foo\abar' # bell splits = [] actions = [] for split in self.processor.split_string(string): splits.append(split) actions.append([action.action for action in self.processor.actions]) self.assertEqual(splits, ['foo', None, 'bar']) self.assertEqual(actions, [[], ['beep'], []]) def test_backspace(self): """ Are backspace characters processed correctly? """ string = 'foo\bbar' # backspace splits = [] actions = [] for split in self.processor.split_string(string): splits.append(split) actions.append([action.action for action in self.processor.actions]) self.assertEqual(splits, ['foo', None, 'bar']) self.assertEqual(actions, [[], ['backspace'], []]) def test_combined(self): """ Are CR and BS characters processed correctly in combination? BS is treated as a change in print position, rather than a backwards character deletion. Therefore a BS at EOL is effectively ignored. """ string = 'abc\rdef\b' # CR and backspace splits = [] actions = [] for split in self.processor.split_string(string): splits.append(split) actions.append([action.action for action in self.processor.actions]) self.assertEqual(splits, ['abc', None, 'def', None]) self.assertEqual(actions, [[], ['carriage-return'], [], ['backspace']]) if __name__ == '__main__': unittest.main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603554022.0 qtconsole-5.5.2/qtconsole/tests/test_app.py0000664000175000017500000000175300000000000021675 0ustar00carloscarlos00000000000000"""Test QtConsoleApp""" # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import os import sys from subprocess import check_output from jupyter_core import paths import pytest from traitlets.tests.utils import check_help_all_output from . import no_display @pytest.mark.skipif(no_display, reason="Doesn't work without a display") def test_help_output(): """jupyter qtconsole --help-all works""" check_help_all_output('qtconsole') @pytest.mark.skipif(no_display, reason="Doesn't work without a display") @pytest.mark.skipif(os.environ.get('CI', None) is None, reason="Doesn't work outside of our CIs") def test_generate_config(): """jupyter qtconsole --generate-config""" config_dir = paths.jupyter_config_dir() check_output([sys.executable, '-m', 'qtconsole', '--generate-config']) assert os.path.isfile(os.path.join(config_dir, 'jupyter_qtconsole_config.py')) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1714923927.0 qtconsole-5.5.2/qtconsole/tests/test_comms.py0000664000175000017500000001254000000000000022227 0ustar00carloscarlos00000000000000import time from queue import Empty import unittest from flaky import flaky from qtconsole.manager import QtKernelManager class Tests(unittest.TestCase): def setUp(self): """Open a kernel.""" self.kernel_manager = QtKernelManager() self.kernel_manager.start_kernel() self.kernel_client = self.kernel_manager.client() self.kernel_client.start_channels(shell=True, iopub=True) self.blocking_client = self.kernel_client.blocking_client() self.blocking_client.start_channels(shell=True, iopub=True) self.comm_manager = self.kernel_client.comm_manager # Check if client is working self.blocking_client.execute('print(0)') try: self._get_next_msg() self._get_next_msg() except TimeoutError: # Maybe it works now? self.blocking_client.execute('print(0)') self._get_next_msg() self._get_next_msg() def tearDown(self): """Close the kernel.""" if self.kernel_manager: self.kernel_manager.shutdown_kernel(now=True) if self.kernel_client: self.kernel_client.shutdown() def _get_next_msg(self, timeout=10): # Get status messages timeout_time = time.time() + timeout msg_type = 'status' while msg_type == 'status': if timeout_time < time.time(): raise TimeoutError try: msg = self.blocking_client.get_iopub_msg(timeout=3) msg_type = msg['header']['msg_type'] except Empty: pass return msg @flaky(max_runs=10) def test_kernel_to_frontend(self): """Communicate from the kernel to the frontend.""" comm_manager = self.comm_manager blocking_client = self.blocking_client class DummyCommHandler(): def __init__(self): comm_manager.register_target('test_api', self.comm_open) self.last_msg = None def comm_open(self, comm, msg): comm.on_msg(self.comm_message) comm.on_close(self.comm_message) self.last_msg = msg['content']['data'] self.comm = comm def comm_message(self, msg): self.last_msg = msg['content']['data'] handler = DummyCommHandler() blocking_client.execute( "from ipykernel.comm import Comm\n" "comm = Comm(target_name='test_api', data='open')\n" "comm.send('message')\n" "comm.close('close')\n" "del comm\n" "print('Done')\n" ) # Get input msg = self._get_next_msg() assert msg['header']['msg_type'] == 'execute_input' # Open comm msg = self._get_next_msg() assert msg['header']['msg_type'] == 'comm_open' comm_manager._dispatch(msg) assert handler.last_msg == 'open' assert handler.comm.comm_id == msg['content']['comm_id'] # Get message msg = self._get_next_msg() assert msg['header']['msg_type'] == 'comm_msg' comm_manager._dispatch(msg) assert handler.last_msg == 'message' assert handler.comm.comm_id == msg['content']['comm_id'] # Get close msg = self._get_next_msg() assert msg['header']['msg_type'] == 'comm_close' comm_manager._dispatch(msg) assert handler.last_msg == 'close' assert handler.comm.comm_id == msg['content']['comm_id'] # Get close msg = self._get_next_msg() assert msg['header']['msg_type'] == 'stream' @flaky(max_runs=10) def test_frontend_to_kernel(self): """Communicate from the frontend to the kernel.""" comm_manager = self.comm_manager blocking_client = self.blocking_client blocking_client.execute( "class DummyCommHandler():\n" " def __init__(self):\n" " get_ipython().kernel.comm_manager.register_target(\n" " 'test_api', self.comm_open)\n" " def comm_open(self, comm, msg):\n" " comm.on_msg(self.comm_message)\n" " comm.on_close(self.comm_message)\n" " print(msg['content']['data'])\n" " def comm_message(self, msg):\n" " print(msg['content']['data'])\n" "dummy = DummyCommHandler()\n" ) # Get input msg = self._get_next_msg() assert msg['header']['msg_type'] == 'execute_input' # Open comm comm = comm_manager.new_comm('test_api', data='open') msg = self._get_next_msg() assert msg['header']['msg_type'] == 'stream' assert msg['content']['text'] == 'open\n' # Get message comm.send('message') msg = self._get_next_msg() assert msg['header']['msg_type'] == 'stream' assert msg['content']['text'] == 'message\n' # Get close comm.close('close') msg = self._get_next_msg() # Received message has a header and parent header. The parent header has # the info about the close message type in Python 3 assert msg['parent_header']['msg_type'] == 'comm_close' assert msg['msg_type'] == 'stream' assert msg['content']['text'] == 'close\n' if __name__ == "__main__": unittest.main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1615943229.0 qtconsole-5.5.2/qtconsole/tests/test_completion_widget.py0000664000175000017500000000641700000000000024633 0ustar00carloscarlos00000000000000import os import tempfile import shutil import unittest import pytest from qtpy import QtCore, QtWidgets from qtpy.QtTest import QTest from qtconsole.console_widget import ConsoleWidget from qtconsole.completion_widget import CompletionWidget from . import no_display class TemporaryDirectory(object): """ Context manager for tempfile.mkdtemp(). This class is available in python +v3.2. See: https://gist.github.com/cpelley/10e2eeaf60dacc7956bb """ def __enter__(self): self.dir_name = tempfile.mkdtemp() return self.dir_name def __exit__(self, exc_type, exc_value, traceback): shutil.rmtree(self.dir_name) TemporaryDirectory = getattr(tempfile, 'TemporaryDirectory', TemporaryDirectory) @pytest.mark.skipif(no_display, reason="Doesn't work without a display") class TestCompletionWidget(unittest.TestCase): @classmethod def setUpClass(cls): """ Create the application for the test case. """ cls._app = QtWidgets.QApplication.instance() if cls._app is None: cls._app = QtWidgets.QApplication([]) cls._app.setQuitOnLastWindowClosed(False) @classmethod def tearDownClass(cls): """ Exit the application. """ QtWidgets.QApplication.quit() def setUp(self): """ Create the main widgets (ConsoleWidget) """ self.console = ConsoleWidget() self.text_edit = self.console._control def test_droplist_completer_shows(self): w = CompletionWidget(self.console) w.show_items(self.text_edit.textCursor(), ["item1", "item2", "item3"]) self.assertTrue(w.isVisible()) def test_droplist_completer_keyboard(self): w = CompletionWidget(self.console) w.show_items(self.text_edit.textCursor(), ["item1", "item2", "item3"]) QTest.keyClick(w, QtCore.Qt.Key_PageDown) QTest.keyClick(w, QtCore.Qt.Key_Enter) self.assertEqual(self.text_edit.toPlainText(), "item3") def test_droplist_completer_mousepick(self): leftButton = QtCore.Qt.LeftButton w = CompletionWidget(self.console) w.show_items(self.text_edit.textCursor(), ["item1", "item2", "item3"]) QTest.mouseClick(w.viewport(), leftButton, pos=QtCore.QPoint(19, 8)) QTest.mouseRelease(w.viewport(), leftButton, pos=QtCore.QPoint(19, 8)) QTest.mouseDClick(w.viewport(), leftButton, pos=QtCore.QPoint(19, 8)) self.assertEqual(self.text_edit.toPlainText(), "item1") self.assertFalse(w.isVisible()) def test_common_path_complete(self): with TemporaryDirectory() as tmpdir: items = [ os.path.join(tmpdir, "common/common1/item1"), os.path.join(tmpdir, "common/common1/item2"), os.path.join(tmpdir, "common/common1/item3")] for item in items: os.makedirs(item) w = CompletionWidget(self.console) w.show_items(self.text_edit.textCursor(), items) self.assertEqual(w.currentItem().text(), '/item1') QTest.keyClick(w, QtCore.Qt.Key_Down) self.assertEqual(w.currentItem().text(), '/item2') QTest.keyClick(w, QtCore.Qt.Key_Down) self.assertEqual(w.currentItem().text(), '/item3') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603554022.0 qtconsole-5.5.2/qtconsole/tests/test_frontend_widget.py0000664000175000017500000000622300000000000024274 0ustar00carloscarlos00000000000000import unittest import pytest from qtpy import QtWidgets from qtconsole.frontend_widget import FrontendWidget from qtpy.QtTest import QTest from . import no_display @pytest.mark.skipif(no_display, reason="Doesn't work without a display") class TestFrontendWidget(unittest.TestCase): @classmethod def setUpClass(cls): """ Create the application for the test case. """ cls._app = QtWidgets.QApplication.instance() if cls._app is None: cls._app = QtWidgets.QApplication([]) cls._app.setQuitOnLastWindowClosed(False) @classmethod def tearDownClass(cls): """ Exit the application. """ QtWidgets.QApplication.quit() def test_transform_classic_prompt(self): """ Test detecting classic prompts. """ w = FrontendWidget(kind='rich') t = w._highlighter.transform_classic_prompt # Base case self.assertEqual(t('>>> test'), 'test') self.assertEqual(t(' >>> test'), 'test') self.assertEqual(t('\t >>> test'), 'test') # No prompt self.assertEqual(t(''), '') self.assertEqual(t('test'), 'test') # Continuation prompt self.assertEqual(t('... test'), 'test') self.assertEqual(t(' ... test'), 'test') self.assertEqual(t(' ... test'), 'test') self.assertEqual(t('\t ... test'), 'test') # Prompts that don't match the 'traditional' prompt self.assertEqual(t('>>>test'), '>>>test') self.assertEqual(t('>> test'), '>> test') self.assertEqual(t('...test'), '...test') self.assertEqual(t('.. test'), '.. test') # Prefix indicating input from other clients self.assertEqual(t('[remote] >>> test'), 'test') # Random other prefix self.assertEqual(t('[foo] >>> test'), '[foo] >>> test') def test_transform_ipy_prompt(self): """ Test detecting IPython prompts. """ w = FrontendWidget(kind='rich') t = w._highlighter.transform_ipy_prompt # In prompt self.assertEqual(t('In [1]: test'), 'test') self.assertEqual(t('In [2]: test'), 'test') self.assertEqual(t('In [10]: test'), 'test') self.assertEqual(t(' In [1]: test'), 'test') self.assertEqual(t('\t In [1]: test'), 'test') # No prompt self.assertEqual(t(''), '') self.assertEqual(t('test'), 'test') # Continuation prompt self.assertEqual(t(' ...: test'), 'test') self.assertEqual(t(' ...: test'), 'test') self.assertEqual(t(' ...: test'), 'test') self.assertEqual(t('\t ...: test'), 'test') # Prompts that don't match the in-prompt self.assertEqual(t('In [1]:test'), 'In [1]:test') self.assertEqual(t('[1]: test'), '[1]: test') self.assertEqual(t('In: test'), 'In: test') self.assertEqual(t(': test'), ': test') self.assertEqual(t('...: test'), '...: test') # Prefix indicating input from other clients self.assertEqual(t('[remote] In [1]: test'), 'test') # Random other prefix self.assertEqual(t('[foo] In [1]: test'), '[foo] In [1]: test') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1683308066.0 qtconsole-5.5.2/qtconsole/tests/test_inprocess_kernel.py0000664000175000017500000000172000000000000024454 0ustar00carloscarlos00000000000000"""Test QtInProcessKernel""" # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import unittest from qtconsole.inprocess import QtInProcessKernelManager class InProcessTests(unittest.TestCase): def setUp(self): """Open an in-process kernel.""" self.kernel_manager = QtInProcessKernelManager() self.kernel_manager.start_kernel() self.kernel_client = self.kernel_manager.client() def tearDown(self): """Shutdown the in-process kernel. """ self.kernel_client.stop_channels() self.kernel_manager.shutdown_kernel() def test_execute(self): """Test execution of shell commands.""" # check that closed works as expected assert not self.kernel_client.iopub_channel.closed() # check that running code works self.kernel_client.execute('a=1') assert self.kernel_manager.kernel.shell.user_ns.get('a') == 1 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1678147849.0 qtconsole-5.5.2/qtconsole/tests/test_jupyter_widget.py0000664000175000017500000001156000000000000024157 0ustar00carloscarlos00000000000000import unittest import sys import pytest from qtpy import QT6 from qtpy import QtWidgets, QtGui from qtconsole.jupyter_widget import JupyterWidget from . import no_display @pytest.mark.skipif(no_display, reason="Doesn't work without a display") class TestJupyterWidget(unittest.TestCase): @classmethod def setUpClass(cls): """ Create the application for the test case. """ cls._app = QtWidgets.QApplication.instance() if cls._app is None: cls._app = QtWidgets.QApplication([]) cls._app.setQuitOnLastWindowClosed(False) @classmethod def tearDownClass(cls): """ Exit the application. """ QtWidgets.QApplication.quit() def test_stylesheet_changed(self): """ Test changing stylesheets. """ w = JupyterWidget(kind='rich') # By default, the background is light. White text is rendered as black self.assertEqual(w._ansi_processor.get_color(15).name(), '#000000') # Change to a dark colorscheme. White text is rendered as white w.syntax_style = 'monokai' self.assertEqual(w._ansi_processor.get_color(15).name(), '#ffffff') @pytest.mark.skipif(not sys.platform.startswith('linux'), reason="Works only on Linux") def test_other_output(self): """ Test displaying output from other clients. """ w = JupyterWidget(kind='rich') w._append_plain_text('Header\n') w._show_interpreter_prompt(1) w.other_output_prefix = '[other] ' w.syntax_style = 'default' msg = dict( execution_count=1, code='a = 1 + 1\nb = range(10)', ) w._append_custom(w._insert_other_input, msg, before_prompt=True) control = w._control document = control.document() self.assertEqual(document.blockCount(), 6) self.assertEqual(document.toPlainText(), ( 'Header\n' '\n' '[other] In [1]: a = 1 + 1\n' ' ...: b = range(10)\n' '\n' 'In [2]: ' )) # Check proper syntax highlighting. # This changes with every Qt6 release, that's why we don't test it on it. if not QT6: html = ( '\n' '\n' '

Header

\n' '


\n' '

[other] In [1]: a = 1 + 1

\n' '

\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0...: b = range(10)

\n' '


\n' '

In [2]:

' ) self.assertEqual(document.toHtml(), html) def test_copy_paste_prompt(self): """Test copy/paste removes partial and full prompts.""" w = JupyterWidget(kind='rich') w._show_interpreter_prompt(1) control = w._control code = " if True:\n print('a')" w._set_input_buffer(code) assert code not in control.toPlainText() cursor = w._get_prompt_cursor() pos = cursor.position() cursor.setPosition(pos - 3) cursor.movePosition(QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor) control.setTextCursor(cursor) control.hasFocus = lambda: True w.copy() clipboard = QtWidgets.QApplication.clipboard() assert clipboard.text() == code w.paste() expected = "In [1]: if True:\n ...: print('a')" assert expected in control.toPlainText() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603554022.0 qtconsole-5.5.2/qtconsole/tests/test_kill_ring.py0000664000175000017500000000434600000000000023070 0ustar00carloscarlos00000000000000import unittest import pytest from qtpy import QtGui, QtWidgets from qtconsole.kill_ring import KillRing, QtKillRing from . import no_display @pytest.mark.skipif(no_display, reason="Doesn't work without a display") class TestKillRing(unittest.TestCase): @classmethod def setUpClass(cls): """ Create the application for the test case. """ cls._app = QtWidgets.QApplication.instance() if cls._app is None: cls._app = QtWidgets.QApplication([]) cls._app.setQuitOnLastWindowClosed(False) @classmethod def tearDownClass(cls): """ Exit the application. """ QtWidgets.QApplication.quit() def test_generic(self): """ Does the generic kill ring work? """ ring = KillRing() self.assertTrue(ring.yank() is None) self.assertTrue(ring.rotate() is None) ring.kill('foo') self.assertEqual(ring.yank(), 'foo') self.assertTrue(ring.rotate() is None) self.assertEqual(ring.yank(), 'foo') ring.kill('bar') self.assertEqual(ring.yank(), 'bar') self.assertEqual(ring.rotate(), 'foo') ring.clear() self.assertTrue(ring.yank() is None) self.assertTrue(ring.rotate() is None) def test_qt_basic(self): """ Does the Qt kill ring work? """ text_edit = QtWidgets.QPlainTextEdit() ring = QtKillRing(text_edit) ring.kill('foo') ring.kill('bar') ring.yank() ring.rotate() ring.yank() self.assertEqual(text_edit.toPlainText(), 'foobar') text_edit.clear() ring.kill('baz') ring.yank() ring.rotate() ring.rotate() ring.rotate() self.assertEqual(text_edit.toPlainText(), 'foo') def test_qt_cursor(self): """ Does the Qt kill ring maintain state with cursor movement? """ text_edit = QtWidgets.QPlainTextEdit() ring = QtKillRing(text_edit) ring.kill('foo') ring.kill('bar') ring.yank() text_edit.moveCursor(QtGui.QTextCursor.Left) ring.rotate() self.assertEqual(text_edit.toPlainText(), 'bar') if __name__ == '__main__': import pytest pytest.main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603554022.0 qtconsole-5.5.2/qtconsole/tests/test_styles.py0000664000175000017500000000100700000000000022430 0ustar00carloscarlos00000000000000import unittest from qtconsole.styles import dark_color, dark_style class TestStyles(unittest.TestCase): def test_dark_color(self): self.assertTrue(dark_color('#000000')) # black self.assertTrue(not dark_color('#ffff66')) # bright yellow self.assertTrue(dark_color('#80807f')) # < 50% gray self.assertTrue(not dark_color('#808080')) # = 50% gray def test_dark_style(self): self.assertTrue(dark_style('monokai')) self.assertTrue(not dark_style('default')) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603554022.0 qtconsole-5.5.2/qtconsole/usage.py0000664000175000017500000002023500000000000020014 0ustar00carloscarlos00000000000000""" Usage information for QtConsole """ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. gui_reference = """\ ===================== The Jupyter QtConsole ===================== This console is designed to emulate the look, feel and workflow of a terminal environment. Beyond this basic design, the console also implements functionality not currently found in most terminal emulators. Some examples of these console enhancements are inline syntax highlighting, multiline editing, inline graphics, and others. This quick reference document contains the basic information you'll need to know to make the most efficient use of it. For the various command line options available at startup, type ``jupyter qtconsole --help`` at the command line. Multiline editing ================= The graphical console is capable of true multiline editing, but it also tries to behave intuitively like a terminal when possible. If you are used to IPython's old terminal behavior, you should find the transition painless. If you learn to use a few basic keybindings, the console provides even greater efficiency. For single expressions or indented blocks, the console behaves almost like the IPython terminal: single expressions are immediately evaluated, and indented blocks are evaluated once a single blank line is entered:: In [1]: print ("Hello Jupyter!") # Enter was pressed at the end of the line Hello Jupyter! In [2]: for num in range(10): ...: print(num) ...: 0 1 2 3 4 5 6 7 8 9 If you want to enter more than one expression in a single input block (something not possible in the terminal), you can use ``Control-Enter`` at the end of your first line instead of ``Enter``. At that point the console goes into 'cell mode' and even if your inputs are not indented, it will continue accepting lines until either you enter an extra blank line or you hit ``Shift-Enter`` (the key binding that forces execution). When a multiline cell is entered, the console analyzes it and executes its code producing an ``Out[n]`` prompt only for the last expression in it, while the rest of the cell is executed as if it was a script. An example should clarify this:: In [3]: x=1 # Hit Ctrl-Enter here ...: y=2 # from now on, regular Enter is sufficient ...: z=3 ...: x**2 # This does *not* produce an Out[] value ...: x+y+z # Only the last expression does ...: Out[3]: 6 The behavior where an extra blank line forces execution is only active if you are actually typing at the keyboard each line, and is meant to make it mimic the IPython terminal behavior. If you paste a long chunk of input (for example a long script copied form an editor or web browser), it can contain arbitrarily many intermediate blank lines and they won't cause any problems. As always, you can then make it execute by appending a blank line *at the end* or hitting ``Shift-Enter`` anywhere within the cell. With the up arrow key, you can retrieve previous blocks of input that contain multiple lines. You can move inside of a multiline cell like you would in any text editor. When you want it executed, the simplest thing to do is to hit the force execution key, ``Shift-Enter`` (though you can also navigate to the end and append a blank line by using ``Enter`` twice). If you are editing a multiline cell and accidentally navigate out of it using the up or down arrow keys, the console clears the cell and replaces it with the contents of the cell which the up or down arrow key stopped on. If you wish to to undo this action, perhaps because of an accidental keypress, use the Undo keybinding, ``Control-z``, to restore the original cell. Key bindings ============ The Jupyter QtConsole supports most of the basic Emacs line-oriented keybindings, in addition to some of its own. The keybindings themselves are: - ``Enter``: insert new line (may cause execution, see above). - ``Ctrl-Enter``: *force* new line, *never* causes execution. - ``Shift-Enter``: *force* execution regardless of where cursor is, no newline added. - ``Up``: step backwards through the history. - ``Down``: step forwards through the history. - ``Shift-Up``: search backwards through the history (like ``Control-r`` in bash). - ``Shift-Down``: search forwards through the history. - ``Control-c``: copy highlighted text to clipboard (prompts are automatically stripped). - ``Control-Shift-c``: copy highlighted text to clipboard (prompts are not stripped). - ``Control-v``: paste text from clipboard. - ``Control-z``: undo (retrieves lost text if you move out of a cell with the arrows). - ``Control-Shift-z``: redo. - ``Control-o``: move to 'other' area, between pager and terminal. - ``Control-l``: clear terminal. - ``Control-a``: go to beginning of line. - ``Control-e``: go to end of line. - ``Control-u``: kill from cursor to the begining of the line. - ``Control-k``: kill from cursor to the end of the line. - ``Control-y``: yank (paste) - ``Control-p``: previous line (like up arrow) - ``Control-n``: next line (like down arrow) - ``Control-f``: forward (like right arrow) - ``Control-b``: back (like left arrow) - ``Control-d``: delete next character, or exits if input is empty - ``Alt-<``: move to the beginning of the input region. - ``alt->``: move to the end of the input region. - ``Alt-d``: delete next word. - ``Alt-Backspace``: delete previous word. - ``Control-.``: force a kernel restart (a confirmation dialog appears). - ``Control-+``: increase font size. - ``Control--``: decrease font size. - ``Control-Alt-Space``: toggle full screen. (Command-Control-Space on Mac OS X) The pager ========= The Jupyter QtConsole will show long blocks of text from many sources using a built-in pager. You can control where this pager appears with the ``--paging`` command-line flag: - ``inside`` [default]: the pager is overlaid on top of the main terminal. You must quit the pager to get back to the terminal (similar to how a pager such as ``less`` or ``more`` pagers behave). - ``vsplit``: the console is made double height, and the pager appears on the bottom area when needed. You can view its contents while using the terminal. - ``hsplit``: the console is made double width, and the pager appears on the right area when needed. You can view its contents while using the terminal. - ``none``: the console displays output without paging. If you use the vertical or horizontal paging modes, you can navigate between terminal and pager as follows: - Tab key: goes from pager to terminal (but not the other way around). - Control-o: goes from one to another always. - Mouse: click on either. In all cases, the ``q`` or ``Escape`` keys quit the pager (when used with the focus on the pager area). Running subprocesses ==================== When running a subprocess from the kernel, you can not interact with it as if it was running in a terminal. So anything that invokes a pager or expects you to type input into it will block and hang (you can kill it with ``Control-C``). The console can use magics provided by the IPython kernel. These magics include ``%less`` to page files (aliased to ``%more``), ``%clear`` to clear the terminal, and ``%man`` on Linux/OSX. These cover the most common commands you'd want to call in your subshell and that would cause problems if invoked via ``!cmd``, but you need to be aware of this limitation. Display ======= For example, if using the IPython kernel, there are functions available for object display: In [4]: from IPython.display import display In [5]: from IPython.display import display_png, display_svg Python objects can simply be passed to these functions and the appropriate representations will be displayed in the console as long as the objects know how to compute those representations. The easiest way of teaching objects how to format themselves in various representations is to define special methods such as: ``_repr_svg_`` and ``_repr_png_``. IPython's display formatters can also be given custom formatter functions for various types:: In [6]: ip = get_ipython() In [7]: png_formatter = ip.display_formatter.formatters['image/png'] In [8]: png_formatter.for_type(Foo, foo_to_png) For further details, see ``IPython.core.formatters``. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1700088347.0 qtconsole-5.5.2/qtconsole/util.py0000664000175000017500000002024200000000000017663 0ustar00carloscarlos00000000000000""" Defines miscellaneous Qt-related helper classes and functions. """ import inspect from qtpy import QtCore, QtGui from traitlets import HasTraits, TraitType #----------------------------------------------------------------------------- # Metaclasses #----------------------------------------------------------------------------- MetaHasTraits = type(HasTraits) MetaQObject = type(QtCore.QObject) class MetaQObjectHasTraits(MetaQObject, MetaHasTraits): """ A metaclass that inherits from the metaclasses of HasTraits and QObject. Using this metaclass allows a class to inherit from both HasTraits and QObject. Using SuperQObject instead of QObject is highly recommended. See QtKernelManager for an example. """ def __new__(mcls, name, bases, classdict): # FIXME: this duplicates the code from MetaHasTraits. # I don't think a super() call will help me here. for k,v in iter(classdict.items()): if isinstance(v, TraitType): v.name = k elif inspect.isclass(v): if issubclass(v, TraitType): vinst = v() vinst.name = k classdict[k] = vinst cls = MetaQObject.__new__(mcls, name, bases, classdict) return cls def __init__(mcls, name, bases, classdict): # Note: super() did not work, so we explicitly call these. MetaQObject.__init__(mcls, name, bases, classdict) MetaHasTraits.__init__(mcls, name, bases, classdict) #----------------------------------------------------------------------------- # Classes #----------------------------------------------------------------------------- def superQ(QClass): """ Permits the use of super() in class hierarchies that contain Qt classes. Unlike QObject, SuperQObject does not accept a QObject parent. If it did, super could not be emulated properly (all other classes in the heierarchy would have to accept the parent argument--they don't, of course, because they don't inherit QObject.) This class is primarily useful for attaching signals to existing non-Qt classes. See QtKernelManagerMixin for an example. """ class SuperQClass(QClass): def __new__(cls, *args, **kw): # We initialize QClass as early as possible. Without this, Qt complains # if SuperQClass is not the first class in the super class list. inst = QClass.__new__(cls) QClass.__init__(inst) return inst def __init__(self, *args, **kw): # Emulate super by calling the next method in the MRO, if there is one. mro = self.__class__.mro() for qt_class in QClass.mro(): mro.remove(qt_class) next_index = mro.index(SuperQClass) + 1 if next_index < len(mro): mro[next_index].__init__(self, *args, **kw) return SuperQClass SuperQObject = superQ(QtCore.QObject) #----------------------------------------------------------------------------- # Functions #----------------------------------------------------------------------------- def get_font(family, fallback=None): """Return a font of the requested family, using fallback as alternative. If a fallback is provided, it is used in case the requested family isn't found. If no fallback is given, no alternative is chosen and Qt's internal algorithms may automatically choose a fallback font. Parameters ---------- family : str A font name. fallback : str A font name. Returns ------- font : QFont object """ font = QtGui.QFont(family) # Check whether we got what we wanted using QFontInfo, since exactMatch() # is overly strict and returns false in too many cases. font_info = QtGui.QFontInfo(font) if fallback is not None and font_info.family() != family: font = QtGui.QFont(fallback) return font # ----------------------------------------------------------------------------- # Vendored from ipython_genutils # ----------------------------------------------------------------------------- def _chunks(l, n): """Yield successive n-sized chunks from l.""" for i in range(0, len(l), n): yield l[i : i + n] def _find_optimal(rlist, *, separator_size, displaywidth): """Calculate optimal info to columnize a list of strings""" for nrow in range(1, len(rlist) + 1): chk = list(map(max, _chunks(rlist, nrow))) sumlength = sum(chk) ncols = len(chk) if sumlength + separator_size * (ncols - 1) <= displaywidth: break return { "columns_numbers": ncols, "rows_numbers": nrow, "columns_width": chk, } def _get_or_default(mylist, i, *, default): """return list item number, or default if don't exist""" if i >= len(mylist): return default else: return mylist[i] def compute_item_matrix(items, empty=None, *, separator_size=2, displaywidth=80): """Returns a nested list, and info to columnize items Parameters ---------- items list of strings to columnize empty : (default None) Default value to fill list if needed separator_size : int (default=2) How much characters will be used as a separation between each column. displaywidth : int (default=80) The width of the area onto which the columns should enter Returns ------- strings_matrix nested list of strings, the outer most list contains as many list as rows, the innermost lists have each as many element as column. If the total number of elements in `items` does not equal the product of rows*columns, the last element of some lists are filled with `None`. dict_info Some info to make columnize easier: columns_numbers number of columns rows_numbers number of rows columns_width list of width of each columns Examples -------- :: In [1]: l = ['aaa','b','cc','d','eeeee','f','g','h','i','j','k','l'] ...: compute_item_matrix(l,displaywidth=12) Out[1]: ([['aaa', 'f', 'k'], ['b', 'g', 'l'], ['cc', 'h', None], ['d', 'i', None], ['eeeee', 'j', None]], {'columns_numbers': 3, 'columns_width': [5, 1, 1], 'rows_numbers': 5}) """ info = _find_optimal( [len(it) for it in items], separator_size=separator_size, displaywidth=displaywidth ) nrow, ncol = info["rows_numbers"], info["columns_numbers"] return ( [ [_get_or_default(items, c * nrow + i, default=empty) for c in range(ncol)] for i in range(nrow) ], info, ) def columnize(items, separator=" ", displaywidth=80): """Transform a list of strings into a single string with columns. Parameters ---------- items : sequence of strings The strings to process. Returns ------- The formatted string. """ if not items: return "\n" matrix, info = compute_item_matrix( items, separator_size=len(separator), displaywidth=displaywidth ) fmatrix = [filter(None, x) for x in matrix] sjoin = lambda x: separator.join( [y.ljust(w, " ") for y, w in zip(x, info["columns_width"])] ) return "\n".join(map(sjoin, fmatrix)) + "\n" def import_item(name): """Import and return ``bar`` given the string ``foo.bar``. Calling ``bar = import_item("foo.bar")`` is the functional equivalent of executing the code ``from foo import bar``. Parameters ---------- name : string The fully qualified name of the module/package being imported. Returns ------- mod : module object The module that was imported. """ parts = name.rsplit(".", 1) if len(parts) == 2: # called with 'foo.bar....' package, obj = parts module = __import__(package, fromlist=[obj]) try: pak = getattr(module, obj) except AttributeError: raise ImportError("No module named %s" % obj) return pak else: # called with un-dotted string return __import__(parts[0]) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1714924509.6896565 qtconsole-5.5.2/qtconsole.egg-info/0000775000175000017500000000000000000000000020026 5ustar00carloscarlos00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1714924509.0 qtconsole-5.5.2/qtconsole.egg-info/PKG-INFO0000644000175000017500000001170000000000000021120 0ustar00carloscarlos00000000000000Metadata-Version: 2.1 Name: qtconsole Version: 5.5.2 Summary: Jupyter Qt console Home-page: http://jupyter.org Author: Jupyter Development Team Author-email: jupyter@googlegroups.com Maintainer: Spyder Development Team License: BSD Keywords: Interactive,Interpreter,Shell Platform: Linux Platform: Mac OS X Platform: Windows Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Requires-Python: >= 3.8 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: traitlets!=5.2.1,!=5.2.2 Requires-Dist: jupyter_core Requires-Dist: jupyter_client>=4.1 Requires-Dist: pygments Requires-Dist: ipykernel>=4.1 Requires-Dist: qtpy>=2.4.0 Requires-Dist: pyzmq>=17.1 Requires-Dist: packaging Provides-Extra: test Requires-Dist: flaky; extra == "test" Requires-Dist: pytest; extra == "test" Requires-Dist: pytest-qt; extra == "test" Provides-Extra: doc Requires-Dist: Sphinx>=1.3; extra == "doc" # Jupyter QtConsole ![Windows tests](https://github.com/jupyter/qtconsole/workflows/Windows%20tests/badge.svg) ![Macos tests](https://github.com/jupyter/qtconsole/workflows/Macos%20tests/badge.svg) ![Linux tests](https://github.com/jupyter/qtconsole/workflows/Linux%20tests/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/jupyter/qtconsole/badge.svg?branch=master)](https://coveralls.io/github/jupyter/qtconsole?branch=master) [![Documentation Status](https://readthedocs.org/projects/qtconsole/badge/?version=stable)](https://qtconsole.readthedocs.io/en/stable/) [![Google Group](https://img.shields.io/badge/-Google%20Group-lightgrey.svg)](https://groups.google.com/forum/#!forum/jupyter) A rich Qt-based console for working with Jupyter kernels, supporting rich media output, session export, and more. The Qtconsole is a very lightweight application that largely feels like a terminal, but provides a number of enhancements only possible in a GUI, such as inline figures, proper multiline editing with syntax highlighting, graphical calltips, and more. ![qtconsole](https://raw.githubusercontent.com/jupyter/qtconsole/master/docs/source/_images/qtconsole.png) ## Install Qtconsole The Qtconsole requires Python bindings for Qt, such as [PyQt6](https://pypi.org/project/PyQt6/), [PySide6](https://pypi.org/project/PySide6/), [PyQt5](https://pypi.org/project/PyQt5/) or [PySide2](https://pypi.org/project/PySide2/). Although [pip](https://pypi.python.org/pypi/pip) and [conda](http://conda.pydata.org/docs) may be used to install the Qtconsole, conda is simpler to use since it automatically installs PyQt5. Alternatively, the Qtconsole installation with pip needs additional steps since pip doesn't install the Qt requirement. ### Install using conda To install: conda install qtconsole **Note:** If the Qtconsole is installed using conda, it will **automatically** install the Qt requirement as well. ### Install using pip To install: pip install qtconsole **Note:** Make sure that Qt is installed. Unfortunately, Qt is not installed when using pip. The next section gives instructions on doing it. ### Installing Qt (if needed) You can install PyQt5 with pip using the following command: pip install pyqt5 or with a system package manager on Linux. For Windows, PyQt binary packages may be used. **Note:** Additional information about using a system package manager may be found in the [qtconsole documentation](https://qtconsole.readthedocs.io). More installation instructions for PyQt can be found in the [PyQt5 documentation](http://pyqt.sourceforge.net/Docs/PyQt5/installation.html) and [PyQt4 documentation](http://pyqt.sourceforge.net/Docs/PyQt4/installation.html) Source packages for Windows/Linux/MacOS can be found here: [PyQt5](https://www.riverbankcomputing.com/software/pyqt/download5) and [PyQt4](https://riverbankcomputing.com/software/pyqt/download). ## Usage To run the Qtconsole: jupyter qtconsole ## Resources - [Project Jupyter website](https://jupyter.org) - Documentation for the Qtconsole * [latest version](https://qtconsole.readthedocs.io/en/latest/) [[PDF](https://media.readthedocs.org/pdf/qtconsole/latest/qtconsole.pdf)] * [stable version](https://qtconsole.readthedocs.io/en/stable/) [[PDF](https://media.readthedocs.org/pdf/qtconsole/stable/qtconsole.pdf)] - [Documentation for Project Jupyter](https://jupyter.readthedocs.io/en/latest/index.html) [[PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf)] - [Issues](https://github.com/jupyter/qtconsole/issues) - [Technical support - Jupyter Google Group](https://groups.google.com/forum/#!forum/jupyter) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1714924509.0 qtconsole-5.5.2/qtconsole.egg-info/SOURCES.txt0000664000175000017500000000413600000000000021716 0ustar00carloscarlos00000000000000.coveragerc .gitignore .mailmap .readthedocs.yaml CONTRIBUTING.md LICENSE MANIFEST.in README.md RELEASE.md setup.py .github/workflows/linux-tests.yml .github/workflows/macos-tests.yml .github/workflows/windows-tests.yml docs/Makefile docs/autogen_config.py docs/environment.yml docs/gh-pages.py docs/source/changelog.rst docs/source/conf.py docs/source/index.rst docs/source/installation.rst docs/source/_images/qtconsole.png docs/source/figs/besselj.png docs/source/figs/colors_dark.png docs/source/figs/jn.html docs/source/figs/jn.xhtml examples/embed_qtconsole.py examples/inprocess_qtconsole.py examples/jupyter-qtconsole.desktop qtconsole/__init__.py qtconsole/__main__.py qtconsole/_version.py qtconsole/ansi_code_processor.py qtconsole/base_frontend_mixin.py qtconsole/bracket_matcher.py qtconsole/call_tip_widget.py qtconsole/client.py qtconsole/comms.py qtconsole/completion_html.py qtconsole/completion_plain.py qtconsole/completion_widget.py qtconsole/console_widget.py qtconsole/frontend_widget.py qtconsole/history_console_widget.py qtconsole/inprocess.py qtconsole/ipython_widget.py qtconsole/jupyter_widget.py qtconsole/kernel_mixins.py qtconsole/kill_ring.py qtconsole/mainwindow.py qtconsole/manager.py qtconsole/pygments_highlighter.py qtconsole/qstringhelpers.py qtconsole/qtconsoleapp.py qtconsole/rich_ipython_widget.py qtconsole/rich_jupyter_widget.py qtconsole/rich_text.py qtconsole/styles.py qtconsole/svg.py qtconsole/usage.py qtconsole/util.py qtconsole.egg-info/PKG-INFO qtconsole.egg-info/SOURCES.txt qtconsole.egg-info/dependency_links.txt qtconsole.egg-info/entry_points.txt qtconsole.egg-info/requires.txt qtconsole.egg-info/top_level.txt qtconsole/resources/icon/JupyterConsole.svg qtconsole/tests/__init__.py qtconsole/tests/test_00_console_widget.py qtconsole/tests/test_ansi_code_processor.py qtconsole/tests/test_app.py qtconsole/tests/test_comms.py qtconsole/tests/test_completion_widget.py qtconsole/tests/test_frontend_widget.py qtconsole/tests/test_inprocess_kernel.py qtconsole/tests/test_jupyter_widget.py qtconsole/tests/test_kill_ring.py qtconsole/tests/test_styles.py requirements/environment.yml././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1714924509.0 qtconsole-5.5.2/qtconsole.egg-info/dependency_links.txt0000664000175000017500000000000100000000000024074 0ustar00carloscarlos00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1714924509.0 qtconsole-5.5.2/qtconsole.egg-info/entry_points.txt0000664000175000017500000000007600000000000023327 0ustar00carloscarlos00000000000000[gui_scripts] jupyter-qtconsole = qtconsole.qtconsoleapp:main ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1714924509.0 qtconsole-5.5.2/qtconsole.egg-info/requires.txt0000664000175000017500000000024600000000000022430 0ustar00carloscarlos00000000000000traitlets!=5.2.1,!=5.2.2 jupyter_core jupyter_client>=4.1 pygments ipykernel>=4.1 qtpy>=2.4.0 pyzmq>=17.1 packaging [doc] Sphinx>=1.3 [test] flaky pytest pytest-qt ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1714924509.0 qtconsole-5.5.2/qtconsole.egg-info/top_level.txt0000664000175000017500000000001200000000000022551 0ustar00carloscarlos00000000000000qtconsole ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1714924509.6896565 qtconsole-5.5.2/requirements/0000775000175000017500000000000000000000000017050 5ustar00carloscarlos00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1697726811.0 qtconsole-5.5.2/requirements/environment.yml0000664000175000017500000000035100000000000022136 0ustar00carloscarlos00000000000000channels: - conda-forge dependencies: # Main dependencies - pyqt - qtpy >=2.0.1 - traitlets - jupyter_core - jupyter_client - pygments - ipykernel - pyzmq >=17.1 # For testing - coveralls - flaky - pytest - pytest-cov - pytest-qt ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1714924509.6896565 qtconsole-5.5.2/setup.cfg0000664000175000017500000000004600000000000016146 0ustar00carloscarlos00000000000000[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1697728145.0 qtconsole-5.5.2/setup.py0000664000175000017500000000635200000000000016045 0ustar00carloscarlos00000000000000#!/usr/bin/env python # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. # the name of the package name = 'qtconsole' #----------------------------------------------------------------------------- # Minimal Python version sanity check #----------------------------------------------------------------------------- import sys v = sys.version_info if v[0] >= 3 and v[:2] < (3, 7): error = "ERROR: %s requires Python version 3.8 or above." % name print(error, file=sys.stderr) sys.exit(1) #----------------------------------------------------------------------------- # get on with it #----------------------------------------------------------------------------- import io import os from setuptools import setup pjoin = os.path.join here = os.path.abspath(os.path.dirname(__file__)) packages = [] for d, _, _ in os.walk(pjoin(here, name)): if os.path.exists(pjoin(d, '__init__.py')): packages.append(d[len(here) + 1:].replace(os.path.sep, '.')) package_data = { 'qtconsole' : ['resources/icon/*.svg'], } version_ns = {} with open(pjoin(here, name, '_version.py')) as f: exec(f.read(), {}, version_ns) with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: long_description = f.read() setup_args = dict( name = name, version = version_ns['__version__'], packages = packages, package_data = package_data, description = "Jupyter Qt console", long_description = long_description, long_description_content_type = 'text/markdown', author = 'Jupyter Development Team', author_email = 'jupyter@googlegroups.com', maintainer = 'Spyder Development Team', url = 'http://jupyter.org', license = 'BSD', platforms = "Linux, Mac OS X, Windows", keywords = ['Interactive', 'Interpreter', 'Shell'], python_requires = '>= 3.8', install_requires = [ 'traitlets!=5.2.1,!=5.2.2', 'jupyter_core', 'jupyter_client>=4.1', 'pygments', 'ipykernel>=4.1', # not a real dependency, but require the reference kernel 'qtpy>=2.4.0', 'pyzmq>=17.1', 'packaging' ], extras_require = { 'test': ['flaky', 'pytest', 'pytest-qt'], 'doc': 'Sphinx>=1.3', }, entry_points = { 'gui_scripts': [ 'jupyter-qtconsole = qtconsole.qtconsoleapp:main', ] }, classifiers = [ 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', ], ) if __name__ == '__main__': setup(**setup_args)