├── .deepsource.toml ├── .github ├── dependabot.yml └── workflows │ ├── sphinx.yml │ └── test.yml ├── .gitignore ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── GPL_LICENSE.txt ├── MANIFEST.in ├── README.rst ├── appveyor.yml ├── appveyor └── deploy_package.sh ├── docs ├── .nojekyll ├── Makefile ├── _images │ ├── add_single_voi.png │ ├── add_vois.png │ ├── add_vois_added.png │ ├── add_vois_visible.png │ ├── beam_kernel_setup.png │ ├── contour_request.png │ ├── create_empty_patient.png │ ├── creating_field.png │ ├── creating_plan_target.png │ ├── executing_plan_end_wo_dvh.png │ ├── executing_plan_start.png │ ├── main_window_dose.png │ ├── main_window_empty.png │ ├── main_window_export.png │ ├── main_window_patient.png │ ├── main_window_patient_loaded_contours.png │ ├── new_patient_small.png │ ├── plan_setup_dose_delivery.png │ ├── plan_setup_info.png │ ├── plan_setup_optimization.png │ ├── plan_setup_results.png │ ├── trip_configuration_local.png │ └── trip_configuration_remote.png ├── authors.rst ├── conf.py ├── contributing.rst ├── index.rst ├── make.bat ├── readme.rst ├── requirements.txt ├── technical.rst └── user_guide.rst ├── main.spec ├── pytest.ini ├── pytripgui ├── __init__.py ├── add_vois_vc │ ├── __init__.py │ ├── add_single_voi_vc │ │ ├── __init__.py │ │ ├── add_single_voi_cont.py │ │ └── add_single_voi_view.py │ ├── add_vois_cont.py │ ├── add_vois_view.py │ ├── voi_widget.py │ └── widgets │ │ ├── __init__.py │ │ ├── cuboidal_voi.ui │ │ ├── cylindrical_voi.ui │ │ ├── list_element_voi.ui │ │ └── spherical_voi.ui ├── app_logic │ ├── __init__.py │ ├── app_callbacks.py │ ├── charts.py │ ├── gui_executor.py │ ├── patient_tree.py │ └── viewcanvas.py ├── canvas_vc │ ├── __init__.py │ ├── canvas_controller.py │ ├── canvas_view.py │ ├── gui_state.py │ ├── objects │ │ ├── __init__.py │ │ ├── ctx.py │ │ ├── data_base.py │ │ ├── dos.py │ │ ├── let.py │ │ ├── vc_text.py │ │ └── vdx.py │ ├── plot_model.py │ ├── plotter │ │ ├── __init__.py │ │ ├── bars │ │ │ ├── __init__.py │ │ │ ├── bar_base.py │ │ │ ├── ctx_bar.py │ │ │ ├── dos_bar.py │ │ │ ├── let_bar.py │ │ │ └── projection_enum.py │ │ ├── coordinate_info.py │ │ ├── images │ │ │ ├── __init__.py │ │ │ ├── ctx_image.py │ │ │ ├── dose_image.py │ │ │ ├── let_image.py │ │ │ └── patient_image_base.py │ │ ├── managers │ │ │ ├── __init__.py │ │ │ ├── blit_manager.py │ │ │ ├── placement_manager.py │ │ │ ├── plotting_manager.py │ │ │ └── voi_manager.py │ │ └── mpl_plotter.py │ └── projection_selector.py ├── config_vc │ ├── __init__.py │ ├── config_cont.py │ └── config_view.py ├── contouring_vc │ ├── __init__.py │ ├── contouring_controller.py │ └── contouring_view.py ├── controller │ ├── __init__.py │ ├── dvh.py │ ├── lvh.py │ └── settings_cont.py ├── empty_patient_vc │ ├── __init__.py │ ├── empty_patient_cont.py │ └── empty_patient_view.py ├── executor_vc │ ├── __init__.py │ └── executor_view.py ├── field_vc │ ├── __init__.py │ ├── angles_standard.py │ ├── field_cont.py │ ├── field_model.py │ └── field_view.py ├── kernel_vc │ ├── __init__.py │ ├── kernel_cont.py │ └── kernel_view.py ├── loading_file_vc │ ├── __init__.py │ ├── loading_file_controller.py │ ├── loading_file_view.py │ └── monophy.gif ├── main.py ├── main_window_qt_vc │ ├── __init__.py │ ├── main_window_cont.py │ └── main_window_view.py ├── messages.py ├── model │ ├── __init__.py │ └── main_model.py ├── plan_executor │ ├── __init__.py │ ├── executor.py │ ├── html_executor_logger.py │ ├── patient_model.py │ ├── simulation_results.py │ ├── threaded_executor.py │ └── trip_config.py ├── plan_vc │ ├── __init__.py │ ├── plan_cont.py │ └── plan_view.py ├── res │ ├── LICENSE.rst │ ├── add_patient.png │ ├── add_vois.png │ ├── create_field.png │ ├── create_plan.png │ ├── execute.png │ └── icon.ico ├── tree_vc │ ├── __init__.py │ ├── tree_controller.py │ ├── tree_items.py │ ├── tree_model.py │ └── tree_view.py ├── util.py ├── utils │ ├── __init__.py │ └── regex.py ├── version.py └── view │ ├── __init__.py │ ├── add_patient.ui │ ├── add_single_voi.ui │ ├── add_vois.ui │ ├── chart_widget.py │ ├── contouring_dialog.ui │ ├── data_sample.py │ ├── data_sample.ui │ ├── dialogs.py │ ├── empty_patient.ui │ ├── execute.ui │ ├── execute_config.ui │ ├── execute_config_view.py │ ├── field.ui │ ├── kernel.ui │ ├── loading_file_dialog.ui │ ├── main_view.py │ ├── main_window.ui │ ├── plan.ui │ ├── plot_volhist.py │ ├── qt_gui.py │ ├── qt_view_adapter.py │ ├── trip_config.ui │ └── viewcanvas.ui ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── requirements-test.txt ├── test_kernel_dialog.py ├── test_patient_tree.py └── test_pytripgui.py └── win_innosetup.iss /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | test_patterns = ["tests/**"] 4 | 5 | [[analyzers]] 6 | name = "python" 7 | enabled = true 8 | 9 | [analyzers.meta] 10 | runtime_version = "3.x.x" 11 | max_line_length = 120 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | # Maintain dependencies for GitHub Actions 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | 10 | - package-ecosystem: "pip" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | # Add reviewers 15 | reviewers: 16 | - "grzanka" -------------------------------------------------------------------------------- /.github/workflows/sphinx.yml: -------------------------------------------------------------------------------- 1 | name: Sphinx build 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: Set up Python 11 | uses: actions/setup-python@v4 12 | with: 13 | python-version: 3.9 14 | - name: Upgrade pip 15 | run: python -m pip install --upgrade pip 16 | - name: Build HTML 17 | run: | 18 | python -m pip install sphinx 19 | python -m pip install -r docs/requirements.txt 20 | make -C docs html 21 | - name: Upload artifacts 22 | uses: actions/upload-artifact@v3 23 | with: 24 | name: html-docs 25 | path: docs/_build/html/ 26 | - name: Deploy 27 | uses: peaceiris/actions-gh-pages@v3 28 | if: github.ref == 'refs/heads/master' 29 | with: 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | publish_dir: docs/_build/html 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Python application 3 | 4 | on: 5 | push: 6 | branches: [ master ] 7 | tags: ['*'] 8 | pull_request: 9 | branches: [ master ] 10 | release: 11 | types: [published] 12 | 13 | 14 | jobs: 15 | smoke_test: 16 | if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Set up Python 22 | uses: actions/setup-python@v4 23 | with: 24 | python-version: 3.9 25 | 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install -r requirements.txt 30 | pip install -r tests/requirements-test.txt 31 | sudo apt install -y libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 32 | 33 | - name: Smoke tests 34 | env: 35 | QT_DEBUG_PLUGINS: 1 36 | DISPLAY: ':99.0' 37 | run: | 38 | ulimit -c unlimited 39 | sudo catchsegv xvfb-run --auto-servernum `which python` -m pytest tests 40 | 41 | 42 | normal_test: 43 | if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" 44 | strategy: 45 | matrix: 46 | python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] 47 | platform: [ubuntu-latest, windows-latest] 48 | 49 | runs-on: ${{ matrix.platform }} 50 | needs: [smoke_test] 51 | 52 | steps: 53 | - uses: actions/checkout@v3 54 | 55 | - name: Set up Python 56 | uses: actions/setup-python@v4 57 | with: 58 | python-version: ${{ matrix.python-version }} 59 | 60 | - name: Install dependencies 61 | run: | 62 | python -m pip install --upgrade pip 63 | pip install -r requirements.txt 64 | pip install -r tests/requirements-test.txt 65 | 66 | - name: Install dependencies (Linux) 67 | if: ${{ runner.os == 'Linux' }} 68 | run: | 69 | sudo apt install -y libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 70 | 71 | - name: Regular test (Linux) 72 | if: ${{ runner.os == 'Linux' }} 73 | env: 74 | DISPLAY: ':99.0' 75 | run: | 76 | ulimit -c unlimited 77 | sudo catchsegv xvfb-run --auto-servernum `which python` -m pytest tests/ 78 | 79 | - name: Regular test (Windows) 80 | if: ${{ runner.os == 'Windows' }} 81 | env: 82 | QT_DEBUG_PLUGINS: 1 83 | run: | 84 | pytest tests/ 85 | 86 | make_and_upload_package: 87 | if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" 88 | 89 | runs-on: ubuntu-latest 90 | needs: [normal_test] 91 | steps: 92 | - uses: actions/checkout@v3 93 | with: 94 | fetch-depth: 0 95 | 96 | - name: Set up Python 97 | uses: actions/setup-python@v4 98 | with: 99 | python-version: 3.9 100 | 101 | - name: Install dependencies 102 | run: | 103 | python -m pip install --upgrade pip 104 | pip install -r requirements.txt 105 | 106 | - name: Make wheel package 107 | run: | 108 | pip install wheel 109 | 110 | # first call would generate VERSION file 111 | PYTHONPATH=. python pytripgui/main.py --version 112 | 113 | python setup.py bdist_wheel 114 | # makes source package 115 | python setup.py sdist 116 | 117 | - name: Publish packages to pypi 118 | uses: pypa/gh-action-pypi-publish@v1.5.0 119 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 120 | with: 121 | # Password for your PyPI user or an access token 122 | password: ${{ secrets.PYPI_TOKEN }} 123 | # The repository URL to use 124 | repository_url: "https://upload.pypi.org/legacy/" 125 | # The target directory for distribution 126 | packages_dir: dist/ 127 | # Show verbose output. 128 | verbose: true 129 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Leszek Grzanka 9 | 10 | Contributors 11 | ------------ 12 | 13 | * Jakob Toftegaard 14 | * Niels Bassler 15 | * Toke Printz 16 | * Leszek Grzanka 17 | * Łukasz Jeleń 18 | * Arkadiusz Ćwikła 19 | * Joanna Fortuna 20 | * Michał Krawczyk 21 | * Mateusz Łaszczyk 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are welcome, and they are greatly appreciated! Every 8 | little bit helps, and credit will always be given. 9 | 10 | You can contribute in many ways: 11 | 12 | Types of Contributions 13 | ---------------------- 14 | 15 | Report Bugs 16 | ~~~~~~~~~~~ 17 | 18 | Report bugs at https://github.com/pytrip/pytripgui/issues. 19 | 20 | If you are reporting a bug, please include: 21 | 22 | * Your operating system name and version. 23 | * Any details about your local setup that might be helpful in troubleshooting. 24 | * Detailed steps to reproduce the bug. 25 | 26 | Issues 27 | ~~~~~~ 28 | 29 | There are two types of issues: bugs and features. They are open to everyone. 30 | 31 | Write Documentation 32 | ~~~~~~~~~~~~~~~~~~~ 33 | 34 | `PyTRiPGUI` could always use more documentation, whether as part of the 35 | official pytripgui docs, in docstrings, or even on the web in blog posts, 36 | articles, and such. 37 | 38 | Submit Feedback 39 | ~~~~~~~~~~~~~~~ 40 | 41 | | The best way to send feedback is to file an issue at 42 | | https://github.com/pytrip/pytripgui/issues. 43 | 44 | If you are proposing a feature: 45 | 46 | * Explain in detail how it would work. 47 | * Keep the scope as narrow as possible, to make it easier to implement. 48 | * Remember that this is a volunteer-driven project, and that contributions 49 | are welcome. :) 50 | 51 | Get Started! 52 | ------------ 53 | 54 | Ready to contribute? Here's how to set up `PyTRiPGUI` for local development. 55 | 56 | 1. Fork the `pytripgui` repo on GitHub. 57 | 2. Clone your fork locally:: 58 | 59 | $ git clone git@github.com:your_name_here/pytripgui.git 60 | 61 | 3. Install your local copy into a virtualenv. This is how you set up your fork for local development:: 62 | 63 | $ cd pytripgui/ 64 | $ python -m venv directory_to_create_venv 65 | 66 | 4. Activate newly created virtual environment: 67 | 68 | On Linux:: 69 | 70 | $ source directory_to_create_venv/bin/activate 71 | 72 | On Windows:: 73 | 74 | $ directory_to_create_venv\Scripts\activate.bat 75 | 76 | 5. Finally install all dependencies needed by your package to develop the code:: 77 | 78 | $ python setup.py develop 79 | 80 | 6. Create a branch for local development:: 81 | 82 | $ git checkout -b name-of-your-bugfix-or-feature 83 | 84 | Now you can make your changes locally. 85 | 86 | 7. (optional) When you're done making changes, you can check locally that your changes pass flake8 and the tests, 87 | including testing other Python versions with pytest:: 88 | 89 | $ flake8 pytripgui tests 90 | $ pytest tests 91 | 92 | To get flake8 and pytest, just pip install them into your virtual environment. 93 | 94 | 8. Commit your changes and push your branch to GitHub:: 95 | 96 | $ git add . 97 | $ git commit -m "Your detailed description of your changes." 98 | $ git push origin name-of-your-bugfix-or-feature 99 | 100 | 9. Submit a pull request through the GitHub website, following our Pull Request Guidelines. 101 | 102 | Pull Request Guidelines 103 | ----------------------- 104 | 105 | Before you submit a pull request, check that it meets these guidelines: 106 | 107 | 1. The pull request should include tests. 108 | 2. If the pull request adds functionality, the docs should be updated. Put 109 | your new functionality into a function with a docstring. 110 | 3. The pull request should work for all supported Python versions and operating systems. Check 111 | https://github.com/pytrip/pytripgui/actions 112 | and make sure that the automated tests pass. 113 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | include *.py 3 | include GPL_LICENSE.txt 4 | include requirements.txt 5 | recursive-include docs *.rst 6 | recursive-exclude docs *.py 7 | graft pytripgui 8 | recursive-exclude pytripgui *.pyc 9 | exclude .gitkeep 10 | exclude .gitconfig 11 | exclude pytest.ini 12 | exclude appveyor.yml 13 | exclude .deepsource.toml 14 | exclude *.iss 15 | exclude *.spec 16 | prune appveyor 17 | prune tests 18 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | PyTRiP98GUI 3 | =========== 4 | 5 | **PyTRiPGUI** is the graphical user interface (GUI) built around the TRiP98 planning system and PyTRiP package. 6 | It is capable of visualising patient CT data, dose and LET overlays. 7 | PyTRiPGUI can make treatment plans using the local or remote TRiP98 package. 8 | 9 | The TRiP98 package is not included here. If you need it, first go to `the TRiP98 webpage `_. 10 | 11 | Quick installation guide 12 | ------------------------ 13 | 14 | PyTRiPGUI currently works on Linux, Windows and macOS with Python 3.6-3.10. 15 | 16 | Windows 17 | ~~~~~~~ 18 | 19 | On Windows you can install PyTRiPGUI using the console or the installer. We recommend using the latest installer 20 | that is `available here `_. 21 | To install, download the .exe file and run it. The installer doesn't require preinstalled Python. 22 | 23 | Linux and macOS 24 | ~~~~~~~~~~~~~~~ 25 | 26 | We recommend that you run a recent Linux distribution with Python version at least 3.6. 27 | 28 | To automatically download and install the PyTRiPGUI system-wide, type:: 29 | 30 | $ pip install pytrip98gui 31 | 32 | NOTE: the pip package is named **pytrip98gui**, while the name of the project is **pytripgui**. 33 | 34 | Start it by calling:: 35 | 36 | $ pytripgui 37 | 38 | More documentation 39 | ------------------ 40 | 41 | For more information, please see the `detailed documentation `_. 42 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # validation page for appveyor config: https://ci.appveyor.com/tools/validate-yaml 2 | 3 | # latest image with newer windows and python 4 | image: Visual Studio 2019 5 | 6 | # we are not building Visual Studio project, so default build step is off 7 | build: off 8 | 9 | environment: 10 | matrix: 11 | - platform: x64 12 | PYTHON: "C:\\Python38-x64" 13 | 14 | install: 15 | # If there is a newer build queued for the same PR, cancel this one. 16 | # The AppVeyor 'rollout builds' option is supposed to serve the same 17 | # purpose but it is problematic because it tends to cancel builds pushed 18 | # directly to master instead of just PR builds (or the converse). 19 | # credits: JuliaLang developers. 20 | - ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod ` 21 | https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | ` 22 | Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { ` 23 | throw "There are newer queued builds for this pull request, failing early." } 24 | # Prepend Python to the PATH of this build 25 | - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" 26 | # update pip 27 | - python -m pip install --upgrade pip 28 | # check versions 29 | - python -V 30 | - pip -V 31 | - python -m pip -V 32 | - pip list 33 | # check 32 or 64 bit 34 | - python -c "import struct;print(8 * struct.calcsize('P'))" 35 | # install usual requirements 36 | - python -m pip install --upgrade setuptools 37 | - python -m pip install -r requirements.txt 38 | - pip list 39 | # check numpy version 40 | - python -c "import numpy as np;print(np.version.version)" 41 | 42 | build_script: 43 | # py to exe package 44 | # install pyinstaller 45 | - python -m pip install pyinstaller 46 | # newer pydicom doesn't work 47 | - python -m pip install "pydicom==2.1.2" 48 | # check python packages list and theirs version 49 | - pip list 50 | # generate installer 51 | - python setup.py -V # trick needed to generate VERSION file 52 | - pyinstaller main.spec 53 | # remove dist-info folders 54 | - ps: Remove-Item dist\pytripgui\*.dist-info -Recurse 55 | # compress pytripgui package 56 | - 7z a pytripgui.zip ./dist/pytripgui 57 | 58 | # installer 59 | # generate single dir installation 60 | # add InnoSetup to PATH 61 | - set PATH="C:\Program Files (x86)\Inno Setup 6";%PATH% 62 | # make windows installer 63 | - iscc win_innosetup.iss 64 | 65 | # make wheel package 66 | - python -m pip install wheel 67 | - pip list 68 | - python setup.py bdist_wheel 69 | # clean build directory 70 | - rd /s /q build 71 | 72 | # upload artifacts 73 | artifacts: 74 | - path: 'dist\*whl' 75 | name: wheel 76 | - path: 'Output\pytrip*exe' 77 | name: installer 78 | - path: 'pytripgui.zip' 79 | name: package 80 | 81 | # push artifacts to github 82 | deploy: 83 | description: 'AppveyorCI build' 84 | provider: GitHub 85 | auth_token: 86 | secure: PK+PK8/w2Emruyqi9bLVDfHGNmbT5Lq+hArBCb9JG1S4V2F9mlTTdMfzTweLlXL3 # github auth token valid until 13.08.2022 87 | artifact: installer 88 | draft: false 89 | prerelease: false 90 | force_update: true 91 | on: 92 | branch: master # release from master branch only 93 | appveyor_repo_tag: true # deploy on tag push only 94 | -------------------------------------------------------------------------------- /appveyor/deploy_package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -x # Print command traces before executing command 4 | 5 | set -e # Exit immediately if a simple command exits with a non-zero status. 6 | 7 | set -o pipefail # Return value of a pipeline as the value of the last command to 8 | # exit with a non-zero status, or zero if all commands in the 9 | # pipeline exit successfully. 10 | 11 | 12 | write_pypirc() { 13 | PYPIRC=~/.pypirc 14 | 15 | if [ -e "${PYPIRC}" ]; then 16 | rm ${PYPIRC} 17 | fi 18 | 19 | touch ${PYPIRC} 20 | cat <${PYPIRC} 21 | [distutils] 22 | index-servers = 23 | pypi 24 | 25 | [pypi] 26 | repository: https://pypi.python.org/pypi 27 | username: ${PYPIUSER} 28 | password: ${PYPIPASS} 29 | 30 | pypirc 31 | 32 | if [ ! -e "${PYPIRC}" ]; then 33 | echo "ERROR: Unable to write file ~/.pypirc" 34 | exit 1 35 | fi 36 | } 37 | 38 | # write .pypirc file with pypi repository credentials 39 | set +x 40 | write_pypirc 41 | set -x 42 | 43 | echo "TAG" $APPVEYOR_REPO_TAG 44 | 45 | ls -al dist 46 | # upload only if tag present 47 | if [[ $APPVEYOR_REPO_TAG == "true" ]]; then 48 | pip install twine 49 | twine upload -r pypi dist/*whl 50 | fi 51 | 52 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/.nojekyll -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_images/add_single_voi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/add_single_voi.png -------------------------------------------------------------------------------- /docs/_images/add_vois.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/add_vois.png -------------------------------------------------------------------------------- /docs/_images/add_vois_added.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/add_vois_added.png -------------------------------------------------------------------------------- /docs/_images/add_vois_visible.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/add_vois_visible.png -------------------------------------------------------------------------------- /docs/_images/beam_kernel_setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/beam_kernel_setup.png -------------------------------------------------------------------------------- /docs/_images/contour_request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/contour_request.png -------------------------------------------------------------------------------- /docs/_images/create_empty_patient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/create_empty_patient.png -------------------------------------------------------------------------------- /docs/_images/creating_field.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/creating_field.png -------------------------------------------------------------------------------- /docs/_images/creating_plan_target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/creating_plan_target.png -------------------------------------------------------------------------------- /docs/_images/executing_plan_end_wo_dvh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/executing_plan_end_wo_dvh.png -------------------------------------------------------------------------------- /docs/_images/executing_plan_start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/executing_plan_start.png -------------------------------------------------------------------------------- /docs/_images/main_window_dose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/main_window_dose.png -------------------------------------------------------------------------------- /docs/_images/main_window_empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/main_window_empty.png -------------------------------------------------------------------------------- /docs/_images/main_window_export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/main_window_export.png -------------------------------------------------------------------------------- /docs/_images/main_window_patient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/main_window_patient.png -------------------------------------------------------------------------------- /docs/_images/main_window_patient_loaded_contours.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/main_window_patient_loaded_contours.png -------------------------------------------------------------------------------- /docs/_images/new_patient_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/new_patient_small.png -------------------------------------------------------------------------------- /docs/_images/plan_setup_dose_delivery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/plan_setup_dose_delivery.png -------------------------------------------------------------------------------- /docs/_images/plan_setup_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/plan_setup_info.png -------------------------------------------------------------------------------- /docs/_images/plan_setup_optimization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/plan_setup_optimization.png -------------------------------------------------------------------------------- /docs/_images/plan_setup_results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/plan_setup_results.png -------------------------------------------------------------------------------- /docs/_images/trip_configuration_local.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/trip_configuration_local.png -------------------------------------------------------------------------------- /docs/_images/trip_configuration_remote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/docs/_images/trip_configuration_remote.png -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ======================================= 2 | Welcome to PyTRiP98GUI's documentation! 3 | ======================================= 4 | 5 | Contents: 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | readme 11 | user_guide 12 | technical 13 | contributing 14 | authors 15 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://www.sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Overview 3 | ======== 4 | 5 | **PyTRiPGUI** is the graphical user interface (GUI) built around the TRiP98 planning system and PyTRiP package. 6 | It is capable of visualising patient CT data, dose and LET overlays. 7 | PyTRiPGUI can make treatment plans using the local or remote TRiP98 package. 8 | 9 | The TRiP98 package is not included here. If you need it, first go to `the TRiP98 webpage `_. 10 | 11 | PyTRiPGUI works under most popular operating systems with necessary packages installed (see installation instructions below). 12 | 13 | Installation guide 14 | ------------------ 15 | 16 | PyTRiPGUI currently works on Linux, Windows and macOS with Python 3.6-3.10. 17 | 18 | Windows 19 | ~~~~~~~ 20 | 21 | On Windows you can install PyTRiPGUI using the console or the installer. We recommend using the latest installer 22 | that is `available here `_. 23 | To install, download the .exe file and run it. The installer doesn't require preinstalled Python. 24 | 25 | Linux and macOS 26 | ~~~~~~~~~~~~~~~ 27 | 28 | We recommend that you run a recent Linux distribution. A recent Ubuntu version or Debian Stable/Testing should work, 29 | or any rolling release (archLinux, openSUSE tumbleweed). In this case, be sure you have **Python** 30 | and **pip** installed. 31 | 32 | As a baseline we recommend running Python version at least 3.6. 33 | To get them and install system-wide on Debian or Ubuntu, type being logged in as a normal user:: 34 | 35 | $ sudo apt-get install python3 python3-pip 36 | 37 | On macOS type:: 38 | 39 | $ brew install python3 python3-pip 40 | 41 | To automatically download and install the PyTRiPGUI system-wide, type:: 42 | 43 | $ pip install pytrip98gui 44 | 45 | NOTE: the pip package is named **pytrip98gui**, while the name of the project is **pytripgui**. 46 | 47 | Start it by calling:: 48 | 49 | $ pytripgui 50 | 51 | More documentation 52 | ------------------ 53 | 54 | If you would like to download and run the source code of PyTRiPGUI, 55 | please see :ref:`developer documentation `. 56 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | furo==2022.6.21 2 | pytrip98gui -------------------------------------------------------------------------------- /docs/technical.rst: -------------------------------------------------------------------------------- 1 | .. _technical: 2 | 3 | ======================= 4 | Developer documentation 5 | ======================= 6 | 7 | Overview 8 | ======== 9 | 10 | .. start-badges 11 | 12 | .. list-table:: 13 | :stub-columns: 1 14 | 15 | * - docs 16 | - |docs| 17 | * - tests 18 | - |appveyor| |ghactions| 19 | * - package 20 | - |version| |downloads| |wheel| |supported-versions| |supported-implementations| 21 | 22 | .. |docs| image:: https://readthedocs.org/projects/pytripgui/badge/?style=flat 23 | :target: https://readthedocs.org/projects/pytripgui 24 | :alt: Documentation Status 25 | 26 | .. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/pytrip/pytripgui?branch=master&svg=true 27 | :alt: Appveyor Build Status 28 | :target: https://ci.appveyor.com/project/pytrip/pytripgui 29 | 30 | .. |ghactions| image:: https://github.com/pytrip/pytripgui/actions/workflows/test.yml/badge.svg 31 | :alt: Github Actions 32 | :target: https://github.com/pytrip/pytripgui/actions/workflows/test.yml 33 | 34 | .. |version| image:: https://img.shields.io/pypi/v/pytrip98gui.svg?style=flat 35 | :alt: PyPI Package latest release 36 | :target: https://pypi.python.org/pypi/pytrip98gui 37 | 38 | .. |downloads| image:: https://img.shields.io/pypi/dm/pytrip98gui.svg?style=flat 39 | :alt: PyPI Package monthly downloads 40 | :target: https://pypi.python.org/pypi/pytrip98gui 41 | 42 | .. |wheel| image:: https://img.shields.io/pypi/wheel/pytrip98gui.svg?style=flat 43 | :alt: PyPI Wheel 44 | :target: https://pypi.python.org/pypi/pytrip98gui 45 | 46 | .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/pytrip98gui.svg?style=flat 47 | :alt: Supported versions 48 | :target: https://pypi.python.org/pypi/pytrip98gui 49 | 50 | .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/pytrip98gui.svg?style=flat 51 | :alt: Supported implementations 52 | :target: https://pypi.python.org/pypi/pytrip98gui 53 | 54 | .. end-badges 55 | 56 | 57 | Installation 58 | ============ 59 | 60 | Stable version :: 61 | 62 | pip install pytrip98gui 63 | 64 | Latest unstable version, directly from GIT repository:: 65 | 66 | pip install git+https://github.com/pytrip/pytripgui.git 67 | 68 | To uninstall, simply use:: 69 | 70 | pip uninstall pytrip98gui 71 | 72 | Running pytripgui 73 | ================= 74 | 75 | To run stable version installed using pip manager, simply type:: 76 | 77 | pytripgui 78 | 79 | To run unstable, development version of pytripgui (when working with source code), type:: 80 | 81 | python -m pytripgui.main 82 | 83 | History 84 | ======= 85 | 86 | * earliest mention of the pytrip project dates back to 2010 http://willworkforscience.blogspot.com/2010/12/happy-new-year.html 87 | 88 | * 2012-2013 pytrip code with an experimental GUI is developed by Niels Bassler and Jakob Toftegaard, code is hosted in SVN repository at Aarhus University (https://svn.nfit.au.dk/trac/pytrip) 89 | 90 | * state of the code in late 2013 can be seen here: https://github.com/pytrip/pytrip/commit/54e2d00d41138431c1c2b69cc6136f87cf4831b8 91 | * pytrip works with python 2.x, GUI is based on wxwidgets library 92 | * pytrip (including experimental GUI) was denoted at v0.1 93 | * functionality of GUI at that moment can be seen in video https://www.youtube.com/embed/6ZqcJ6OZ598 94 | 95 | * 2014 Manuscript published: 96 | 97 | * Toftegaard J, Petersen JB, Bassler N. PyTRiP-a toolbox and GUI for the proton/ion therapy planning system TRiP. In Journal of Physics: Conference Series 2014 Mar 24 (Vol. 489, No. 1, p. 012045). https://doi.org/10.1088/1742-6596/489/1/012045 98 | 99 | * 2014-2016 pytrip code (including GUI) is publicly available as a SourceForge project (https://sourceforge.net/projects/pytrip/) 100 | 101 | * Jakob Toftegaard issued several commits, Toke Printz included some fixes (https://github.com/pytrip/pytrip/commit/d6dedb8b5e309f33e06fb766542345064348e7e0) 102 | 103 | * 28.08.2016 pytrip (including GUI) is migrated to GIT at Github repository (https://github.com/pytrip/pytrip) 104 | 105 | * pytripgui is extracted to a separate project (https://github.com/pytrip/pytripgui) 106 | * Leszek Grzanka joins the developer team 107 | 108 | * 08.05.2018 pytripgui is migrated from wxwidgets to Qt5 framework (https://github.com/pytrip/pytripgui/commit/cb27fc909d132ce5f7a5e0be5df2dbbfd64e6c1d) 109 | 110 | * GUI shifts completely from python 2.x to python 3.x 111 | 112 | * 04.2019 Łukasz Jeleń joins developer team, introducing MVC architecture in the project 113 | 114 | * 06.2021 Arkadiusz Ćwikła, Joanna Fortuna, Michał Krawczyk and Mateusz Łaszczyk join project (part of a bachelor thesis at the AGH University) 115 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | log_cli = true -------------------------------------------------------------------------------- /pytripgui/__init__.py: -------------------------------------------------------------------------------- 1 | # find specification of the pytripgui module 2 | from importlib import util 3 | import os 4 | 5 | # get location of __init__.py file (the one you see now) in the filesystem 6 | 7 | spec = util.find_spec('pytripgui') 8 | init_location = spec.origin 9 | 10 | # VERSION should sit next to __init__.py in the directory structure 11 | version_file = os.path.join(os.path.dirname(init_location), 'VERSION') 12 | 13 | try: 14 | # read first line of the file (removing newline character) 15 | with open(version_file, 'r') as f: 16 | __version__ = f.readline().strip() 17 | except FileNotFoundError: 18 | # backup solution - read the version from git 19 | from pytripgui.version import git_version 20 | __version__ = git_version() 21 | -------------------------------------------------------------------------------- /pytripgui/add_vois_vc/__init__.py: -------------------------------------------------------------------------------- 1 | from pytripgui.add_vois_vc.add_vois_cont import AddVOIsController 2 | from pytripgui.add_vois_vc.add_vois_view import AddVOIsQtView 3 | 4 | # from https://docs.python.org/3/tutorial/modules.html 5 | # if a package's __init__.py code defines a list named __all__, 6 | # it is taken to be the list of module names that should be imported when from package import * is encountered. 7 | __all__ = ['AddVOIsQtView', 'AddVOIsController'] 8 | -------------------------------------------------------------------------------- /pytripgui/add_vois_vc/add_single_voi_vc/__init__.py: -------------------------------------------------------------------------------- 1 | from pytripgui.add_vois_vc.add_single_voi_vc.add_single_voi_cont import AddSingleVOIController 2 | from pytripgui.add_vois_vc.add_single_voi_vc.add_single_voi_view import AddSingleVOIQtView 3 | 4 | # from https://docs.python.org/3/tutorial/modules.html 5 | # if a package's __init__.py code defines a list named __all__, 6 | # it is taken to be the list of module names that should be imported when from package import * is encountered. 7 | __all__ = ['AddSingleVOIQtView', 'AddSingleVOIController'] 8 | -------------------------------------------------------------------------------- /pytripgui/add_vois_vc/add_single_voi_vc/add_single_voi_cont.py: -------------------------------------------------------------------------------- 1 | from pytrip.vdx import create_sphere, create_cube, create_cylinder 2 | 3 | import logging 4 | 5 | from pytripgui.add_vois_vc.voi_widget import SphericalVOIWidget, CuboidalVOIWidget, VOIWidget, CylindricalVOIWidget 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class AddSingleVOIController: 11 | def __init__(self, model, view, used_voi_names): 12 | self.model = model 13 | self.view = view 14 | self.used_voi_names = used_voi_names 15 | self.is_accepted = False 16 | self._setup_callbacks() 17 | 18 | self.voi_types = {"Spherical": SphericalVOIWidget, "Cuboidal": CuboidalVOIWidget, 19 | "Cylindrical": CylindricalVOIWidget} 20 | self._reload_voi() 21 | 22 | def _setup_callbacks(self) -> None: 23 | self.view.accept_buttons.accepted.disconnect() 24 | self.view.accept_buttons.accepted.connect(self._save_and_exit) 25 | 26 | self.view.voi_combobox.emit_on_item_change(self._reload_voi) 27 | 28 | def _save_and_exit(self) -> None: 29 | if self._validate_voi(): 30 | self.is_accepted = True 31 | self.view.accept() 32 | 33 | def _reload_voi(self) -> None: 34 | voi_type = self.view.voi_combobox.current_text 35 | if voi_type in self.voi_types: 36 | if self.view.voi_layout.count(): 37 | self.view.voi_layout.itemAt(0).widget().close() 38 | voi = self.voi_types[voi_type] 39 | self.view.voi_layout.insertWidget(0, voi()) 40 | 41 | def _validate_voi(self) -> bool: 42 | voi_widget = self.view.voi_layout.itemAt(0).widget() 43 | # validate fields 44 | if not voi_widget.validate(): 45 | return False 46 | 47 | # check if name isn't already being used 48 | if voi_widget.name.lower() in self.used_voi_names: 49 | self.view.info.text = "This name is already being used by another VOI." 50 | return False 51 | 52 | ctx = self.model 53 | center_no_offsets = [a - b for (a, b) in zip(voi_widget.center, [ctx.xoffset, ctx.yoffset, ctx.zoffset])] 54 | if isinstance(voi_widget, SphericalVOIWidget): 55 | voi = create_sphere( 56 | cube=ctx, 57 | name=voi_widget.name, 58 | center=center_no_offsets, 59 | radius=voi_widget.radius 60 | ) 61 | elif isinstance(voi_widget, CuboidalVOIWidget): 62 | voi = create_cube( 63 | cube=ctx, 64 | name=voi_widget.name, 65 | center=center_no_offsets, 66 | width=voi_widget.width, 67 | height=voi_widget.height, 68 | depth=voi_widget.depth, 69 | ) 70 | elif isinstance(voi_widget, CylindricalVOIWidget): 71 | voi = create_cylinder( 72 | cube=ctx, 73 | name=voi_widget.name, 74 | center=center_no_offsets, 75 | radius=voi_widget.radius, 76 | depth=voi_widget.depth, 77 | ) 78 | else: 79 | logger.debug("VOI widget unrecognised") 80 | return False 81 | 82 | # validate containment in ctx 83 | if not voi.is_fully_contained(): 84 | self.view.info.text = "VOI isn't fully contained in the given patient." 85 | return False 86 | return True 87 | 88 | def get_voi_widget(self) -> VOIWidget: 89 | return self.view.voi_layout.itemAt(0).widget() 90 | -------------------------------------------------------------------------------- /pytripgui/add_vois_vc/add_single_voi_vc/add_single_voi_view.py: -------------------------------------------------------------------------------- 1 | from pytripgui.view.qt_view_adapter import ComboBox, Label 2 | from pytripgui.view.qt_gui import AddVOIDialog 3 | 4 | import logging 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class AddSingleVOIQtView: 10 | """ 11 | """ 12 | def __init__(self, parent=None): 13 | self._ui = AddVOIDialog(parent) 14 | self.voi_combobox = ComboBox(self._ui.VOI_comboBox) 15 | self.voi_layout = self._ui.VOI_layout 16 | 17 | self.info = Label(self._ui.info_label) 18 | 19 | self.accept = self._ui.accept 20 | self.accept_buttons = self._ui.accept_buttonBox 21 | 22 | def show(self) -> None: 23 | self._ui.show() 24 | # try to not cover vital info of parent dialog (patient limits) 25 | self._ui.move(self._ui.pos().x(), 1.3 * self._ui.pos().y()) 26 | self._ui.exec_() 27 | 28 | def exit(self) -> None: 29 | self._ui.close() 30 | -------------------------------------------------------------------------------- /pytripgui/add_vois_vc/add_vois_view.py: -------------------------------------------------------------------------------- 1 | from pytripgui.view.qt_view_adapter import PushButton, Label 2 | from pytripgui.view.qt_gui import AddVOIsDialog 3 | 4 | import logging 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class AddVOIsQtView: 10 | """ 11 | """ 12 | def __init__(self, parent=None): 13 | self.ui = AddVOIsDialog(parent) 14 | self.name = Label(self.ui.name_label) 15 | self.width = Label(self.ui.width_label) 16 | self.height = Label(self.ui.height_label) 17 | self.depth = Label(self.ui.depth_label) 18 | self.pixel_size = Label(self.ui.pixelSize_label) 19 | self.pixel_number_x = Label(self.ui.pixelNumberX_label) 20 | self.pixel_number_y = Label(self.ui.pixelNumberY_label) 21 | self.slice_number = Label(self.ui.sliceNumber_label) 22 | self.slice_distance = Label(self.ui.sliceDistance_label) 23 | self.x_offset = Label(self.ui.xOffset_label) 24 | self.y_offset = Label(self.ui.yOffset_label) 25 | self.slice_offset = Label(self.ui.sliceOffset_label) 26 | 27 | self.x_min = Label(self.ui.xMin_label) 28 | self.x_max = Label(self.ui.xMax_label) 29 | self.y_min = Label(self.ui.yMin_label) 30 | self.y_max = Label(self.ui.yMax_label) 31 | self.z_min = Label(self.ui.zMin_label) 32 | self.z_max = Label(self.ui.zMax_label) 33 | 34 | self.voi_scroll_area = self.ui.voi_scrollArea 35 | 36 | self.add_voi_button = PushButton(self.ui.addVOI_pushButton) 37 | 38 | self.accept = self.ui.accept 39 | self.accept_buttons = self.ui.accept_buttonBox 40 | 41 | def show(self) -> None: 42 | self.ui.show() 43 | self.ui.exec_() 44 | 45 | def exit(self) -> None: 46 | self.ui.close() 47 | -------------------------------------------------------------------------------- /pytripgui/add_vois_vc/voi_widget.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtWidgets, uic 2 | from PyQt5.QtGui import QRegularExpressionValidator 3 | from pathlib import Path 4 | 5 | from pytripgui.utils.regex import Regex 6 | from pytripgui.view.qt_view_adapter import LineEdit 7 | 8 | 9 | class VOIWidget(QtWidgets.QFrame): 10 | def __init__(self, widget_file): 11 | super().__init__() 12 | widget_path = Path(Path(__file__).parent, "widgets", widget_file).resolve() 13 | uic.loadUi(widget_path, self) 14 | 15 | self._name = LineEdit(self.name_lineEdit) 16 | self._center = [ 17 | LineEdit(self.centerX_lineEdit), 18 | LineEdit(self.centerY_lineEdit), 19 | LineEdit(self.centerZ_lineEdit) 20 | ] 21 | 22 | validator = QRegularExpressionValidator(Regex.STRING.value) 23 | self._name.enable_validation(validator) 24 | 25 | validator = QRegularExpressionValidator(Regex.FLOAT.value) 26 | enable_validation_list(validator, self._center) 27 | 28 | @property 29 | def name(self): 30 | return self._name.text 31 | 32 | @property 33 | def center(self): 34 | return [float(field.text) for field in self._center] 35 | 36 | def validate(self) -> bool: 37 | return self._name.validate() and validate_list(self._center) 38 | 39 | def disable_fields(self) -> None: 40 | self._name.set_enabled(False) 41 | for field in self._center: 42 | field.set_enabled(False) 43 | 44 | 45 | class SphericalVOIWidget(VOIWidget): 46 | def __init__(self): 47 | super().__init__("spherical_voi.ui") 48 | 49 | self._radius = LineEdit(self.radius_lineEdit) 50 | 51 | validator = QRegularExpressionValidator(Regex.FLOAT_UNSIGNED.value) 52 | self._radius.enable_validation(validator) 53 | 54 | @property 55 | def radius(self): 56 | return float(self._radius.text) 57 | 58 | def validate(self) -> bool: 59 | return super().validate() and self._radius.validate() 60 | 61 | def disable_fields(self) -> None: 62 | super().disable_fields() 63 | self._radius.set_enabled(False) 64 | 65 | 66 | class CuboidalVOIWidget(VOIWidget): 67 | def __init__(self): 68 | super().__init__("cuboidal_voi.ui") 69 | 70 | self._width = LineEdit(self.width_lineEdit) 71 | self._height = LineEdit(self.height_lineEdit) 72 | self._depth = LineEdit(self.depth_lineEdit) 73 | self._dims = [self._width, self._height, self._depth] 74 | 75 | validator = QRegularExpressionValidator(Regex.FLOAT_UNSIGNED.value) 76 | enable_validation_list(validator, self._dims) 77 | 78 | @property 79 | def width(self) -> float: 80 | return float(self._width.text) 81 | 82 | @property 83 | def height(self) -> float: 84 | return float(self._height.text) 85 | 86 | @property 87 | def depth(self) -> float: 88 | return float(self._depth.text) 89 | 90 | @property 91 | def dims(self) -> list: 92 | return [float(i.text) for i in self._dims] 93 | 94 | def validate(self) -> bool: 95 | return super().validate() and validate_list(self._dims) 96 | 97 | def disable_fields(self) -> None: 98 | super().disable_fields() 99 | for field in self._dims: 100 | field.set_enabled(False) 101 | 102 | 103 | class CylindricalVOIWidget(VOIWidget): 104 | def __init__(self): 105 | super().__init__("cylindrical_voi.ui") 106 | 107 | self._radius = LineEdit(self.radius_lineEdit) 108 | self._depth = LineEdit(self.depth_lineEdit) 109 | 110 | validator = QRegularExpressionValidator(Regex.FLOAT_UNSIGNED.value) 111 | self._radius.enable_validation(validator) 112 | self._depth.enable_validation(validator) 113 | 114 | @property 115 | def radius(self): 116 | return float(self._radius.text) 117 | 118 | @property 119 | def depth(self): 120 | return float(self._depth.text) 121 | 122 | def validate(self) -> bool: 123 | return super().validate() and self._radius.validate() and self._depth.validate() 124 | 125 | def disable_fields(self) -> None: 126 | super().disable_fields() 127 | self._radius.set_enabled(False) 128 | self._depth.set_enabled(False) 129 | 130 | 131 | def enable_validation_list(validator: QRegularExpressionValidator, items: list) -> None: 132 | for item in items: 133 | item.enable_validation(validator) 134 | 135 | 136 | def validate_list(items: list) -> bool: 137 | result = True 138 | for item in items: 139 | if not item.validate(): 140 | result = False 141 | return result 142 | -------------------------------------------------------------------------------- /pytripgui/add_vois_vc/widgets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/pytripgui/add_vois_vc/widgets/__init__.py -------------------------------------------------------------------------------- /pytripgui/add_vois_vc/widgets/list_element_voi.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | VOI 4 | 5 | 6 | 7 | 0 8 | 0 9 | 660 10 | 250 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | 21 | 0 22 | 220 23 | 24 | 25 | 26 | 27 | 16777215 28 | 300 29 | 30 | 31 | 32 | Frame 33 | 34 | 35 | 36 | 37 | 38 | QFrame::StyledPanel 39 | 40 | 41 | QFrame::Raised 42 | 43 | 44 | 10 45 | 46 | 47 | 10 48 | 49 | 50 | 51 | 5 52 | 53 | 54 | 5 55 | 56 | 57 | 5 58 | 59 | 60 | 5 61 | 62 | 63 | 5 64 | 65 | 66 | 67 | 68 | 10 69 | 70 | 71 | 5 72 | 73 | 74 | 5 75 | 76 | 77 | 5 78 | 79 | 80 | 5 81 | 82 | 83 | 84 | 85 | 86 | 87 | Remove 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /pytripgui/app_logic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/pytripgui/app_logic/__init__.py -------------------------------------------------------------------------------- /pytripgui/app_logic/charts.py: -------------------------------------------------------------------------------- 1 | from pytripgui.view.chart_widget import ChartWidget 2 | 3 | from PyQt5.QtWidgets import QDockWidget 4 | from PyQt5.QtCore import Qt 5 | 6 | 7 | class Charts: 8 | def __init__(self, parent_gui): 9 | self._parent_gui = parent_gui 10 | 11 | self._charts = [] 12 | self._dock_widgets = [] 13 | 14 | def set_simulation_result(self, simulation_result): 15 | self._charts = [] 16 | 17 | for histogram_name in simulation_result.volume_histograms: 18 | widget = ChartWidget() 19 | widget.title = histogram_name 20 | 21 | histogram = simulation_result.volume_histograms[histogram_name] 22 | for name, value in histogram.items(): 23 | widget.add_series(value.x, value.y, name) 24 | 25 | self._charts.append(widget) 26 | 27 | self._show() 28 | 29 | def _show(self): 30 | self._close_all() 31 | self._dock_widgets = [] 32 | 33 | for chart in self._charts: 34 | widget = QDockWidget() 35 | widget.setWidget(chart.view) 36 | self._parent_gui.ui.addDockWidget(Qt.RightDockWidgetArea, widget) 37 | self._dock_widgets.append(widget) 38 | 39 | def _close_all(self): 40 | for _dock_widget in self._dock_widgets: 41 | _dock_widget.close() 42 | -------------------------------------------------------------------------------- /pytripgui/app_logic/gui_executor.py: -------------------------------------------------------------------------------- 1 | from pytripgui.executor_vc.executor_view import ExecutorQtView 2 | 3 | from pytripgui.plan_executor.threaded_executor import ThreadedExecutor 4 | from pytripgui.tree_vc.tree_items import SimulationResultItem 5 | from pytripgui.messages import InfoMessages 6 | 7 | from PyQt5.QtCore import QTimer 8 | 9 | 10 | class GuiExecutor: 11 | GUI_UPDATE_RATE_MS = 10 # GUI update rate during trip98 execution [ms] 12 | 13 | def __init__(self, trip_config, patient, plan, result_callback, parent_view): 14 | if not plan.data.fields: 15 | parent_view.show_info(*InfoMessages["addOneField"]) 16 | return 17 | 18 | self.result_callback = result_callback 19 | self.patient = patient 20 | 21 | self.done = False # True when object can be removed from memory 22 | 23 | self._gui_update_timer = QTimer() 24 | self._thread = ThreadedExecutor(plan, patient, trip_config) 25 | 26 | self._ui = ExecutorQtView(parent_view) 27 | 28 | def show(self): 29 | self._ui.show() 30 | 31 | def update_gui(self): 32 | if self._thread.is_alive(): 33 | # if thread is still alive, execute this function in GUI_UPDATE_RATE_MS 34 | self._gui_update_timer.singleShot(GuiExecutor.GUI_UPDATE_RATE_MS, self.update_gui) 35 | else: 36 | self._call_result_callback() 37 | self._ui.enable_ok_button() 38 | self.done = True 39 | 40 | while not self._thread.logger.empty(): 41 | text = self._thread.logger.get() 42 | self._ui.append_log(text) 43 | 44 | def start(self): 45 | self._thread.start() 46 | # update gui in GUI_UPDATE_RATE_MS 47 | self._gui_update_timer.singleShot(GuiExecutor.GUI_UPDATE_RATE_MS, self.update_gui) 48 | 49 | def _call_result_callback(self): 50 | if self._thread.item_queue.empty(): 51 | return 52 | 53 | item = SimulationResultItem() 54 | item.data = self._thread.item_queue.get(False) 55 | 56 | for dose in item.data.get_doses(): 57 | dose_item = SimulationResultItem() 58 | dose_item.data = dose 59 | item.add_child(dose_item) 60 | 61 | for let in item.data.get_lets(): 62 | let_item = SimulationResultItem() 63 | let_item.data = let 64 | item.add_child(let_item) 65 | 66 | self.result_callback(item, self.patient) 67 | -------------------------------------------------------------------------------- /pytripgui/app_logic/patient_tree.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pytripgui.tree_vc.tree_model import TreeModel 4 | from pytripgui.tree_vc.tree_items import PatientList 5 | 6 | from pytripgui.tree_vc.tree_controller import TreeController 7 | from pytripgui.tree_vc.tree_view import TreeView 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class PatientTree: 13 | def __init__(self, parent): 14 | self.patient_tree_model = TreeModel(PatientList()) 15 | self.patient_tree_view = TreeView() 16 | 17 | self.patient_tree_view.setModel(self.patient_tree_model) 18 | self.patient_tree_cont = TreeController(self.patient_tree_model, self.patient_tree_view) 19 | 20 | self._parent = parent 21 | 22 | self.widget = self._parent.patientTree_dockWidget 23 | self.widget.setWidget(self.patient_tree_view) 24 | 25 | def set_visible(self, visible): 26 | if visible: 27 | self.widget.show() 28 | else: 29 | self.widget.hide() 30 | 31 | def app_callback(self, app_callback): 32 | self.patient_tree_cont.new_item_callback = app_callback.new_item_callback 33 | self.patient_tree_cont.edit_item_callback = app_callback.edit_item_callback 34 | 35 | self.patient_tree_cont.open_voxelplan_callback = app_callback.on_open_voxelplan 36 | self.patient_tree_cont.open_dicom_callback = app_callback.on_open_dicom 37 | 38 | self.patient_tree_cont.export_patient_voxelplan_callback = app_callback.export_patient_voxelplan_callback 39 | self.patient_tree_cont.export_patient_dicom_callback = app_callback.export_patient_dicom_callback 40 | self.patient_tree_cont.export_dose_voxelplan_callback = app_callback.export_dose_voxelplan_callback 41 | self.patient_tree_cont.export_dose_dicom_callback = app_callback.export_dose_dicom_callback 42 | self.patient_tree_cont.export_plan_callback = app_callback.export_plan_exec_callback 43 | 44 | self.patient_tree_cont.import_dose_voxelplan_callback = app_callback.import_dose_voxelplan_callback 45 | self.patient_tree_cont.import_dose_dicom_callback = app_callback.import_dose_dicom_callback 46 | 47 | self.patient_tree_cont.execute_plan_callback = app_callback.on_execute_selected_plan 48 | self.patient_tree_cont.one_click_callback = app_callback.one_click_callback 49 | 50 | def add_new_item(self, parent_item, item): 51 | self.patient_tree_cont.add_new_item(parent_item, item) 52 | 53 | def selected_item_patient(self): 54 | return self.patient_tree_view.selected_item_patient 55 | 56 | def selected_item(self): 57 | return self.patient_tree_view.selected_item 58 | -------------------------------------------------------------------------------- /pytripgui/app_logic/viewcanvas.py: -------------------------------------------------------------------------------- 1 | from pytripgui.canvas_vc.canvas_view import CanvasView 2 | from pytripgui.canvas_vc.canvas_controller import CanvasController 3 | 4 | 5 | class ViewCanvases: 6 | def __init__(self, parent): 7 | self.viewcanvas_view = CanvasView(parent) 8 | self.plot_cont = CanvasController(None, self.viewcanvas_view) 9 | 10 | def widget(self): 11 | return self.viewcanvas_view.widget() 12 | 13 | def set_patient(self, patient, state=None): 14 | self.plot_cont.set_model_data_and_update_view(patient, state) 15 | 16 | def set_simulation_results(self, simulation_results, simulation_item, state=None): 17 | self.plot_cont.set_simulation_results(simulation_results, simulation_item, state) 18 | 19 | def update_voi_list(self, patient, state=None): 20 | self.plot_cont.update_voi_list(patient, state) 21 | 22 | def get_gui_state(self): 23 | return self.plot_cont.get_gui_state() 24 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/pytripgui/canvas_vc/__init__.py -------------------------------------------------------------------------------- /pytripgui/canvas_vc/gui_state.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | 3 | from pytripgui.canvas_vc.projection_selector import ProjectionSelector 4 | 5 | 6 | class PatientGuiState: 7 | """ 8 | This class holds information about viewing patient 9 | - on which slice user stopped scrolling on each plane 10 | - which VOIs user picked to be shown 11 | etc. 12 | """ 13 | def __init__(self): 14 | self.projection_selector: Optional[ProjectionSelector] = None 15 | self.ticked_voi_list: List = [] 16 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/objects/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/pytripgui/canvas_vc/objects/__init__.py -------------------------------------------------------------------------------- /pytripgui/canvas_vc/objects/ctx.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pytrip import Cube 4 | 5 | from pytripgui.canvas_vc.objects.data_base import PlotDataBase 6 | from pytripgui.canvas_vc.projection_selector import ProjectionSelector 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class Ctx(PlotDataBase): 12 | def __init__(self, cube: Cube, selector: ProjectionSelector): 13 | super().__init__(cube, selector) 14 | 15 | self.contrast_ct = [-500, 2000] 16 | 17 | def prepare_data_to_plot(self): 18 | if self.cube is None: 19 | return 20 | 21 | self._set_aspect() 22 | self.data_to_plot = self.projection_selector.get_projection(self.cube) 23 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/objects/data_base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Optional 3 | 4 | from pytrip import Cube 5 | 6 | from pytripgui.canvas_vc.projection_selector import ProjectionSelector 7 | 8 | 9 | class PlotDataBase(ABC): 10 | def __init__(self, cube: Cube, selector: ProjectionSelector): 11 | self.aspect = 1.0 # aspect ratio of plot 12 | 13 | self.cube: Cube = cube # placeholder for cube object from pytrip 14 | self.data_to_plot = None # placeholder for extracted and prepared data to plot 15 | 16 | self.projection_selector: ProjectionSelector = selector 17 | 18 | @abstractmethod 19 | def prepare_data_to_plot(self): 20 | pass 21 | 22 | def _set_aspect(self): 23 | """ 24 | Set the aspect of the axis scaling, i.e. the ratio of y-unit to x-unit. 25 | """ 26 | # here we are calculating aspect reverse, because of the way we are accessing data in projection selector 27 | # I mean, I guess... something is wrong 28 | plane = self.projection_selector.plane 29 | c = self.cube 30 | vertical_size = 1.0 # some default value 31 | horizontal_size = 1.0 # some default value 32 | # "Transversal" (xy) 33 | if plane == "Transversal": 34 | vertical_size = c.dimx * c.pixel_size 35 | horizontal_size = c.dimy * c.pixel_size 36 | # "Sagittal" (yz) 37 | elif plane == "Sagittal": 38 | vertical_size = c.dimy * c.pixel_size 39 | horizontal_size = c.dimz * c.slice_distance 40 | # "Coronal" (xz) 41 | elif plane == "Coronal": 42 | vertical_size = c.dimx * c.pixel_size 43 | horizontal_size = c.dimz * c.slice_distance 44 | 45 | self.aspect = vertical_size / horizontal_size 46 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/objects/dos.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | import numpy as np 4 | 5 | import logging 6 | 7 | from pytrip import Cube 8 | 9 | from pytripgui.canvas_vc.objects.data_base import PlotDataBase 10 | from pytripgui.canvas_vc.projection_selector import ProjectionSelector 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class DoseAxisType(Enum): 16 | """ 17 | Different type of dose scaling 18 | """ 19 | auto = 0 20 | rel = 1 21 | abs = 2 22 | 23 | 24 | class Dos(PlotDataBase): 25 | def __init__(self, cube: Cube, selector: ProjectionSelector): 26 | super().__init__(cube, selector) 27 | 28 | self.dose_axis = DoseAxisType.auto 29 | 30 | self.dos_scale = None 31 | self.min_dose = 0 32 | self.max_dose = None 33 | self.factor = 1.0 34 | self._max_dose_from_cube = None 35 | 36 | def prepare_data_to_plot(self): 37 | if not self.cube: 38 | return 39 | 40 | if self._max_dose_from_cube is None: 41 | self._max_dose_from_cube = np.amax(self.cube.cube) 42 | 43 | self.dos_scale = self._get_proposed_scale() 44 | self._set_aspect() 45 | 46 | if self.dos_scale == DoseAxisType.abs: 47 | self.factor = 1000.0 / self.cube.target_dose 48 | elif self.dos_scale == DoseAxisType.rel: 49 | self.factor = 10.0 50 | 51 | self.max_dose = self._max_dose_from_cube / self.factor 52 | 53 | dos_data = self.projection_selector.get_projection(self.cube) 54 | 55 | self.data_to_plot = dos_data / self.factor 56 | self.data_to_plot[self.data_to_plot <= self.min_dose] = self.min_dose 57 | 58 | def _get_proposed_scale(self): 59 | if self.cube.target_dose <= 0: 60 | return DoseAxisType.rel 61 | if self.dose_axis == DoseAxisType.auto and self.cube.target_dose != 0.0: 62 | return DoseAxisType.abs 63 | return self.dose_axis 64 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/objects/let.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import numpy as np 4 | from pytrip import Cube 5 | 6 | from pytripgui.canvas_vc.objects.data_base import PlotDataBase 7 | from pytripgui.canvas_vc.projection_selector import ProjectionSelector 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class Let(PlotDataBase): 13 | def __init__(self, cube: Cube, selector: ProjectionSelector): 14 | super().__init__(cube, selector) 15 | 16 | self.min_let = 0 17 | self.max_let = None 18 | 19 | def prepare_data_to_plot(self): 20 | if self.cube is None: 21 | return 22 | 23 | if self.max_let is None: 24 | self.max_let = np.amax(self.cube.cube) 25 | 26 | self._set_aspect() 27 | self.data_to_plot = self.projection_selector.get_projection(self.cube) 28 | self.data_to_plot[self.data_to_plot <= self.min_let] = self.min_let 29 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/objects/vc_text.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | 5 | 6 | class ViewCanvasTextCont: 7 | """ 8 | This class holds logic for plotting all various text decorators for the ViewCanvas plot. 9 | # TODO: find better name than "ViewCanvas" for this object. 10 | """ 11 | def __init__(self, projection_selector): 12 | self.projection_selector = projection_selector 13 | self.zoom = 100.0 14 | self.center = [50.0, 50.0] 15 | 16 | def offset(self, plc): 17 | """ 18 | """ 19 | 20 | bbox = plc.axes.get_window_extent().transformed(plc.figure.dpi_scale_trans.inverted()) 21 | width, height = bbox.width * plc.figure.dpi, bbox.height * plc.figure.dpi 22 | size = [width, height] 23 | 24 | width = (float(size[0]) / self.zoom) * 100.0 25 | height = (float(size[1]) / self.zoom) * 100.0 26 | center = [float(size[0]) * self.center[0] / 100, float(size[1]) * self.center[1] / 100] 27 | offset = [center[0] - width / 2, center[1] - height / 2] 28 | return offset 29 | 30 | def plot(self, plc): 31 | """ 32 | Plot the text overlays 33 | :params idx: index of slice to be plotted. 34 | """ 35 | 36 | # pm = plc._model 37 | # 38 | # size = pm.slice_size 39 | # offset = self.offset(plc) 40 | # idx = pm.slice_pos_idx # current slice index (starts at 0, takes plane of view into account) 41 | 42 | axes = plc.axes 43 | 44 | bbox = plc.axes.get_window_extent().transformed(plc.figure.dpi_scale_trans.inverted()) 45 | width, height = bbox.width * plc.figure.dpi, bbox.height * plc.figure.dpi 46 | # size = [width, height] 47 | # width = size[0] 48 | # height = size[1] 49 | # 50 | # _slices = pm.slice_size[2] 51 | # _slice_pos = pm.slice_pos_mm 52 | 53 | if self.projection_selector.plane == "Transversal": 54 | markers = ['R', 'L', 'A', 'P'] 55 | elif self.projection_selector.plane == "Sagittal": 56 | markers = ['D', 'V', 'A', 'P'] 57 | elif self.projection_selector.plane == "Coronal": 58 | markers = ['R', 'L', 'A', 'P'] 59 | 60 | # relative position of orientation markers 61 | rel_pos = ((0.03, 0.5), (0.95, 0.5), (0.5, 0.02), (0.5, 0.95)) 62 | 63 | for i, marker in enumerate(markers): 64 | axes.text(rel_pos[i][0] * width, 65 | rel_pos[i][1] * height, 66 | marker, 67 | color=plc.text_color, 68 | va="top", 69 | fontsize=20) 70 | 71 | # # text label on current slice# and position in mm 72 | # axes.text(#offset[0], 73 | # #offset[1] + 3.0 / self.zoom * 100, 74 | # "Slice #: {:d}/{:d}\n".format(idx + 1, _slices) + 75 | # "Slice Position: {:.1f} mm ".format(_slice_pos), 76 | # color=pm.text_color, 77 | # va="top", 78 | # fontsize=8) 79 | # 80 | # # text label on HU higher and lower level 81 | # # TODO: what does "W / L" mean? 82 | # axes.text(offset[0] + width / self.zoom * 100, 83 | # offset[1] + 3.0 / self.zoom * 100, 84 | # "W / L: %d / %d" % (pm.contrast_ct[1], pm.contrast_ct[0]), 85 | # ha="right", 86 | # color=pm.text_color, 87 | # va="top", 88 | # fontsize=8) 89 | # 90 | # # current plane of view 91 | # axes.text(offset[0], 92 | # offset[1] + (height - 5) / self.zoom * 100, 93 | # pm.plane, 94 | # color=pm.text_color, 95 | # va="bottom", 96 | # fontsize=8) 97 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/objects/vdx.py: -------------------------------------------------------------------------------- 1 | class Vdx: 2 | """ 3 | This class holds data model for plotting Vdx stuff. 4 | """ 5 | def __init__(self, ctx, projection_selector): 6 | self.ctx = ctx 7 | self.projection_selector = projection_selector 8 | self.voi_list = [] 9 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/plot_model.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Optional 3 | 4 | from numpy import unravel_index 5 | 6 | from pytripgui.canvas_vc.objects.ctx import Ctx 7 | from pytripgui.canvas_vc.objects.dos import Dos 8 | from pytripgui.canvas_vc.objects.let import Let 9 | from pytripgui.canvas_vc.objects.vdx import Vdx 10 | from pytripgui.canvas_vc.projection_selector import ProjectionSelector 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class PlotModel: 16 | def __init__(self, projection_selector=ProjectionSelector()): 17 | 18 | self.vdx: Optional[Vdx] = None # cube is also in the main_model, but here this is specific for plotting. 19 | 20 | self.projection_selector = projection_selector 21 | self.display_filter = "" 22 | self.dose: Optional[Dos] = None 23 | self.let: Optional[Let] = None 24 | self.ctx: Optional[Ctx] = None 25 | 26 | # These are used by getters and setters, must come after the other values are initialized. 27 | self.cube = None 28 | self.slice_pos_mm: float = 0.0 29 | 30 | def set_ctx(self, ctx): 31 | self.ctx = Ctx(ctx, self.projection_selector) 32 | if not self.projection_selector.is_loaded(): 33 | self.projection_selector.load_slices_count(ctx) 34 | 35 | def set_let(self, let): 36 | self.let = Let(let, self.projection_selector) 37 | if not self.projection_selector.is_loaded(): 38 | self.projection_selector.load_slices_count(let) 39 | 40 | def set_dose(self, dose): 41 | self.dose = Dos(dose, self.projection_selector) 42 | if not self.projection_selector.is_loaded(): 43 | self.projection_selector.load_slices_count(dose) 44 | 45 | max_item_index = unravel_index(dose.cube.argmax(), dose.cube.shape) 46 | self.projection_selector._transversal_slice_no = max_item_index[0] 47 | self.projection_selector._sagittal_slice_no = max_item_index[2] 48 | self.projection_selector._coronal_slice_no = max_item_index[1] 49 | 50 | def set_vdx(self): 51 | self.vdx = Vdx(self.ctx.cube, self.projection_selector) 52 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/plotter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/pytripgui/canvas_vc/plotter/__init__.py -------------------------------------------------------------------------------- /pytripgui/canvas_vc/plotter/bars/__init__.py: -------------------------------------------------------------------------------- 1 | from pytripgui.canvas_vc.plotter.bars.ctx_bar import CtxBar 2 | from pytripgui.canvas_vc.plotter.bars.dos_bar import DosBar 3 | from pytripgui.canvas_vc.plotter.bars.let_bar import LetBar 4 | from pytripgui.canvas_vc.plotter.bars.projection_enum import BarProjection 5 | 6 | # from https://docs.python.org/3/tutorial/modules.html 7 | # if a package's __init__.py code defines a list named __all__, 8 | # it is taken to be the list of module names that should be imported when from package import * is encountered. 9 | __all__ = ['CtxBar', 'DosBar', 'LetBar', 'BarProjection'] 10 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/plotter/bars/bar_base.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import ABC 3 | 4 | from matplotlib import pyplot as plt 5 | from matplotlib.axes import Axes 6 | from matplotlib.pyplot import colorbar 7 | 8 | from pytripgui.canvas_vc.plotter.bars.projection_enum import BarProjection 9 | """ 10 | This class and its subclasses were made to remove extra responsibilities from mpl_plotter. 11 | This class holds basic logic and parameters that are shared between all of its subclasses. 12 | If it is needed, subclasses can change those parameters - like dos_bar does. 13 | """ 14 | 15 | # set matplotlib logging level to ERROR, in order not to pollute our log space 16 | logging.getLogger('matplotlib').setLevel(logging.ERROR) 17 | 18 | 19 | class BarBase(ABC, Axes): 20 | """ 21 | Abstract base class that holds information how bars should be made 22 | """ 23 | # projection registration in matplotlib requires class to have parameter called "name" 24 | name: str = BarProjection.DEFAULT.value 25 | 26 | def __init__(self, fig, rect, **kwargs): 27 | super().__init__(fig, rect, **kwargs) 28 | self.text_color = "#33DD33" # text decorator colour 29 | self.fg_color = 'white' # colour for colourbar ticks and labels 30 | self.bg_color = 'black' # background colour, i.e. between colourbar and CTX/DOS/LET plot 31 | self.cb_fontsize = 8 # fontsize of colourbar labels 32 | self.label = 'DEFAULT_LABEL' 33 | self.bar = None 34 | self.set_aspect(30) 35 | 36 | def plot_bar(self, data, **kwargs): 37 | """ 38 | Plots bar based on passed data 39 | """ 40 | cb = colorbar(data, cax=self) 41 | cb.set_label(self.label, color=self.fg_color, fontsize=self.cb_fontsize) 42 | cb.outline.set_edgecolor(self.bg_color) 43 | cb.ax.yaxis.set_tick_params(color=self.fg_color) 44 | cb.ax.yaxis.set_label_position('left') 45 | plt.setp(plt.getp(cb.ax.axes, 'yticklabels'), color=self.fg_color) 46 | cb.ax.yaxis.set_tick_params(color=self.fg_color, labelsize=self.cb_fontsize) 47 | self.bar = cb 48 | 49 | def clear_bar(self): 50 | """ 51 | Clears whole axes, removes bar. 52 | """ 53 | self.cla() 54 | self.remove() 55 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/plotter/bars/ctx_bar.py: -------------------------------------------------------------------------------- 1 | from matplotlib.projections import register_projection 2 | 3 | from pytripgui.canvas_vc.plotter.bars.bar_base import BarBase 4 | from pytripgui.canvas_vc.plotter.bars.projection_enum import BarProjection 5 | 6 | 7 | class CtxBar(BarBase): 8 | name: str = BarProjection.CTX.value 9 | 10 | def __init__(self, fig, rect, **kwargs): 11 | super().__init__(fig, rect, **kwargs) 12 | self.label = "HU" 13 | 14 | 15 | register_projection(CtxBar) 16 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/plotter/bars/dos_bar.py: -------------------------------------------------------------------------------- 1 | from matplotlib.projections import register_projection 2 | 3 | from pytripgui.canvas_vc.plotter.bars.bar_base import BarBase 4 | from pytripgui.canvas_vc.plotter.bars.projection_enum import BarProjection 5 | 6 | 7 | class DosBar(BarBase): 8 | name: str = BarProjection.DOS.value 9 | 10 | def __init__(self, fig, rect, **kwargs): 11 | super().__init__(fig, rect, **kwargs) 12 | self.label = "Dose" 13 | 14 | def plot_bar(self, data, **kwargs): 15 | super().plot_bar(data) 16 | if kwargs['scale'] == "abs": 17 | self.bar.set_label("Dose [Gy]") 18 | else: 19 | self.bar.set_label("Dose [%]") 20 | 21 | 22 | register_projection(DosBar) 23 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/plotter/bars/let_bar.py: -------------------------------------------------------------------------------- 1 | from matplotlib.projections import register_projection 2 | 3 | from pytripgui.canvas_vc.plotter.bars.bar_base import BarBase 4 | from pytripgui.canvas_vc.plotter.bars.projection_enum import BarProjection 5 | 6 | 7 | class LetBar(BarBase): 8 | name: str = BarProjection.LET.value 9 | 10 | def __init__(self, fig, rect, **kwargs): 11 | super().__init__(fig, rect, **kwargs) 12 | self.label = "LET (keV/um)" 13 | 14 | 15 | register_projection(LetBar) 16 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/plotter/bars/projection_enum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class BarProjection(Enum): 5 | """ 6 | Enum class that holds all the strings that are used to register all bar classes in matplotlib 7 | """ 8 | DEFAULT = 'BAR_PROJECTION_DEFAULT_NAME' 9 | CTX = 'ctx_bar' 10 | DOS = 'dos_bar' 11 | LET = 'let_bar' 12 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/plotter/images/__init__.py: -------------------------------------------------------------------------------- 1 | from pytripgui.canvas_vc.plotter.images.ctx_image import CtxImage 2 | from pytripgui.canvas_vc.plotter.images.dose_image import DoseImage 3 | from pytripgui.canvas_vc.plotter.images.let_image import LetImage 4 | 5 | __all__ = ['CtxImage', 'DoseImage', 'LetImage'] 6 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/plotter/images/ctx_image.py: -------------------------------------------------------------------------------- 1 | from matplotlib import pyplot as plt 2 | 3 | from pytripgui.canvas_vc.plotter.images.patient_image_base import PatientImageBase 4 | from pytripgui.canvas_vc.objects.ctx import Ctx 5 | 6 | 7 | class CtxImage(PatientImageBase): 8 | def __init__(self, axes): 9 | super().__init__(axes) 10 | self._colormap = plt.get_cmap("gray") 11 | self.zorder = 1 12 | 13 | def plot(self, data: Ctx) -> None: 14 | extent = self.calculate_extent(data) 15 | self._image = self._axes.imshow(data.data_to_plot, 16 | cmap=self._colormap, 17 | vmin=data.contrast_ct[0], 18 | vmax=data.contrast_ct[1], 19 | aspect=data.aspect, 20 | extent=extent, 21 | origin='lower', 22 | zorder=self.zorder) 23 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/plotter/images/dose_image.py: -------------------------------------------------------------------------------- 1 | from matplotlib import pyplot as plt 2 | 3 | from pytripgui.canvas_vc.plotter.images.patient_image_base import PatientImageBase 4 | from pytripgui.canvas_vc.objects.dos import Dos 5 | 6 | 7 | class DoseImage(PatientImageBase): 8 | def __init__(self, axes): 9 | super().__init__(axes) 10 | self._colormap = plt.get_cmap() 11 | self._colormap._init() 12 | self._colormap._lut[:, -1] = 0.7 13 | self._colormap._lut[0, -1] = 0.0 14 | self.zorder = 5 15 | 16 | def plot(self, data: Dos) -> None: 17 | extent = self.calculate_extent(data) 18 | self._image = self._axes.imshow(data.data_to_plot, 19 | cmap=self._colormap, 20 | vmax=data.max_dose, 21 | aspect=data.aspect, 22 | extent=extent, 23 | origin='lower', 24 | zorder=self.zorder, 25 | interpolation='none') 26 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/plotter/images/let_image.py: -------------------------------------------------------------------------------- 1 | from matplotlib import pyplot as plt 2 | 3 | from pytripgui.canvas_vc.plotter.images.patient_image_base import PatientImageBase 4 | from pytripgui.canvas_vc.objects.let import Let 5 | 6 | 7 | class LetImage(PatientImageBase): 8 | def __init__(self, axes): 9 | super().__init__(axes) 10 | self._colormap = plt.get_cmap() 11 | self._colormap._init() 12 | self._colormap._lut[:, -1] = 0.7 13 | self._colormap._lut[0, -1] = 0.0 14 | self.zorder = 10 15 | 16 | def plot(self, data: Let) -> None: 17 | extent = self.calculate_extent(data) 18 | self._image = self._axes.imshow(data.data_to_plot, 19 | cmap=self._colormap, 20 | vmax=data.max_let, 21 | aspect=data.aspect, 22 | extent=extent, 23 | origin='lower', 24 | zorder=self.zorder) 25 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/plotter/images/patient_image_base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Optional 3 | 4 | from matplotlib.axes import Axes 5 | from matplotlib.image import AxesImage 6 | from pytrip import Cube 7 | 8 | from pytripgui.canvas_vc.objects.data_base import PlotDataBase 9 | 10 | """ 11 | This class and its subclasses were made to remove extra responsibility from mpl_plotter. 12 | It has methods that allow to show image, check if it is shown, update and remove it. 13 | Greatly removes code duplicates and hides information how images are made. 14 | """ 15 | 16 | 17 | class PatientImageBase(ABC): 18 | """ 19 | Abstract base class that holds CRUD-like methods common for all shown images 20 | 21 | Method plot(data) must be implemented in subclasses, because it strongly depends on type of shown image 22 | """ 23 | 24 | def __init__(self, axes): 25 | """ 26 | Parameters 27 | ---------- 28 | axes : Axes -- axes on which images will be shown 29 | """ 30 | self._axes: Axes = axes 31 | self._image: Optional[AxesImage] = None 32 | 33 | @abstractmethod 34 | def plot(self, data) -> None: 35 | """ 36 | Plots image from passed data. 37 | """ 38 | 39 | def get(self) -> Optional[AxesImage]: 40 | """ 41 | Returns plotted image. 42 | """ 43 | return self._image 44 | 45 | def update(self, data) -> None: 46 | """ 47 | Updates data in plotted image. 48 | """ 49 | self._image.set_data(data.data_to_plot) 50 | 51 | def remove(self) -> None: 52 | """ 53 | Removes image from axes. 54 | """ 55 | self._image.remove() 56 | self._image = None 57 | 58 | def is_present(self) -> bool: 59 | """ 60 | Returns True if image is present and False if image is absent 61 | """ 62 | return self._image is not None 63 | 64 | def calculate_extent(self, data: PlotDataBase): 65 | """ 66 | Returns extent for passed data. 67 | """ 68 | # get minimal X, Y and Z coordinates 69 | cube: Cube = data.cube 70 | min_x_mm, min_y_mm, min_z_mm = cube.indices_to_pos([0, 0, 0]) 71 | # get maximal X, Y and Z coordinates 72 | # 'data.cube.dimz - 1' is described in https://github.com/pytrip/pytrip/issues/592 73 | max_x_mm, max_y_mm, max_z_mm = cube.indices_to_pos([cube.dimx, cube.dimy, cube.dimz - 1]) 74 | plane = data.projection_selector.plane 75 | # depending on plane, return proper list those above 76 | # extent = [horizontal_min, horizontal_max, vertical_min, vertical_max] 77 | # "Transversal" (xy) 78 | if plane == "Transversal": 79 | return min_x_mm, max_x_mm, min_y_mm, max_y_mm 80 | # "Sagittal" (yz) 81 | if plane == "Sagittal": 82 | return min_y_mm, max_y_mm, min_z_mm, max_z_mm 83 | # "Coronal" (xz) 84 | if plane == "Coronal": 85 | return min_x_mm, max_x_mm, min_z_mm, max_z_mm 86 | 87 | raise ValueError("Wrong plane string - " + plane + " - in " + type(self).__name__) 88 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/plotter/managers/__init__.py: -------------------------------------------------------------------------------- 1 | from pytripgui.canvas_vc.plotter.managers.blit_manager import BlitManager 2 | from pytripgui.canvas_vc.plotter.managers.placement_manager import PlacementManager 3 | from pytripgui.canvas_vc.plotter.managers.plotting_manager import PlottingManager 4 | 5 | __all__ = ['BlitManager', 'PlacementManager', 'PlottingManager'] 6 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/plotter/managers/blit_manager.py: -------------------------------------------------------------------------------- 1 | # class copied from https://matplotlib.org/stable/tutorials/advanced/blitting.html 2 | # added removing artists 3 | 4 | 5 | class BlitManager: 6 | def __init__(self, canvas, animated_artists=()): 7 | """ 8 | Parameters 9 | ---------- 10 | canvas : FigureCanvasAgg 11 | The canvas to work with, this only works for sub-classes of the Agg 12 | canvas which have the `~FigureCanvasAgg.copy_from_bbox` and 13 | `~FigureCanvasAgg.restore_region` methods. 14 | 15 | animated_artists : Iterable[Artist] 16 | List of the artists to manage 17 | """ 18 | self.canvas = canvas 19 | self._bg = None 20 | self._artists = [] 21 | 22 | for a in animated_artists: 23 | self.add_artist(a) 24 | # grab the background on every draw 25 | self.cid = canvas.mpl_connect("draw_event", self.on_draw) 26 | 27 | def on_draw(self, event): 28 | """Callback to register with 'draw_event'.""" 29 | cv = self.canvas 30 | if event is not None and event.canvas != cv: 31 | raise RuntimeError 32 | self._bg = cv.copy_from_bbox(cv.figure.bbox) 33 | self._draw_animated() 34 | 35 | def add_artist(self, art): 36 | """ 37 | Add an artist to be managed. 38 | 39 | Parameters 40 | ---------- 41 | art : Artist 42 | 43 | The artist to be added. Will be set to 'animated' (just 44 | to be safe). *art* must be in the figure associated with 45 | the canvas this class is managing. 46 | 47 | """ 48 | if art.figure != self.canvas.figure: 49 | raise RuntimeError 50 | art.set_animated(True) 51 | self._artists.append(art) 52 | 53 | def remove_artist(self, art): 54 | """ 55 | Remove an artist. 56 | 57 | Parameters 58 | ---------- 59 | art : Artist 60 | 61 | The artist to be removed. 62 | *art* must be in the figure associated with 63 | the canvas this class is managing. 64 | 65 | """ 66 | if art.figure != self.canvas.figure: 67 | raise RuntimeError 68 | self._artists.remove(art) 69 | 70 | def _draw_animated(self): 71 | """Draw all of the animated artists.""" 72 | fig = self.canvas.figure 73 | for a in self._artists: 74 | fig.draw_artist(a) 75 | 76 | def update(self): 77 | """Update the screen with animated artists.""" 78 | cv = self.canvas 79 | fig = cv.figure 80 | # paranoia in case we missed the draw event, 81 | if self._bg is None: 82 | self.on_draw(None) 83 | else: 84 | # restore the background 85 | cv.restore_region(self._bg) 86 | # draw all of the animated artists 87 | self._draw_animated() 88 | # update the GUI state 89 | cv.blit(fig.bbox) 90 | # let the GUI event loop process anything it has to do 91 | cv.flush_events() 92 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/plotter/managers/placement_manager.py: -------------------------------------------------------------------------------- 1 | from matplotlib.figure import Figure 2 | from matplotlib.gridspec import GridSpec, SubplotSpec 3 | """ 4 | This class was made to remove extra responsibilities from mpl_plotter. 5 | It gathers information about elements that are added to the figure and determines where they should be put 6 | based on some boolean mask. 7 | Thanks to it, it is possible to display elements of figure differently based on what elements are to be displayed. 8 | 9 | If you want to add new element, follow these few steps: 10 | 1. Add flag that represents presence or absence of your new element 11 | 2. Add new entry to _positions dictionary for your element 12 | 3. Add getter for that new entry 13 | 4. Add methods that set that flag to true and false 14 | 5. Add method that holds condition with your element's flag 15 | 6. Add method that changes entries in _positions dictionary in the way that your elements fits in figure 16 | """ 17 | 18 | 19 | class PlacementManager: 20 | """ 21 | Class that holds information what is present on figure and positions of those elements. 22 | Updates positions every time something is added to figure to ensure that everything has space to be shown properly. 23 | """ 24 | def __init__(self, figure: Figure): 25 | """ 26 | Parameters: 27 | ---------- 28 | figure: Figure -- figure on which grid to position elements will be made 29 | """ 30 | self.columns: int = 16 31 | self.rows: int = 9 32 | self._grid_spec: GridSpec = GridSpec(ncols=self.columns, nrows=self.rows, figure=figure) 33 | 34 | self._ctx_bar: bool = False 35 | self._dose_bar: bool = False 36 | self._let_bar: bool = False 37 | 38 | self._positions: dict = { 39 | 'coord_info': self._grid_spec[:2, 13:], 40 | 'plotter': self._grid_spec[:, 2:14], 41 | 'ctx_bar': None, 42 | 'dose_bar': None, 43 | 'let_bar': None 44 | } 45 | 46 | """ 47 | Block of methods that set each flag to false or true 48 | """ 49 | 50 | def add_ctx_bar(self) -> None: 51 | self._ctx_bar = True 52 | self._update_places() 53 | 54 | def remove_ctx_bar(self) -> None: 55 | self._ctx_bar = False 56 | self._update_places() 57 | 58 | def add_dose_bar(self) -> None: 59 | self._dose_bar = True 60 | self._update_places() 61 | 62 | def remove_dose_bar(self) -> None: 63 | self._dose_bar = False 64 | self._update_places() 65 | 66 | def add_let_bar(self) -> None: 67 | self._let_bar = True 68 | self._update_places() 69 | 70 | def remove_let_bar(self) -> None: 71 | self._let_bar = False 72 | self._update_places() 73 | 74 | """ 75 | Block of methods that hold conditions and methods that change _positions based on those conditions 76 | """ 77 | 78 | def _only_ctx(self) -> bool: 79 | return self._ctx_bar and not self._dose_bar and not self._let_bar 80 | 81 | def _only_ctx_places(self) -> None: 82 | self._positions['ctx_bar'] = self._grid_spec[:, 1] 83 | self._positions['dose_bar'] = None 84 | self._positions['let_bar'] = None 85 | 86 | def _ctx_and_dose(self) -> bool: 87 | return self._ctx_bar and self._dose_bar and not self._let_bar 88 | 89 | def _ctx_and_dose_places(self) -> None: 90 | self._positions['ctx_bar'] = self._grid_spec[:, 1] 91 | self._positions['dose_bar'] = self._grid_spec[:, 0] 92 | self._positions['let_bar'] = None 93 | 94 | def _ctx_and_let(self) -> bool: 95 | return self._ctx_bar and not self._dose_bar and self._let_bar 96 | 97 | def _ctx_and_let_places(self) -> None: 98 | self._positions['ctx_bar'] = self._grid_spec[:, 1] 99 | self._positions['dose_bar'] = None 100 | self._positions['let_bar'] = self._grid_spec[:, 0] 101 | 102 | """ 103 | Method that checks which condition is satisfied and updates positions 104 | """ 105 | 106 | def _update_places(self) -> None: 107 | if self._only_ctx(): 108 | self._only_ctx_places() 109 | elif self._ctx_and_dose(): 110 | self._ctx_and_dose_places() 111 | elif self._ctx_and_let(): 112 | self._ctx_and_let_places() 113 | 114 | """ 115 | Block of getter for each element 116 | """ 117 | 118 | def get_coord_info_place(self) -> SubplotSpec: 119 | return self._positions['coord_info'] 120 | 121 | def get_main_plot_place(self) -> SubplotSpec: 122 | return self._positions['plotter'] 123 | 124 | def get_ctx_bar_place(self) -> SubplotSpec: 125 | return self._positions['ctx_bar'] 126 | 127 | def get_dose_bar_place(self) -> SubplotSpec: 128 | return self._positions['dose_bar'] 129 | 130 | def get_let_bar_place(self) -> SubplotSpec: 131 | return self._positions['let_bar'] 132 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/plotter/mpl_plotter.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtCore 2 | from PyQt5.QtWidgets import QSizePolicy 3 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas 4 | from matplotlib.figure import Figure 5 | 6 | from pytripgui.canvas_vc.plotter.managers import PlottingManager, BlitManager 7 | 8 | 9 | class MplPlotter(FigureCanvas): 10 | """ 11 | Viewer class for matplotlib 2D plotting widget 12 | """ 13 | def __init__(self, parent=None, width=16, height=9, dpi=100): 14 | """ 15 | Parameters: 16 | ---------- 17 | parent : type? -- ? 18 | 19 | width : int -- width of created figure 20 | 21 | height : int -- height of created figure 22 | 23 | dpi: int -- dpi of created figure 24 | """ 25 | super().__init__(Figure()) 26 | """ 27 | self.figure has to be initialized before self.blit_manager 28 | it is so because blit_manager uses figure to do its work - restoring background 29 | if blit_manager will be created before figure is initialized 30 | dummy figure will be used and restoring background will work improperly 31 | """ 32 | self.figure: Figure = Figure(figsize=(width, height), dpi=dpi, constrained_layout=True) 33 | self.blit_manager: BlitManager = BlitManager(self) 34 | self.plotting_manager: PlottingManager = PlottingManager(self.figure, self.blit_manager) 35 | 36 | FigureCanvas.__init__(self, self.figure) 37 | 38 | if parent: 39 | parent.addWidget(self) 40 | 41 | FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding) 42 | FigureCanvas.updateGeometry(self) 43 | 44 | # next too lines are needed in order to catch keypress events in plot canvas by mpl_connect() 45 | FigureCanvas.setFocusPolicy(self, QtCore.Qt.ClickFocus) 46 | FigureCanvas.setFocus(self) 47 | 48 | def set_button_press_callback(self, callback): 49 | self.figure.canvas.mpl_connect('button_press_event', callback) 50 | 51 | def set_scroll_event_callback(self, callback): 52 | self.figure.canvas.mpl_connect('scroll_event', callback) 53 | 54 | def set_mouse_motion_callback(self, callback): 55 | self.figure.canvas.mpl_connect('motion_notify_event', callback) 56 | 57 | def set_key_press_callback(self, callback): 58 | self.figure.canvas.mpl_connect('key_press_event', callback) 59 | 60 | def remove_dos(self): 61 | self.plotting_manager.remove_dos() 62 | 63 | def plot_dos(self, dos): 64 | self.plotting_manager.plot_dos(dos) 65 | 66 | def remove_let(self): 67 | self.plotting_manager.remove_let() 68 | 69 | def plot_let(self, data): 70 | self.plotting_manager.plot_let(data) 71 | 72 | def remove_ctx(self): 73 | self.plotting_manager.remove_ctx() 74 | 75 | def plot_ctx(self, data): 76 | self.plotting_manager.plot_ctx(data) 77 | 78 | def plot_voi(self, vdx): 79 | self.plotting_manager.plot_voi(vdx) 80 | 81 | def remove_voi(self): 82 | self.plotting_manager.remove_voi() 83 | 84 | def update(self): 85 | self.blit_manager.update() 86 | -------------------------------------------------------------------------------- /pytripgui/canvas_vc/projection_selector.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | 5 | 6 | class ProjectionSelector: 7 | def __init__(self): 8 | self._transversal_slice_no = 0 9 | self._sagittal_slice_no = 0 10 | self._coronal_slice_no = 0 11 | 12 | self._transversal_last_slice_no = 0 13 | self._sagittal_last_slice_no = 0 14 | self._coronal_last_slice_no = 0 15 | 16 | # "Transversal" (xy) 17 | # "Sagittal" (yz) 18 | # "Coronal" (xz) 19 | self.plane = "Transversal" 20 | 21 | def next_slice(self): 22 | self.current_slice_no = (self.current_slice_no + 1) % self.last_slice_no 23 | 24 | def prev_slice(self): 25 | self.current_slice_no = (self.current_slice_no - 1) % self.last_slice_no 26 | 27 | def get_projection(self, data): 28 | if self.plane == "Transversal": 29 | return data.cube[self.current_slice_no, ::, ::] 30 | if self.plane == "Sagittal": 31 | return data.cube[::, ::, self.current_slice_no] 32 | if self.plane == "Coronal": 33 | return data.cube[::, self.current_slice_no, ::] 34 | 35 | def get_current_slices(self): 36 | return { 37 | 'Transversal': self._transversal_slice_no, 38 | 'Sagittal': self._sagittal_slice_no, 39 | 'Coronal': self._coronal_slice_no 40 | } 41 | 42 | def get_last_slices(self): 43 | return { 44 | 'Transversal': self._transversal_last_slice_no, 45 | 'Sagittal': self._sagittal_last_slice_no, 46 | 'Coronal': self._coronal_last_slice_no 47 | } 48 | 49 | def load_slices_count(self, data): 50 | self._transversal_last_slice_no = data.dimz 51 | self._sagittal_last_slice_no = data.dimy 52 | self._coronal_last_slice_no = data.dimx 53 | 54 | self._transversal_slice_no = self._transversal_last_slice_no // 2 55 | self._sagittal_slice_no = self._sagittal_last_slice_no // 2 56 | self._coronal_slice_no = self._coronal_last_slice_no // 2 57 | 58 | def is_loaded(self): 59 | """Check if all slice numbers are non-zero""" 60 | return self._transversal_last_slice_no * self._sagittal_last_slice_no * self._coronal_last_slice_no != 0 61 | 62 | @property 63 | def current_slice_no(self): 64 | if self.plane == "Transversal": 65 | return self._transversal_slice_no 66 | if self.plane == "Sagittal": 67 | return self._sagittal_slice_no 68 | if self.plane == "Coronal": 69 | return self._coronal_slice_no 70 | 71 | @current_slice_no.getter 72 | def current_slice_no(self): 73 | if self.plane == "Transversal": 74 | return self._transversal_slice_no 75 | if self.plane == "Sagittal": 76 | return self._sagittal_slice_no 77 | if self.plane == "Coronal": 78 | return self._coronal_slice_no 79 | 80 | @current_slice_no.setter 81 | def current_slice_no(self, position): 82 | if self.plane == "Transversal": 83 | self._transversal_slice_no = position 84 | if self.plane == "Sagittal": 85 | self._sagittal_slice_no = position 86 | if self.plane == "Coronal": 87 | self._coronal_slice_no = position 88 | 89 | @property 90 | def last_slice_no(self): 91 | if self.plane == "Transversal": 92 | return self._transversal_last_slice_no 93 | if self.plane == "Sagittal": 94 | return self._sagittal_last_slice_no 95 | if self.plane == "Coronal": 96 | return self._coronal_last_slice_no 97 | -------------------------------------------------------------------------------- /pytripgui/config_vc/__init__.py: -------------------------------------------------------------------------------- 1 | from pytripgui.config_vc.config_view import ConfigQtView 2 | from pytripgui.config_vc.config_cont import ConfigController 3 | 4 | # from https://docs.python.org/3/tutorial/modules.html 5 | # if a package's __init__.py code defines a list named __all__, 6 | # it is taken to be the list of module names that should be imported when from package import * is encountered. 7 | __all__ = ['ConfigQtView', 'ConfigController'] 8 | -------------------------------------------------------------------------------- /pytripgui/config_vc/config_cont.py: -------------------------------------------------------------------------------- 1 | from pytripgui.plan_executor.trip_config import Trip98ConfigModel 2 | from paramiko import ssh_exception 3 | 4 | import logging 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class ConfigController: 10 | def __init__(self, model, view): 11 | self.model = model 12 | self.view = view 13 | self.user_clicked_save = False 14 | self._setup_callbacks() 15 | 16 | self.view.test_ssh_clicked_callback_connect(self._test_ssh) 17 | 18 | if not self.model: 19 | self.model = [Trip98ConfigModel()] 20 | 21 | def _setup_callbacks(self): 22 | self.view.set_ok_callback(self._save_and_exit) 23 | self.view.set_cancel_callback(self._exit) 24 | 25 | self.view.add_button.emit_on_click(lambda: self.view.configs.append_element(Trip98ConfigModel(), "")) 26 | self.view.remove_button.emit_on_click(self.view.configs.remove_current_item) 27 | 28 | def _save_and_exit(self): 29 | self.user_clicked_save = True 30 | self.model = self.view.configs.data 31 | self._exit() 32 | 33 | def _exit(self): 34 | if self.view.configs.count: 35 | self._set_model_from_view() 36 | self.view.exit() 37 | 38 | def set_view_from_model(self): 39 | self.view.configs.fill(self.model, lambda item: item.name) 40 | self.view.configs.emit_on_item_change(self._on_item_change_callback) 41 | self._set_current_config() 42 | 43 | def _on_item_change_callback(self): 44 | self._set_model_from_view() 45 | self._set_current_config() 46 | 47 | def _set_current_config(self): 48 | config = self.view.configs.current_data 49 | 50 | self.view.remote_execution = config.remote_execution 51 | self.view.name.text = config.name 52 | self.view.wdir_path.text = config.wdir_path 53 | self.view.trip_path.text = config.trip_path 54 | self.view.hlut_path.text = config.hlut_path 55 | self.view.dedx_path.text = config.dedx_path 56 | self.view.host_name.text = config.host_name 57 | self.view.user_name.text = config.user_name 58 | self.view.pkey_path.text = config.pkey_path 59 | self.view.password.text = config.password 60 | self.view.wdir_remote_path.text = config.wdir_remote_path 61 | 62 | def _set_model_from_view(self): 63 | config = self.view.configs.last_data 64 | 65 | # after you delete config, there is nothing in last_data 66 | if not config: 67 | return 68 | 69 | config.remote_execution = self.view.remote_execution 70 | config.name = self.view.name.text 71 | config.wdir_path = self.view.wdir_path.text 72 | config.trip_path = self.view.trip_path.text 73 | config.hlut_path = self.view.hlut_path.text 74 | config.dedx_path = self.view.dedx_path.text 75 | config.host_name = self.view.host_name.text 76 | config.user_name = self.view.user_name.text 77 | config.pkey_path = self.view.pkey_path.text 78 | config.password = self.view.password.text 79 | config.wdir_remote_path = self.view.wdir_remote_path.text 80 | 81 | def _test_ssh(self): 82 | import paramiko 83 | ssh = paramiko.SSHClient() 84 | ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 85 | 86 | key_path = None 87 | if self.view.pkey_path.text: 88 | key_path = self.view.pkey_path.text 89 | 90 | try: 91 | ssh.connect(hostname=self.view.host_name.text, 92 | username=self.view.user_name.text, 93 | password=self.view.password.text, 94 | key_filename=key_path) 95 | except ssh_exception.AuthenticationException as e: 96 | self.view.info_box.show_error("Authentication", e.__str__()) 97 | except FileNotFoundError as e: 98 | self.view.info_box.show_error("File not found", e.__str__()) 99 | except ValueError as e: 100 | self.view.info_box.show_error("Value", e.__str__()) 101 | else: 102 | 103 | sftp = ssh.open_sftp() 104 | try: 105 | sftp.stat(self.view.wdir_remote_path.text) 106 | except FileNotFoundError: 107 | self.view.info_box.show_error("File not found", "Remote working directory doesn't exist") 108 | else: 109 | self.view.info_box.show_info("SSH Connection", "Everything OK") 110 | 111 | ssh.close() 112 | -------------------------------------------------------------------------------- /pytripgui/contouring_vc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/pytripgui/contouring_vc/__init__.py -------------------------------------------------------------------------------- /pytripgui/contouring_vc/contouring_controller.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from typing import Collection 4 | 5 | from PyQt5.QtWidgets import QApplication 6 | from pytrip import Voi 7 | 8 | from pytripgui.contouring_vc.contouring_view import ContouringView 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class ContouringController: 14 | def __init__(self, vois: Collection[Voi], parent=None): 15 | self._view = ContouringView(parent=parent, voi_number=len(vois)) 16 | self._vois = vois 17 | self._view.connect_yes(lambda: self._contour_vois()) 18 | 19 | def _contour_vois(self): 20 | self._view.update_accepted() 21 | QApplication.processEvents() 22 | total_vois = len(self._vois) 23 | start = time.time() 24 | for current, voi in enumerate(self._vois): 25 | self._view.update_progress(voi.name, current, total_vois) 26 | QApplication.processEvents() 27 | voi.calculate_slices_with_contours_in_sagittal_and_coronal() 28 | end = time.time() 29 | self._view.update_finished(end-start) 30 | 31 | def show(self): 32 | self._view.show() 33 | -------------------------------------------------------------------------------- /pytripgui/contouring_vc/contouring_view.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from PyQt5.QtWidgets import QApplication, QDialog, QDialogButtonBox 4 | from pytripgui.view.qt_gui import ContouringDialog 5 | 6 | 7 | class ContouringView: 8 | def __init__(self, parent=None, voi_number=None): 9 | self._ui: QDialog = ContouringDialog(parent) 10 | 11 | # set window text defaults 12 | self._initial_prompt = "Would you like to precalculate VOI contours?" 13 | self._initial_warning = "This may take a while - there are {} VOIs, " \ 14 | "but it will speed up viewing VOI contours.".format(voi_number) 15 | self._before_calculation = "Getting ready..." 16 | self._progress_message = "Precalculating contours for VOI: \n{name} ({current}/{total})" 17 | self._time_of_calc = "{label}: {elapsed_time:.2f} seconds" 18 | self._calculation_warning = "This may take a while..." 19 | self._finish_message = "Precalculating complete!" 20 | 21 | def _set_progress_label_text(self, text): 22 | self._ui.progress_label.setText(text) 23 | 24 | def _set_warning_label_text(self, text): 25 | self._ui.warning_label.setText(text) 26 | 27 | def _set_window_title(self, title): 28 | self._ui.setWindowTitle(title) 29 | 30 | def show(self): 31 | self._set_progress_label_text(self._initial_prompt) 32 | self._set_warning_label_text(self._initial_warning) 33 | self._ui.show() 34 | # we need to process events to let UI init take effect 35 | QApplication.processEvents() 36 | 37 | def _buttons_set_enabled(self, enabled): 38 | self._ui.button_box.setEnabled(enabled) 39 | 40 | def _set_buttons(self, flags): 41 | self._ui.button_box.setStandardButtons(flags) 42 | 43 | def update_accepted(self): 44 | self._set_progress_label_text(self._before_calculation) 45 | self._set_warning_label_text(self._calculation_warning) 46 | self._buttons_set_enabled(False) 47 | self._set_buttons(QDialogButtonBox.Ok) 48 | self._ui.button_box.button(QDialogButtonBox.Ok).clicked.connect(self._ui.accept) 49 | 50 | def update_progress(self, voi_name, current, total): 51 | self._set_progress_label_text(self._progress_message.format(name=voi_name, current=current + 1, total=total)) 52 | 53 | def update_finished(self, elapsed_time): 54 | self._set_progress_label_text(self._finish_message) 55 | self._set_warning_label_text(self._time_of_calc.format(label='Elapsed time', elapsed_time=elapsed_time)) 56 | self._buttons_set_enabled(True) 57 | 58 | def connect_yes(self, callback): 59 | self._ui.button_box.button(QDialogButtonBox.Yes).clicked.connect(callback) 60 | -------------------------------------------------------------------------------- /pytripgui/controller/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/pytripgui/controller/__init__.py -------------------------------------------------------------------------------- /pytripgui/controller/dvh.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pytrip.volhist import VolHist 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | class Dvh: 9 | """ 10 | This class holds logic for plotting CTX stuff. 11 | """ 12 | def __init__(self, model, view): 13 | """ 14 | """ 15 | self.model = model 16 | self.view = view 17 | self.fig = self.view.ui.dvh 18 | self.fig.xlabel = "Dose [%]" 19 | self.fig.ylabel = "Volume [%]" 20 | 21 | def add_dvh(self, dos, voi): 22 | """ 23 | Calculates and plots a DVH for all dos in model based on voi. 24 | TODO: fix me later 25 | """ 26 | pm = self.model.plot 27 | 28 | dvh = self._calc_dvh(dos, voi) 29 | pm.dvhs.append(dvh) 30 | if not dvh.x or not dvh.y: 31 | return 32 | self.update_plot_dvh() 33 | 34 | def add_dvh_dos(self, dos, voi): 35 | """ 36 | Calculates and plots a DVH based on dos and voi. 37 | """ 38 | pm = self.model.plot 39 | 40 | dvh = self._calc_dvh(dos, voi) 41 | pm.dvhs.append(dvh) 42 | self.update_plot_dvh() 43 | 44 | @staticmethod 45 | def _calc_dvh(dos, voi): 46 | """ Calculates a Dvh 47 | """ 48 | # TODO, this could be run threaded when loading a DOS and VDX is present. 49 | 50 | logger.debug("Processing VOI '{:s}'...".format(voi.name)) 51 | return VolHist(dos, voi) 52 | 53 | def update_plot_dvh(self): 54 | """ 55 | """ 56 | # TODO: clear plot 57 | dvhs = self.model.plot.dvhs 58 | if dvhs: 59 | axes = self.fig.axes 60 | 61 | for dvh in dvhs: 62 | axes.plot(dvh.x, dvh.y, label=dvh.name) 63 | # labels are the same for each item in 'self.model.plot.dvhs' list 64 | axes.set_xlabel(dvhs[0].xlabel) 65 | axes.set_ylabel(dvhs[0].ylabel) 66 | 67 | self.fig.show() 68 | -------------------------------------------------------------------------------- /pytripgui/controller/lvh.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pytrip.volhist import VolHist 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | class Lvh: 9 | """ 10 | This class holds logic for plotting CTX stuff. 11 | """ 12 | def __init__(self, model, view): 13 | """ 14 | """ 15 | self.model = model 16 | self.view = view 17 | self.fig = self.view.ui.lvh 18 | self.fig.xlabel = "LET [keV/um]" 19 | self.fig.ylabel = "Volume [%]" 20 | 21 | def add_lvh(self, let, voi): 22 | """ 23 | Calculates and plots a LVH based on let and voi. 24 | """ 25 | pm = self.model.plot 26 | 27 | lvh = self._calc_lvh(let, voi) 28 | if not lvh.x or not lvh.y: 29 | return 30 | pm.lvhs.append(lvh) 31 | self.update_plot_lvh() 32 | 33 | @staticmethod 34 | def _calc_lvh(let, voi): 35 | """ Calculates a Lvh 36 | """ 37 | # TODO, this could be run threaded when loading a LET and VDX is present. 38 | 39 | logger.info("LVH Processing VOI '{:s}'...".format(voi.name)) 40 | return VolHist(let, voi) 41 | 42 | def update_plot_lvh(self): 43 | """ 44 | """ 45 | # TODO: clear plot 46 | lvhs = self.model.plot.lvhs 47 | if lvhs: 48 | axes = self.fig.axes 49 | for lvh in lvhs: 50 | axes.plot(lvh.x, lvh.y, label=lvh.name) 51 | # labels are the same for each item in 'self.model.plot.lvhs' list 52 | axes.set_xlabel(lvhs[0].xlabel) 53 | axes.set_ylabel(lvhs[0].ylabel) 54 | 55 | self.fig.show() 56 | -------------------------------------------------------------------------------- /pytripgui/controller/settings_cont.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | import logging 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | class SettingsController: 9 | """ 10 | Class for interacting with saved configurations. 11 | 12 | Settings are connected to Model, in a way that 13 | - upon mysettingscontroller.load(), SettingsModel attributes starting with "_" are not written to Model. 14 | - upon mysettingscontroller.save(), Model attributes starting with "__" are not written to SettingsModel. 15 | 16 | This way, 17 | a) _version is written to disk, but imported into Model when loading 18 | b) __internal_attribute__ are not passed between Model and SettingsModel 19 | """ 20 | 21 | default_filename = "settings_pytripgui.pkl" 22 | 23 | def __init__(self, model): 24 | """ 25 | """ 26 | 27 | self.model = model 28 | # new settings handler here 29 | 30 | self.pkl_path = os.path.join(self.get_user_directory(), self.default_filename) 31 | logger.debug("New setup config parser based on {:s}".format(self.pkl_path)) 32 | 33 | try: 34 | self.load() 35 | except ModuleNotFoundError: 36 | logger.error("Cannot load settings") 37 | 38 | def load(self): 39 | """ 40 | Load config, sets self.model accordingly. 41 | If file does not exist, exit silently. 42 | """ 43 | 44 | logger.debug("SettingsController.load() : loading settings from {}".format(self.pkl_path)) 45 | 46 | model = self.model 47 | pkl = self.pkl_path 48 | 49 | if os.path.isfile(pkl): 50 | with open(pkl, 'rb') as f: 51 | _ms = pickle.load(f) 52 | 53 | # set all model attributes accordingly 54 | for _attr in dir(_ms): 55 | if _attr[0] != "_": # do not copy internal variables 56 | _value = getattr(_ms, _attr) 57 | if hasattr(model, _attr): 58 | setattr(model, _attr, _value) 59 | else: 60 | logger.warning("pytripgui.model has no attribute '{}'. It will not be set.".format(_attr)) 61 | else: 62 | logger.info("Settings file {} not found.".format(pkl)) 63 | 64 | def save(self, path=None): 65 | """ 66 | Saves the current model configuration to the settings file. 67 | 68 | :params str path: 69 | If path is not given, then the settings will be saved in the default config location. 70 | If path is given, then it will be saved at the given path, but this path is forgotten afterwards. 71 | 72 | This is useful for exporting the settings to different computers. 73 | """ 74 | model = self.model 75 | _ms = self.model.settings 76 | 77 | if path: 78 | pkl = path 79 | else: 80 | pkl = self.pkl_path 81 | 82 | logger.debug("SettingsController.save() : saving settings to {}".format(self.pkl_path)) 83 | 84 | # sync model.settings attributes from model 85 | for _attr in dir(_ms): 86 | if "__" not in _attr: # do not copy internal variables 87 | _value = getattr(model, _attr) 88 | setattr(_ms, _attr, _value) 89 | 90 | with open(pkl, 'wb') as f: 91 | pickle.dump(model.settings, f) 92 | 93 | @staticmethod 94 | def get_user_directory(): 95 | """ 96 | Returns PyTRiP user config dir. Create it, if it does not exsist. 97 | """ 98 | path = os.path.join(os.path.expanduser("~"), ".pytrip") 99 | if not os.path.exists(path): 100 | os.makedirs(path) 101 | return path 102 | -------------------------------------------------------------------------------- /pytripgui/empty_patient_vc/__init__.py: -------------------------------------------------------------------------------- 1 | from pytripgui.empty_patient_vc.empty_patient_view import EmptyPatientQtView 2 | from pytripgui.empty_patient_vc.empty_patient_cont import EmptyPatientController 3 | 4 | # from https://docs.python.org/3/tutorial/modules.html 5 | # if a package's __init__.py code defines a list named __all__, 6 | # it is taken to be the list of module names that should be imported when from package import * is encountered. 7 | __all__ = ['EmptyPatientQtView', 'EmptyPatientController'] 8 | -------------------------------------------------------------------------------- /pytripgui/empty_patient_vc/empty_patient_view.py: -------------------------------------------------------------------------------- 1 | from pytripgui.view.qt_view_adapter import LineEdit, TabWidget, LineEditMath 2 | from pytripgui.view.qt_gui import EmptyPatientDialog 3 | 4 | import logging 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class EmptyPatientQtView: 10 | """ 11 | """ 12 | def __init__(self, parent=None): 13 | self._ui = EmptyPatientDialog(parent) 14 | self.name = LineEdit(self._ui.name_lineEdit) 15 | self.hu_value = LineEditMath(self._ui.hUValue_lineEdit) 16 | 17 | self.dimensions_tabs = TabWidget(self._ui.dimensions_tabWidget) 18 | self.dimensions_fields = [{ 19 | "width": LineEditMath(self._ui.width_lineEdit_1), 20 | "height": LineEditMath(self._ui.height_lineEdit_1), 21 | "depth": LineEditMath(self._ui.depth_lineEdit_1), 22 | "slice_distance": LineEditMath(self._ui.sliceDistance_lineEdit_1), 23 | "pixel_size": LineEditMath(self._ui.pixelSize_lineEdit_1), 24 | }, { 25 | "width": LineEditMath(self._ui.width_lineEdit_2), 26 | "height": LineEditMath(self._ui.height_lineEdit_2), 27 | "depth": LineEditMath(self._ui.depth_lineEdit_2), 28 | "slice_number": LineEditMath(self._ui.sliceNumber_lineEdit_2), 29 | "pixel_number_x": LineEditMath(self._ui.pixelNumberX_lineEdit_2), 30 | "pixel_number_y": LineEditMath(self._ui.pixelNumberY_lineEdit_2), 31 | }, { 32 | "slice_number": LineEditMath(self._ui.sliceNumber_lineEdit_3), 33 | "slice_distance": LineEditMath(self._ui.sliceDistance_lineEdit_3), 34 | "pixel_number_x": LineEditMath(self._ui.pixelNumberX_lineEdit_3), 35 | "pixel_number_y": LineEditMath(self._ui.pixelNumberY_lineEdit_3), 36 | "pixel_size": LineEditMath(self._ui.pixelSize_lineEdit_3), 37 | }] 38 | 39 | self.xoffset = LineEditMath(self._ui.xOffset_lineEdit) 40 | self.yoffset = LineEditMath(self._ui.yOffset_lineEdit) 41 | self.slice_offset = LineEditMath(self._ui.sliceOffset_lineEdit) 42 | 43 | self.accept = self._ui.accept 44 | self.accept_buttons = self._ui.accept_buttonBox 45 | 46 | def show(self) -> None: 47 | self._ui.show() 48 | self._ui.exec_() 49 | 50 | def exit(self) -> None: 51 | self._ui.close() 52 | -------------------------------------------------------------------------------- /pytripgui/executor_vc/__init__.py: -------------------------------------------------------------------------------- 1 | from pytripgui.executor_vc.executor_view import ExecutorQtView 2 | 3 | # from https://docs.python.org/3/tutorial/modules.html 4 | # if a package's __init__.py code defines a list named __all__, 5 | # it is taken to be the list of module names that should be imported when from package import * is encountered. 6 | __all__ = ['ExecutorQtView'] 7 | -------------------------------------------------------------------------------- /pytripgui/executor_vc/executor_view.py: -------------------------------------------------------------------------------- 1 | from pytripgui.view.qt_gui import UiExecuteDialog 2 | 3 | 4 | class ExecutorQtView: 5 | def __init__(self, parent=None): 6 | self._ui = UiExecuteDialog(parent) 7 | self._setup_internal_callbacks() 8 | 9 | def show(self): 10 | self._ui.show() 11 | self._ui.exec_() 12 | 13 | def _exit(self): 14 | self._ui.close() 15 | 16 | def _set_ok_callback(self, fun): 17 | self._ui.accept_ButtonBox.accepted.connect(fun) 18 | 19 | def _setup_internal_callbacks(self): 20 | self._set_ok_callback(self._exit) 21 | 22 | def append_log(self, text): 23 | self._ui.stdout_textBrowser.append(text) 24 | 25 | def enable_ok_button(self): 26 | self._ui.accept_ButtonBox.setEnabled(True) 27 | -------------------------------------------------------------------------------- /pytripgui/field_vc/__init__.py: -------------------------------------------------------------------------------- 1 | from pytripgui.field_vc.field_view import FieldQtView 2 | from pytripgui.field_vc.field_cont import FieldController 3 | 4 | # from https://docs.python.org/3/tutorial/modules.html 5 | # if a package's __init__.py code defines a list named __all__, 6 | # it is taken to be the list of module names that should be imported when from package import * is encountered. 7 | __all__ = ['FieldQtView', 'FieldController'] 8 | -------------------------------------------------------------------------------- /pytripgui/field_vc/angles_standard.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class AnglesStandard(Enum): 5 | TRIP = 1 6 | IEC = 2 7 | -------------------------------------------------------------------------------- /pytripgui/field_vc/field_cont.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pytrip.res.point import angles_to_trip, angles_from_trip 3 | 4 | from pytripgui.field_vc.angles_standard import AnglesStandard 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class FieldController: 10 | def __init__(self, model, view, kernels): 11 | self.model = model 12 | self.view = view 13 | self.kernels = kernels 14 | self.user_clicked_save = False 15 | 16 | def set_view_from_model(self): 17 | if self._is_isocenter_manually(): 18 | self.view.set_isocenter_values(self.model.isocenter) 19 | self.view.set_isocenter_state(True) 20 | else: 21 | self.view.set_isocenter_state(False) 22 | 23 | self._setup_ok_and_cancel_buttons_callbacks() 24 | 25 | self.view.angles_standard = self.model.angles_standard 26 | self._set_view_angles_according_to_standard(self.model.angles_standard, self.model.gantry_angle_trip98, 27 | self.model.couch_angle_trip98, True) 28 | 29 | self.view.spot_size = self.model.fwhm 30 | self.view.raster_step = self.model.raster_step 31 | self.view.dose_extension = self.model.dose_extension 32 | self.view.contour_extension = self.model.contour_extension 33 | self.view.depth_steps = self.model.zsteps 34 | 35 | def _is_isocenter_manually(self): 36 | return len(self.model.isocenter) == 3 37 | 38 | def _setup_ok_and_cancel_buttons_callbacks(self): 39 | self.view.set_ok_callback(self._save_and_exit) 40 | self.view.set_cancel_callback(self._exit) 41 | self.view.set_gui_needs_update_callback(self._recalculate_gui_values) 42 | 43 | def _save_and_exit(self): 44 | self.set_model_from_view() 45 | self.user_clicked_save = True 46 | self.view.exit() 47 | 48 | def _exit(self): 49 | self.view.exit() 50 | 51 | def set_model_from_view(self): 52 | if self.view.is_isocenter_manually(): 53 | self.model.isocenter = self.view.get_isocenter_value() 54 | else: 55 | self.model.isocenter = [] 56 | 57 | self.model.angles_standard = self.view.angles_standard 58 | self.model.gantry_angle_trip98, self.model.couch_angle_trip98 = self._get_view_angles_in_trip_standard() 59 | 60 | self.model.fwhm = self.view.spot_size 61 | self.model.raster_step = self.view.raster_step 62 | self.model.dose_extension = self.view.dose_extension 63 | self.model.contour_extension = self.view.contour_extension 64 | self.model.zsteps = self.view.depth_steps 65 | 66 | def _get_view_angles_in_trip_standard(self): 67 | _gantry_angle, _couch_angle = self.view.gantry_angle, self.view.couch_angle 68 | 69 | if self.view.angles_standard == AnglesStandard.IEC: 70 | _gantry_angle, _couch_angle = angles_to_trip(_gantry_angle, _couch_angle) 71 | 72 | return _gantry_angle, _couch_angle 73 | 74 | def _recalculate_gui_values(self): 75 | self._set_view_angles_according_to_standard(self.view.angles_standard, self.view.gantry_angle, 76 | self.view.couch_angle) 77 | 78 | def _set_view_angles_according_to_standard(self, standard, gantry_angle, couch_angle, init=False): 79 | if standard == AnglesStandard.IEC: 80 | _gantry_angle, _couch_angle = angles_from_trip(gantry_angle, couch_angle) 81 | self.view.gantry_angle = _gantry_angle 82 | self.view.couch_angle = _couch_angle 83 | else: 84 | _gantry_angle, _couch_angle = angles_to_trip(gantry_angle, couch_angle) if not init \ 85 | else (gantry_angle, couch_angle) 86 | self.view.gantry_angle = _gantry_angle 87 | self.view.couch_angle = _couch_angle 88 | -------------------------------------------------------------------------------- /pytripgui/field_vc/field_model.py: -------------------------------------------------------------------------------- 1 | from pytrip.tripexecuter.field import Field 2 | 3 | from pytripgui.field_vc.angles_standard import AnglesStandard 4 | 5 | 6 | class FieldModel(Field): 7 | def __init__(self, field=Field(), angles_standard=AnglesStandard.TRIP): 8 | super().__init__(field) 9 | self.angles_standard = angles_standard 10 | 11 | @property 12 | def gantry_angle_trip98(self): 13 | return self.gantry 14 | 15 | @property 16 | def couch_angle_trip98(self): 17 | return self.couch 18 | 19 | @gantry_angle_trip98.setter 20 | def gantry_angle_trip98(self, angle): 21 | self.gantry = angle 22 | 23 | @couch_angle_trip98.setter 24 | def couch_angle_trip98(self, angle): 25 | self.couch = angle 26 | -------------------------------------------------------------------------------- /pytripgui/kernel_vc/__init__.py: -------------------------------------------------------------------------------- 1 | from pytripgui.kernel_vc.kernel_view import KernelQtView 2 | from pytripgui.kernel_vc.kernel_cont import KernelController 3 | 4 | # from https://docs.python.org/3/tutorial/modules.html 5 | # if a package's __init__.py code defines a list named __all__, 6 | # it is taken to be the list of module names that should be imported when from package import * is encountered. 7 | __all__ = ['KernelQtView', 'KernelController'] 8 | -------------------------------------------------------------------------------- /pytripgui/kernel_vc/kernel_cont.py: -------------------------------------------------------------------------------- 1 | from pytrip.tripexecuter import KernelModel 2 | from pytrip.tripexecuter import Projectile 3 | 4 | import logging 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class KernelController: 10 | def __init__(self, model, view): 11 | self.kernels = model 12 | self.last_kernel_index = None 13 | self.view = view 14 | self.user_clicked_save = False 15 | self._setup_ok_and_cancel_buttons_callbacks() 16 | self.view.set_selected_beam_kernel_callback(self._current_kernel_index_has_changed) 17 | self.view.new_beam_kernel_callback(self._new_beam_kernel) 18 | self.view.remove_beam_kernel_callback(self._remove_current_beam_kernel) 19 | 20 | def _setup_ok_and_cancel_buttons_callbacks(self): 21 | self.view.set_ok_callback(self._save_and_exit) 22 | self.view.set_cancel_callback(self._exit) 23 | 24 | def _save_and_exit(self): 25 | self.set_model_from_view() 26 | self.user_clicked_save = True 27 | self.view.exit() 28 | 29 | def _exit(self): 30 | self.view.exit() 31 | 32 | def _current_kernel_index_has_changed(self, index): 33 | # this mean there is no data on kernel list 34 | if index == -1: 35 | self._new_beam_kernel() 36 | 37 | # if item was deleted, then this statement is false 38 | if index != self.last_kernel_index: 39 | # save previous kernel config to VIEW memory 40 | if self.last_kernel_index is not None: 41 | current_kernel_config = self._visible_kernel_config 42 | self.view.replace_kernel_by_index(current_kernel_config, self.last_kernel_index) 43 | 44 | # load new kernel config 45 | self.last_kernel_index = index 46 | new_kernel_config = self.view.get_selected_kernel() 47 | self._visible_kernel_config = new_kernel_config 48 | 49 | def _new_beam_kernel(self): 50 | kernel = KernelModel("Kernel") 51 | kernel.projectile = Projectile("H") 52 | self.view.add_kernel_with_name(kernel, kernel.name) 53 | self.view.select_recently_added_kernel() 54 | 55 | def _remove_current_beam_kernel(self): 56 | self.view.remove_current_kernel() 57 | 58 | def set_view_from_model(self): 59 | # projectile symbols should be setup before setup any kernel 60 | sorted_projectile = [y[0] for y in sorted(Projectile.projectile_defaults.items(), key=lambda x: x[1][0])] 61 | self.view.setup_all_available_projectile_symbols(sorted_projectile) 62 | if not self.kernels: 63 | self._new_beam_kernel() 64 | return 65 | 66 | self._setup_kernels() 67 | 68 | def _setup_kernels(self): 69 | for kernel in self.kernels: 70 | self.view.add_kernel_with_name(kernel, kernel.name) 71 | 72 | def set_model_from_view(self): 73 | # saves current kernel config to GUI memory 74 | self.view.replace_kernel_by_index(self._visible_kernel_config, self.last_kernel_index) 75 | 76 | # read all data from GUI memory 77 | kernels = self.view.get_all_kernels() 78 | self.kernels.clear() 79 | for kernel in kernels: 80 | self.kernels.append(kernel) 81 | 82 | @property 83 | def _visible_kernel_config(self): 84 | kernel = KernelModel() 85 | kernel.comment = self.view.comment 86 | kernel.projectile = Projectile("H") 87 | kernel.projectile.name = self.view.projectile_name 88 | kernel.projectile.iupac = self.view.projectile_symbol 89 | kernel.projectile.z = self.view.z 90 | kernel.projectile.a = self.view.a 91 | kernel.ddd_path = self.view.ddd_dir_path 92 | kernel.spc_path = self.view.spc_dir_path 93 | kernel.sis_path = self.view.sis_path 94 | kernel.name = self.view.kernel_name 95 | return kernel 96 | 97 | @_visible_kernel_config.getter 98 | def _visible_kernel_config(self): 99 | kernel = KernelModel() 100 | kernel.comment = self.view.comment 101 | kernel.projectile = Projectile("H") 102 | kernel.projectile.name = self.view.projectile_name 103 | kernel.projectile.iupac = self.view.projectile_symbol 104 | kernel.projectile.z = self.view.z 105 | kernel.projectile.a = self.view.a 106 | kernel.ddd_path = self.view.ddd_dir_path 107 | kernel.spc_path = self.view.spc_dir_path 108 | kernel.sis_path = self.view.sis_path 109 | kernel.name = self.view.kernel_name 110 | return kernel 111 | 112 | @_visible_kernel_config.setter 113 | def _visible_kernel_config(self, kernel): 114 | self.view.comment = kernel.comment 115 | if kernel.projectile is None: 116 | kernel.projectile = Projectile("H") 117 | self.view.projectile_name = kernel.projectile.name 118 | self.view.projectile_symbol = kernel.projectile.iupac 119 | self.view.z = kernel.projectile.z 120 | self.view.a = kernel.projectile.a 121 | self.view.ddd_dir_path = kernel.ddd_path 122 | self.view.spc_dir_path = kernel.spc_path 123 | self.view.sis_path = kernel.sis_path 124 | self.view.kernel_name = kernel.name 125 | -------------------------------------------------------------------------------- /pytripgui/loading_file_vc/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /pytripgui/loading_file_vc/loading_file_controller.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import logging 4 | from typing import Callable, Tuple 5 | 6 | from pytripgui.loading_file_vc.loading_file_view import LoadingFileView 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class LoadingFileController: 12 | def __init__(self, load_function: Callable, function_args: Tuple, parent=None, window_title=None, 13 | progress_message=None, finish_message=None): 14 | self._view = LoadingFileView(parent=parent, window_title=window_title, 15 | progress_message=progress_message, finish_message=finish_message) 16 | self._args = function_args 17 | self._load_function = load_function 18 | 19 | def start(self): 20 | self._view.show() 21 | # execute the loading function (e.g. opening DICOM) 22 | loaded = self._load_function(*self._args) 23 | if loaded: 24 | self._update_finished() 25 | else: 26 | # if the user canceled opening, instantly close the loading window without waiting for confirmation 27 | self._view.reject() 28 | 29 | def connect_finished(self, callback): 30 | self._view.connect_finished(callback) 31 | 32 | def _update_finished(self): 33 | self._view.update_finished() 34 | -------------------------------------------------------------------------------- /pytripgui/loading_file_vc/loading_file_view.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from PyQt5.QtWidgets import QApplication, QDialog 4 | from pytripgui.view.qt_gui import LoadingFileDialog 5 | 6 | 7 | class LoadingFileView: 8 | def __init__(self, parent=None, window_title=None, 9 | progress_message=None, finish_message=None): 10 | # define default text strings 11 | if not window_title: 12 | window_title = "Loading..." 13 | if not progress_message: 14 | progress_message = "Loading, please wait..." 15 | if not finish_message: 16 | finish_message = "Loading complete." 17 | 18 | self._ui: QDialog = LoadingFileDialog(parent) 19 | 20 | # set window text 21 | self.set_window_title(window_title) 22 | self.set_info_label_text(progress_message) 23 | self._finish_message = finish_message 24 | 25 | def set_info_label_text(self, text): 26 | self._ui.info_label.setText(text) 27 | 28 | def set_window_title(self, title): 29 | self._ui.setWindowTitle(title) 30 | 31 | def show(self): 32 | self._ui.show() 33 | # we need to process events to let UI init take effect 34 | QApplication.processEvents() 35 | 36 | def _ok_button_set_enabled(self, enabled): 37 | self._ui.ok_button.setEnabled(enabled) 38 | 39 | def update_finished(self): 40 | self.set_info_label_text(self._finish_message) 41 | self._ok_button_set_enabled(True) 42 | 43 | def connect_finished(self, callback): 44 | self._ui.finished.connect(callback) 45 | 46 | def reject(self): 47 | self._ui.reject() 48 | -------------------------------------------------------------------------------- /pytripgui/loading_file_vc/monophy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/pytripgui/loading_file_vc/monophy.gif -------------------------------------------------------------------------------- /pytripgui/main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import sys 4 | 5 | from PyQt5.QtGui import QIcon 6 | from PyQt5.QtWidgets import QApplication 7 | 8 | from pytripgui.main_window_qt_vc.main_window_view import MainWindowQtView 9 | from pytripgui.main_window_qt_vc.main_window_cont import MainWindowController 10 | from pytripgui.model.main_model import MainModel 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | def main(args=None): 16 | if args is None: 17 | args = sys.argv[1:] 18 | from pytripgui import __version__ as _ptgv 19 | from pytrip import __version__ as _ptv 20 | _vers = "PyTRiP98GUI {} using PyTRiP98 {}".format(_ptgv, _ptv) 21 | 22 | # setup parser 23 | parser = argparse.ArgumentParser() 24 | parser.add_argument('-v', '--verbosity', action='count', help="increase output verbosity", default=0) 25 | parser.add_argument('-V', '--version', action='version', version=_vers) 26 | parser.add_argument("--ctx", help="CtxCube", type=str, nargs='?') 27 | # parser.add_argument("--vdx", help="VdxCube", type=str, nargs='?') 28 | parser.add_argument("--dos", help="DosCube", type=str, nargs='?') 29 | parser.add_argument("--let", help="LETCube", type=str, nargs='?') 30 | parsed_args = parser.parse_args(sys.argv[1:]) 31 | 32 | # set logging level 33 | if parsed_args.verbosity == 1: 34 | logging.basicConfig(level=logging.INFO) 35 | elif parsed_args.verbosity > 1: 36 | logging.basicConfig(level=logging.DEBUG) 37 | else: 38 | logging.basicConfig() 39 | 40 | if parsed_args.verbosity <= 3: 41 | # set PyQt5 logging level to ERROR, in order not to pollute our log space 42 | logging.getLogger('PyQt5').setLevel(logging.ERROR) 43 | 44 | # all these objects need to be saved as variables, otherwise they will be garbage collected before app execution 45 | app = QApplication(sys.argv) 46 | view = MainWindowQtView() 47 | view.ui.setWindowIcon(QIcon('res/icon.ico')) 48 | model = MainModel() 49 | controller = MainWindowController(model, view) 50 | 51 | view.show() 52 | 53 | if controller: 54 | logger.debug("MainWindowController is active to serve its callbacks.") 55 | 56 | return app.exec_() 57 | 58 | 59 | if __name__ == '__main__': 60 | sys.exit(main(sys.argv[1:])) 61 | -------------------------------------------------------------------------------- /pytripgui/main_window_qt_vc/__init__.py: -------------------------------------------------------------------------------- 1 | from pytripgui.main_window_qt_vc.main_window_view import MainWindowQtView 2 | from pytripgui.main_window_qt_vc.main_window_cont import MainWindowController 3 | 4 | # from https://docs.python.org/3/tutorial/modules.html 5 | # if a package's __init__.py code defines a list named __all__, 6 | # it is taken to be the list of module names that should be imported when from package import * is encountered. 7 | __all__ = ['MainWindowQtView', 'MainWindowController'] 8 | -------------------------------------------------------------------------------- /pytripgui/main_window_qt_vc/main_window_cont.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pytripgui.app_logic.viewcanvas import ViewCanvases 4 | from pytripgui.loading_file_vc.loading_file_view import LoadingFileView 5 | from pytripgui.messages import InfoMessages 6 | 7 | from pytripgui.app_logic.patient_tree import PatientTree 8 | from pytripgui.app_logic.app_callbacks import AppCallback 9 | import sys 10 | import os 11 | 12 | from pytripgui.tree_vc.tree_items import PatientItem 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class MainWindowController: 18 | """ 19 | TODO: some description here 20 | """ 21 | def __init__(self, model, view): 22 | """ 23 | TODO: some description here 24 | """ 25 | self.model = model 26 | self.view = view 27 | 28 | self._initialize() 29 | 30 | def _initialize(self): 31 | """ 32 | TODO: some description here 33 | """ 34 | self.app_callback = AppCallback(self) 35 | 36 | self.model.patient_tree = PatientTree(self.view.ui) 37 | self.model.patient_tree.app_callback(self.app_callback) 38 | 39 | # main window callbacks 40 | self.view.open_voxelplan_callback = self.app_callback.on_open_voxelplan 41 | self.view.open_dicom_callback = self.app_callback.on_open_dicom 42 | self.view.import_dose_voxelplan_callback = self.app_callback.import_dose_voxelplan_callback 43 | self.view.import_dose_dicom_callback = self.app_callback.import_dose_dicom_callback 44 | # self.view.import_let_callback = self.app_callback.on_import_let 45 | self.view.open_kernels_configurator_callback = self.app_callback.on_kernels_configurator 46 | self.view.add_new_plan_callback = self.app_callback.on_add_new_plan 47 | self.view.trip_config_callback = self.app_callback.on_trip98_config 48 | self.view.action_add_patient = self.app_callback.on_add_patient 49 | self.view.action_add_vois = self.app_callback.on_add_vois 50 | self.view.action_create_field = self.app_callback.on_create_field 51 | self.view.action_execute_plan = self.app_callback.on_execute_selected_plan 52 | self.view.action_open_tree = self.app_callback.patient_tree_show 53 | 54 | self.view.about_callback = self.on_about 55 | self.view.exit_callback = self.on_exit 56 | 57 | def open_voxelplan(self, path): 58 | """ 59 | Opens Voxelplan 60 | :param path: path to .hed file 61 | :return: True if voxelplan is opened, False otherwise 62 | """ 63 | filename, _ = os.path.splitext(path) 64 | if not filename: 65 | return False 66 | 67 | patient = PatientItem() 68 | patient_data = patient.data 69 | try: 70 | patient_data.open_ctx(path) 71 | except FileNotFoundError as e: 72 | logger.error(str(e)) 73 | return False 74 | try: 75 | patient_data.open_vdx(filename + ".vdx") # Todo catch more exceptions 76 | except FileNotFoundError: 77 | logger.warning("Loaded patient has no VOI data") 78 | # TODO add empty vdx init if needed 79 | patient_data.vdx = None 80 | 81 | self._add_new_item(None, patient) 82 | return True 83 | 84 | def open_dicom(self, path): 85 | """ 86 | Opens Dicom 87 | :param path: path to dicom directory 88 | :return: True if dicom is opened, False otherwise 89 | """ 90 | if not path: 91 | return False 92 | 93 | patient = PatientItem() 94 | patient_data = patient.data 95 | patient_data.open_dicom(path) # Todo catch exceptions 96 | 97 | self._add_new_item(None, patient) 98 | return True 99 | 100 | def _add_new_item(self, item_list_parent, item): 101 | if not self.model.viewcanvases: 102 | self.model.viewcanvases = ViewCanvases(self.view.ui) 103 | self.view.add_widget(self.model.viewcanvases.widget()) 104 | 105 | self.model.patient_tree.add_new_item(item_list_parent, item) 106 | return True 107 | 108 | def on_about(self): 109 | """ 110 | Callback to display the "about" box. 111 | """ 112 | self.view.show_info(*InfoMessages["about"]) 113 | 114 | @staticmethod 115 | def on_exit(): 116 | sys.exit() 117 | -------------------------------------------------------------------------------- /pytripgui/messages.py: -------------------------------------------------------------------------------- 1 | from pytripgui import __version__ as pytripgui_version 2 | from pytrip import __version__ as pytrip_version 3 | 4 | about_txt_en = """PyTRipGUI Version: {} 5 | PyTRiP Version: {} 6 | (c) 2010 - 2021 PyTRiP98 Developers 7 | Niels Bassler 8 | Leszek Grzanka 9 | Toke Printz 10 | Łukasz Jeleń 11 | Arkadiusz Ćwikła 12 | Joanna Fortuna 13 | Michał Krawczyk 14 | Mateusz Łaszczyk 15 | """.format(pytripgui_version, pytrip_version) 16 | 17 | InfoMessages_en = { 18 | "about": ["PyTRiPGUI", about_txt_en], 19 | "addNewPatient": ["Add new Patient", "Before continue, you should create or import Patient"], 20 | "loadCtxVdx": ["Load CTX and VDX file", "Before continue, you should have loaded CTX and VDX data"], 21 | "addOneField": ["Add at least one field", "Before continue, you should add at least one field to selected plan"], 22 | "configureKernelList": 23 | ["Add at least one kernel", "Before continue, you should add at least one kernel in Settings/Beam Kernel"], 24 | "configureTrip": 25 | ["Configure trip98 settings", "Before continue, you should configure TRiP98 paths in Settings/TRiP98 Config"], 26 | "kernelSisPath": ["Given kernel has no SIS path", "Kernel selected by plan has no valid SIS path"], 27 | "noTargetRoiSelected": ["No target ROI selected", "Please select TargetROI in 'Target' tab"] 28 | } 29 | 30 | InfoMessages = InfoMessages_en 31 | -------------------------------------------------------------------------------- /pytripgui/model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/pytripgui/model/__init__.py -------------------------------------------------------------------------------- /pytripgui/model/main_model.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pytripgui.app_logic.patient_tree import PatientTree 4 | from pytripgui.app_logic.viewcanvas import ViewCanvases 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class MainModel: 10 | def __init__(self): 11 | 12 | from pytrip import __version__ as _pytrip_version 13 | from pytripgui import __version__ as _pytripgui_version 14 | 15 | self._pytrip_version = _pytrip_version 16 | self._pytripgui_version = _pytripgui_version 17 | 18 | self.trip_configs = [] 19 | self.kernels = [] # placeholder for KernelModels 20 | 21 | self.viewcanvases: ViewCanvases = None 22 | self.patient_tree: PatientTree = None 23 | self.settings = SettingsModel(self) 24 | 25 | 26 | class SettingsModel: 27 | """ 28 | This class contains a list model parameters which need to be retained when closing PyTRiPGUI. 29 | The attribute names must be identical to those in Model. 30 | Model attribute names with leading _ are saved, but not loaded. 31 | Model attribute names with leading __ are not saved and not loaded. 32 | """ 33 | def __init__(self, model): 34 | """ 35 | This object is pickled upon save and unpickled upon load. 36 | It is connected to Model, in a way that 37 | - upon SettingsController.load(), SettingsModel attributes starting with "_" are not written to Model. 38 | - upon SettingsController.save(), Model attributes starting with "__" are not written to SettingsModel. 39 | 40 | This way, 41 | a) _version is written to disk, but imported into Model when loading 42 | b) __internal_attribute__ are not passed between Model and SettingsModel 43 | """ 44 | self.trip_configs = model.trip_configs 45 | 46 | self.kernels = model.kernels 47 | 48 | self._pytrip_version = model._pytrip_version # saved, but not loaded 49 | self._pytripgui_version = model._pytripgui_version # saved, but not loaded 50 | -------------------------------------------------------------------------------- /pytripgui/plan_executor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/pytripgui/plan_executor/__init__.py -------------------------------------------------------------------------------- /pytripgui/plan_executor/executor.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import logging 3 | 4 | import pytrip.tripexecuter as pte 5 | from pytripgui.plan_executor.simulation_results import SimulationResults 6 | import sys 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class PlanExecutor: 12 | def __init__(self, trip_config, logger=None): 13 | self.trip_config = trip_config 14 | self.logger = logger 15 | 16 | def check_config(self): 17 | # TODO replace with function that actually runs trip, and collect returned errors 18 | if self.trip_config.wdir_path == "": 19 | return -1 20 | if self.trip_config.trip_path == "": 21 | return -1 22 | return 0 23 | 24 | def execute(self, patient, plan): 25 | plan = copy.deepcopy(plan) 26 | 27 | plan.working_dir = self.trip_config.wdir_path 28 | plan.dedx_path = self.trip_config.dedx_path 29 | plan.hlut_path = self.trip_config.hlut_path 30 | 31 | te = pte.Execute(patient.ctx, patient.vdx) 32 | if self.trip_config.remote_execution: 33 | te.remote = True 34 | te.servername = self.trip_config.host_name 35 | te.username = self.trip_config.user_name 36 | te.password = self.trip_config.password 37 | te.pkey_path = self.trip_config.pkey_path 38 | te.remote_base_dir = self.trip_config.wdir_remote_path + '/' 39 | 40 | te.trip_bin_path = self.trip_config.trip_path + '/' + 'TRiP98' 41 | 42 | if self.logger: 43 | te.add_executor_logger(self.logger) 44 | 45 | try: 46 | te.execute(plan, use_default_logger=False) 47 | except BaseException as e: 48 | logger.error(e.__str__()) 49 | sys.exit(-1) 50 | 51 | results = SimulationResults(patient, plan, plan.basename) 52 | 53 | return results 54 | -------------------------------------------------------------------------------- /pytripgui/plan_executor/html_executor_logger.py: -------------------------------------------------------------------------------- 1 | 2 | from queue import Queue 3 | from pytrip.tripexecuter import ExecutorLogger 4 | 5 | 6 | class HtmlExecutorLogger(ExecutorLogger): 7 | def __init__(self): 8 | self._queue = Queue() 9 | 10 | self._info_tag = "{}" # bold 11 | self._red_font = "{}" 12 | self._error_tag = self._red_font.format("{}") # red and bold 13 | 14 | def info(self, text): 15 | self._queue.put(self._info_tag.format(text)) 16 | 17 | def log(self, text): 18 | text = self._format_tags(text) 19 | text = self._format_ansi(text) 20 | text = self._format_colors(text) 21 | self._queue.put(text) 22 | 23 | def error(self, text): 24 | self._queue.put(self._error_tag.format(text)) 25 | 26 | def empty(self): 27 | return self._queue.empty() 28 | 29 | def get(self): 30 | return self._queue.get(block=False) 31 | 32 | def _format_tags(self, text): 33 | """ Changes '<' and '>' in tags to html entities 34 | """ 35 | text = text.replace("", "<E>") # error 36 | text = text.replace("", "<SYS>") # system 37 | text = text.replace("", "<I>") # info 38 | text = text.replace("", "<D>") # debug 39 | return text 40 | 41 | def _format_colors(self, text): 42 | """ Changes lines with or to red color 43 | """ 44 | if "<E>" in text or "<SYS>" in text: 45 | text = self._red_font.format(text) 46 | return text 47 | 48 | def _format_ansi(self, text): 49 | """ Remove ansi escape sequences 50 | """ 51 | import re 52 | return re.sub(r"\033\[[0-9]+m", "", text) 53 | -------------------------------------------------------------------------------- /pytripgui/plan_executor/patient_model.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pytrip as pt 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | class PatientModel: 9 | def __init__(self): 10 | self.name = "Patient" 11 | self.ctx = None 12 | self.vdx = None 13 | self.dcm = None 14 | 15 | self.plans = [] 16 | 17 | def open_ctx(self, path): 18 | ctx = pt.CtxCube() 19 | ctx.read(path) 20 | self.ctx = ctx 21 | self.name = ctx.basename 22 | 23 | def open_vdx(self, path): 24 | vdx = pt.VdxCube(self.ctx) 25 | vdx.read(path) 26 | self.vdx = vdx 27 | if self.name != vdx.basename: 28 | logger.error("CTX | VDX patient name not match") 29 | 30 | def open_dicom(self, path): 31 | self.dcm = pt.dicomhelper.read_dicom_dir(path) 32 | 33 | if 'images' in self.dcm: 34 | self.ctx = pt.CtxCube() 35 | self.ctx.read_dicom(self.dcm) 36 | self.name = self.ctx.basename 37 | 38 | if 'rtss' in self.dcm: 39 | self.vdx = pt.VdxCube(self.ctx) 40 | self.vdx.read_dicom(self.dcm) 41 | self.name = self.vdx.basename 42 | -------------------------------------------------------------------------------- /pytripgui/plan_executor/simulation_results.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pytrip import volhist, DosCube, LETCube 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | class SimulationResults: 9 | def __init__(self, patient, plan, name): 10 | self.patient = patient 11 | self.plan = plan 12 | self.name = name 13 | self.volume_histograms = {} 14 | 15 | self._compute_target_dvh() 16 | self._compute_target_lvh() 17 | 18 | def get_doses(self): 19 | return self.plan.dosecubes 20 | 21 | def get_dose(self, dose_type): 22 | if dose_type not in DosCube.allowed_suffix: 23 | raise ValueError("Wrong dose type") 24 | 25 | for dose in self.plan.dosecubes: 26 | if dose.basename.endswith("." + dose_type): 27 | return dose 28 | return None 29 | 30 | def get_lets(self): 31 | return self.plan.letcubes 32 | 33 | def import_dos(self, dos_path): 34 | logger.debug("Open DosCube {:s}".format(dos_path)) 35 | dos = DosCube() 36 | dos.read(dos_path) 37 | self.plan.dosecubes.append(dos) 38 | 39 | def import_let(self, let_path): 40 | logger.debug("Open LETCube {:s}".format(let_path)) 41 | let = LETCube() 42 | let.read(let_path) 43 | self.plan.letcubes.append(let) 44 | 45 | def get_let(self, let_type): 46 | if let_type not in LETCube.allowed_suffix: 47 | raise ValueError("Wrong LET type") 48 | 49 | for let in self.plan.letcubes: 50 | if let.basename.endswith("." + let_type): 51 | return let 52 | return None 53 | 54 | def _compute_target_dvh(self): 55 | for dose in self.plan.dosecubes: 56 | dose_type = dose.basename.split(".")[-1] 57 | target_name = self.plan.voi_target.name 58 | dvh = {target_name: volhist.VolHist(dose, self.patient.vdx.get_voi_by_name(target_name))} 59 | self.volume_histograms["DVH " + dose_type] = dvh 60 | 61 | def _compute_target_lvh(self): 62 | for let in self.plan.letcubes: 63 | let_type = let.basename.split(".")[-1] 64 | target_name = self.plan.voi_target.name 65 | lvh = {target_name: volhist.VolHist(let, self.patient.vdx.get_voi_by_name(target_name))} 66 | self.volume_histograms["LVH " + let_type] = lvh 67 | 68 | def __str__(self): 69 | return "Sim: " + self.name 70 | -------------------------------------------------------------------------------- /pytripgui/plan_executor/threaded_executor.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from queue import Queue 3 | from pytripgui.plan_executor.executor import PlanExecutor 4 | from pytripgui.plan_executor.html_executor_logger import HtmlExecutorLogger 5 | 6 | 7 | class ThreadedExecutor(Thread): 8 | def __init__(self, plan, patient, trip_config): 9 | super().__init__() 10 | 11 | self.logger = HtmlExecutorLogger() 12 | self.item_queue = Queue() 13 | 14 | self.patient = patient 15 | self.plan = plan 16 | self.trip_config = trip_config 17 | 18 | def run(self): 19 | self.on_execute_selected_plan_threaded() 20 | 21 | def on_execute_selected_plan_threaded(self): 22 | sim_results = self._execute_plan(self.plan, self.patient) 23 | if sim_results: 24 | self.item_queue.put(sim_results) 25 | 26 | def _execute_plan(self, plan, patient): 27 | plan_executor = PlanExecutor(self.trip_config, self.logger) 28 | item = plan_executor.execute(patient.data, plan.data) 29 | return item 30 | -------------------------------------------------------------------------------- /pytripgui/plan_executor/trip_config.py: -------------------------------------------------------------------------------- 1 | class Trip98ConfigModel: 2 | def __init__(self): 3 | self.name = "" 4 | self.remote_execution = False 5 | self.hlut_path = "" 6 | self.dedx_path = "" 7 | self.wdir_path = "" 8 | self.trip_path = "" 9 | 10 | # remote execution 11 | self.host_name = "" 12 | self.user_name = "" 13 | self.pkey_path = "" 14 | self.password = "" 15 | self.wdir_remote_path = "" 16 | -------------------------------------------------------------------------------- /pytripgui/plan_vc/__init__.py: -------------------------------------------------------------------------------- 1 | from pytripgui.plan_vc.plan_view import PlanQtView 2 | from pytripgui.plan_vc.plan_cont import PlanController 3 | 4 | # from https://docs.python.org/3/tutorial/modules.html 5 | # if a package's __init__.py code defines a list named __all__, 6 | # it is taken to be the list of module names that should be imported when from package import * is encountered. 7 | __all__ = ['PlanQtView', 'PlanController'] 8 | -------------------------------------------------------------------------------- /pytripgui/res/LICENSE.rst: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of PyTRiPGUI. 3 | 4 | PyTRiPGUI is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | PyTRiPGUI is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with PyTRiPGUI. If not, see 16 | """ 17 | 18 | --- LICENSE --- 19 | PyTRiPGUI is licensed under GPL version 3 20 | (c) Copyright 2014 by 21 | Jakob Toftegaard, 22 | Niels Bassler, 23 | for the Aarhus Particle Therapy Group 24 | http://www.phys.au.dk/aptg 25 | 26 | This program is free software: you can redistribute it and/or modify 27 | it under the terms of the GNU General Public License as published by 28 | the Free Software Foundation, either version 3 of the License, or 29 | (at your option) any later version. 30 | 31 | This program is distributed in the hope that it will be useful, 32 | but WITHOUT ANY WARRANTY; without even the implied warranty of 33 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 34 | GNU General Public License for more details. 35 | 36 | You should have received a copy of the GNU General Public License 37 | along with this program. If not, see . 38 | -------------------------------------------------------------------------------- /pytripgui/res/add_patient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/pytripgui/res/add_patient.png -------------------------------------------------------------------------------- /pytripgui/res/add_vois.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/pytripgui/res/add_vois.png -------------------------------------------------------------------------------- /pytripgui/res/create_field.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/pytripgui/res/create_field.png -------------------------------------------------------------------------------- /pytripgui/res/create_plan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/pytripgui/res/create_plan.png -------------------------------------------------------------------------------- /pytripgui/res/execute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/pytripgui/res/execute.png -------------------------------------------------------------------------------- /pytripgui/res/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/pytripgui/res/icon.ico -------------------------------------------------------------------------------- /pytripgui/tree_vc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/pytripgui/tree_vc/__init__.py -------------------------------------------------------------------------------- /pytripgui/tree_vc/tree_controller.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | 5 | 6 | class TreeController: 7 | def __init__(self, model, view): 8 | """ 9 | edit_item_callback is called when user wants to add new, or edit existing item: edit_item(item_to_edit) 10 | Callback function should return False if user canceled operation, or True if approved 11 | """ 12 | self.new_item_callback = None 13 | self.edit_item_callback = None 14 | self.open_voxelplan_callback = None 15 | self.export_voxelplan_callback = None 16 | self.export_patient_voxelplan_callback = None 17 | self.export_patient_dicom_callback = None 18 | self.execute_plan_callback = None 19 | self.one_click_callback = None 20 | self.export_dose_voxelplan_callback = None 21 | self.export_dose_dicom_callback = None 22 | self.export_plan_callback = None 23 | self.import_dose_voxelplan_callback = None 24 | self.import_dose_dicom_callback = None 25 | 26 | # internal 27 | self._tree_model = model 28 | self._view = view 29 | self._view.internal_events.on_add_child += self._add_new_item_callback 30 | self._view.internal_events.on_edit_selected_item += self._edit_selected_item_callback 31 | self._view.internal_events.on_open_voxelplan += self._open_voxelplan_callback 32 | self._view.internal_events.on_export_patient_voxelplan += self._export_patient_voxelplan_callback 33 | self._view.internal_events.on_export_patient_dicom += self._export_patient_dicom_callback 34 | self._view.internal_events.on_delete += self._delete_callback 35 | self._view.internal_events.on_execute += self._execute_callback 36 | self._view.internal_events.on_click += self._on_click_callback 37 | self._view.internal_events.on_export_dose_voxelplan += self._export_dose_voxelplan_callback 38 | self._view.internal_events.on_export_dose_dicom += self._export_dose_dicom_callback 39 | self._view.internal_events.on_export_plan += self._export_plan_callback 40 | self._view.internal_events.on_import_dose_voxelplan += self._import_dose_voxelplan_callback 41 | self._view.internal_events.on_import_dose_dicom += self._import_dose_dicom_callback 42 | 43 | def _delete_callback(self): 44 | parent = self._view.selected_q_item.parent() 45 | self._tree_model.delete_item(self._view.selected_q_item) 46 | self._view.select_element(parent) 47 | 48 | def _add_new_item_callback(self): 49 | if self.new_item_callback: 50 | self.new_item_callback(self._view.selected_item) 51 | 52 | def add_new_item(self, parent_item, item): 53 | if parent_item: 54 | last_row = parent_item.row_count() 55 | else: 56 | last_row = 0 57 | 58 | if item: 59 | new_q_item = self._tree_model.insertRows(last_row, 1, parent_item, item) 60 | self._view.select_element(new_q_item) 61 | 62 | def _edit_selected_item_callback(self): 63 | self.edit_item_callback(self._view.selected_item, self._view.selected_item_patient) 64 | 65 | def _open_voxelplan_callback(self): 66 | self.open_voxelplan_callback() 67 | 68 | def _export_voxelplan_callback(self): 69 | self.export_voxelplan_callback(self._view.selected_item) 70 | 71 | def _export_patient_voxelplan_callback(self): 72 | self.export_patient_voxelplan_callback(self._view.selected_item) 73 | 74 | def _export_patient_dicom_callback(self): 75 | self.export_patient_dicom_callback(self._view.selected_item) 76 | 77 | def _execute_callback(self): 78 | if self.execute_plan_callback: 79 | self.execute_plan_callback() 80 | 81 | def _on_click_callback(self): 82 | self.one_click_callback() 83 | 84 | def _export_dose_voxelplan_callback(self): 85 | self.export_dose_voxelplan_callback(self._view.selected_item) 86 | 87 | def _export_dose_dicom_callback(self): 88 | self.export_dose_dicom_callback(self._view.selected_item) 89 | 90 | def _export_plan_callback(self): 91 | self.export_plan_callback(self._view.selected_item) 92 | 93 | def _import_dose_voxelplan_callback(self): 94 | self.import_dose_voxelplan_callback() 95 | 96 | def _import_dose_dicom_callback(self): 97 | self.import_dose_dicom_callback() 98 | -------------------------------------------------------------------------------- /pytripgui/tree_vc/tree_model.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from PyQt5.QtCore import QVariant, QModelIndex, Qt 4 | from PyQt5.QtCore import QAbstractItemModel 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class TreeModel(QAbstractItemModel): 10 | def __init__(self, root_item): 11 | super().__init__(None) 12 | 13 | self._root_item = root_item 14 | 15 | def headerData(self, p_int, qt_orientation, role=None): 16 | if p_int > 0: 17 | return QVariant() 18 | 19 | if role == Qt.DisplayRole: 20 | return QVariant("Patients: ") 21 | 22 | return QVariant() 23 | 24 | def columnCount(self, parent=None, *args, **kwargs): 25 | return 1 # Only one column is supported 26 | 27 | def rowCount(self, parent=None, *args, **kwargs): 28 | if not parent or not parent.isValid(): 29 | logger.debug("rowCount() for: {}".format("root")) 30 | return self._root_item.row_count() 31 | name = parent.internalPointer().__repr__() 32 | logger.debug("rowCount() for: {}".format(name)) 33 | return parent.internalPointer().row_count() 34 | 35 | def hasChildren(self, parent=None, *args, **kwargs): 36 | if not parent or not parent.isValid(): 37 | logger.debug("hasChildren() for: {}".format("root")) 38 | return self._root_item.has_children() 39 | name = parent.internalPointer().__repr__() 40 | has_children = parent.internalPointer().has_children() 41 | logger.debug("hasChildren() for: {} returns: {}".format(name, has_children)) 42 | return has_children 43 | 44 | def index(self, p_int, p_int_1, parent=None, *args, **kwargs): 45 | logger.debug("index() {} {}".format(p_int, p_int_1)) 46 | 47 | if not self.hasIndex(p_int, p_int_1, parent) or \ 48 | parent is None: 49 | return QModelIndex() 50 | 51 | if not parent.isValid(): 52 | return self._create_index(self._root_item, p_int, p_int_1) 53 | return self._create_index(parent.internalPointer(), p_int, p_int_1) 54 | 55 | def hasIndex(self, p_int, p_int_1, parent=None, *args, **kwargs): 56 | if p_int_1 != 0: 57 | return False # current implementation supports one column 58 | 59 | if not parent.isValid(): 60 | logger.debug("hasIndex() for: {}:{}:{} returns: {}".format("root", p_int, p_int_1, True)) 61 | return True 62 | name = parent.internalPointer().__repr__() 63 | has_index = parent.internalPointer().has_index(p_int) 64 | logger.debug("hasIndex() for: {}:{}:{} returns: {}".format(name, p_int, p_int_1, has_index)) 65 | return has_index 66 | 67 | def _create_index(self, parent, p_int, p_int_1): 68 | name = parent.__repr__() 69 | logger.debug("_create_index() returns: {}".format(name)) 70 | 71 | selected_item = parent.index(p_int, p_int_1) 72 | if selected_item: 73 | return self.createIndex(p_int, p_int_1, selected_item) 74 | return QModelIndex() 75 | 76 | def delete_item(self, q_item): 77 | parent = q_item.parent() 78 | parent_item = parent.internalPointer() 79 | row = q_item.row() 80 | 81 | self.beginRemoveRows(parent, row, row) 82 | parent_item.delete_child(q_item.internalPointer()) 83 | self.endRemoveRows() 84 | 85 | def data(self, q_model_index, role=None): 86 | if role == Qt.DisplayRole: 87 | return q_model_index.internalPointer().__repr__() 88 | 89 | def parent(self, q_child_item=None): 90 | if not q_child_item.isValid(): 91 | logger.debug("q_model_index is invalid") 92 | return QModelIndex() 93 | 94 | if q_child_item.internalPointer() is None: 95 | logger.error("No internal pointer") 96 | return QModelIndex() 97 | 98 | child_item = q_child_item.internalPointer() 99 | 100 | if child_item == self._root_item: 101 | logger.debug("parent() - root item has't got a parent") 102 | return QModelIndex() 103 | 104 | parent_item = child_item.parent 105 | if parent_item is None: 106 | return QModelIndex() 107 | 108 | return self.createIndex(parent_item.row(), 0, parent_item) 109 | 110 | def insertRows(self, row, count, parent_item=None, child=None): 111 | if count != 1: 112 | raise Exception("Only one row at one time") 113 | 114 | if parent_item: 115 | parent = self.createIndex(parent_item.row(), 0, parent_item) 116 | else: 117 | parent_item = self._root_item 118 | parent = QModelIndex() 119 | 120 | self.beginInsertRows(parent, row, row + count - 1) 121 | parent_item.add_child(child) 122 | self.endInsertRows() 123 | 124 | return self.createIndex(child.row(), 0, child) 125 | -------------------------------------------------------------------------------- /pytripgui/util.py: -------------------------------------------------------------------------------- 1 | """ 2 | Functions of universal character. 3 | """ 4 | import logging 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | def main_dir(): 10 | """ 11 | Returns base dir of pytrip. 12 | """ 13 | import os 14 | import sys 15 | if getattr(sys, 'frozen', False): 16 | return os.environ.get("_MEIPASS2", os.path.abspath(".")) 17 | # when using single directory installer, this one should be probably used: 18 | # return os.path.dirname(sys.executable) 19 | return os.path.dirname(__file__) 20 | -------------------------------------------------------------------------------- /pytripgui/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/pytripgui/utils/__init__.py -------------------------------------------------------------------------------- /pytripgui/utils/regex.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from PyQt5.QtCore import QRegularExpression 4 | 5 | 6 | class Regex(Enum): 7 | STRING = QRegularExpression(r"\w+[\s\w]*") 8 | INT = QRegularExpression(r"-?\d*") 9 | INT_POSITIVE = QRegularExpression(r"\d*[1-9]\d*") 10 | FLOAT = QRegularExpression(r"-?((\d+([,\.]\d{0,3})?)|(\d*[,\.]\d{1,3}))") 11 | FLOAT_POSITIVE = QRegularExpression(r"(\d*[1-9]\d*([,\.]\d{0,3})?)|(\d*[,\.](?=\d{1,3}$)(\d*[1-9]\d*))") 12 | FLOAT_UNSIGNED = QRegularExpression(r"0+|((\d*[1-9]\d*([,\.]\d{0,3})?)|(\d*[,\.](?=\d{1,3}$)(\d*[1-9]\d*)))") 13 | -------------------------------------------------------------------------------- /pytripgui/version.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | 5 | def git_version(): 6 | """ 7 | Inspired by https://github.com/numpy/numpy/blob/master/setup.py 8 | :return: the git revision as a string 9 | """ 10 | def _minimal_ext_cmd(cmd): 11 | # construct minimal environment 12 | env = {} 13 | for k in ['SYSTEMROOT', 'PATH', 'HOME']: 14 | v = os.environ.get(k) 15 | if v is not None: 16 | env[k] = v 17 | # LANGUAGE is used on win32 18 | env['LANGUAGE'] = 'C' 19 | env['LANG'] = 'C' 20 | env['LC_ALL'] = 'C' 21 | out = subprocess.Popen(cmd, stdout=subprocess.PIPE, env=env).communicate()[0] 22 | return out 23 | 24 | try: 25 | out = _minimal_ext_cmd(['git', 'describe', '--tags', '--long']) 26 | GIT_REVISION = out.strip().decode('ascii') 27 | if GIT_REVISION: 28 | no_of_commits_since_last_tag = int(GIT_REVISION.split('-')[1]) 29 | tag_name = GIT_REVISION.split('-')[0][1:] 30 | if no_of_commits_since_last_tag == 0: 31 | version = tag_name 32 | else: 33 | version = '{}+rev{}'.format(tag_name, no_of_commits_since_last_tag) 34 | else: 35 | version = "Unknown" 36 | except OSError: 37 | version = "Unknown" 38 | 39 | return version 40 | -------------------------------------------------------------------------------- /pytripgui/view/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/pytripgui/view/__init__.py -------------------------------------------------------------------------------- /pytripgui/view/add_patient.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | NewPatientDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 240 10 | 155 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | 21 | 0 22 | 0 23 | 24 | 25 | 26 | 27 | 16777215 28 | 538 29 | 30 | 31 | 32 | New patient 33 | 34 | 35 | 36 | 37 | 38 | Create empty patient 39 | 40 | 41 | 42 | 43 | 44 | 45 | Open Voxelplan 46 | 47 | 48 | 49 | 50 | 51 | 52 | true 53 | 54 | 55 | Open Dicom 56 | 57 | 58 | 59 | 60 | 61 | 62 | QDialogButtonBox::Cancel 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /pytripgui/view/add_single_voi.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | addVOI 4 | 5 | 6 | 7 | 0 8 | 0 9 | 660 10 | 200 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | Qt::NoFocus 21 | 22 | 23 | Add VOI 24 | 25 | 26 | false 27 | 28 | 29 | true 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Spherical 39 | 40 | 41 | 42 | 43 | Cuboidal 44 | 45 | 46 | 47 | 48 | Cylindrical 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | Qt::Horizontal 57 | 58 | 59 | 60 | 40 61 | 20 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 0 78 | 0 79 | 80 | 81 | 82 | 83 | 16777215 84 | 20 85 | 86 | 87 | 88 | color : red; 89 | 90 | 91 | <html><head/><body><p><br/></p></body></html> 92 | 93 | 94 | 95 | 96 | 97 | 98 | Qt::Horizontal 99 | 100 | 101 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | accept_buttonBox 113 | accepted() 114 | addVOI 115 | accept() 116 | 117 | 118 | 248 119 | 254 120 | 121 | 122 | 157 123 | 274 124 | 125 | 126 | 127 | 128 | accept_buttonBox 129 | rejected() 130 | addVOI 131 | reject() 132 | 133 | 134 | 316 135 | 260 136 | 137 | 138 | 286 139 | 274 140 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /pytripgui/view/chart_widget.py: -------------------------------------------------------------------------------- 1 | # from PyQt5 import QtChart 2 | from PyQt5.QtChart import QChart, QChartView, QLineSeries 3 | from PyQt5.QtGui import QPainter 4 | from PyQt5.QtCore import Qt 5 | 6 | 7 | class ChartWidget: 8 | def __init__(self): 9 | self.model = QChart() 10 | self.view = QChartView() 11 | 12 | self.view.setChart(self.model) 13 | 14 | self.model.legend().setAlignment(Qt.AlignBottom) 15 | self.view.setRenderHint(QPainter.Antialiasing) 16 | 17 | def show(self): 18 | self.view.show() 19 | 20 | def exit(self): 21 | self.view.close() 22 | 23 | @property 24 | def title(self): 25 | return self.model.title() 26 | 27 | @title.setter 28 | def title(self, title): 29 | self.model.setTitle(title) 30 | 31 | def add_series(self, x_list, y_list, name): 32 | series = QLineSeries() 33 | series.setName(name) 34 | 35 | for x, y in zip(x_list, y_list): 36 | series.append(x, y) 37 | 38 | self.model.addSeries(series) 39 | self.model.createDefaultAxes() 40 | 41 | # axis = QValueAxis() 42 | # axis.setTitleText("Dose") 43 | # 44 | # self.model.setAxisX(axis, series) 45 | # self.model.axisY(series).setTitleText("Dose") 46 | -------------------------------------------------------------------------------- /pytripgui/view/contouring_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 300 10 | 150 11 | 12 | 13 | 14 | Precalculate VOI contours 15 | 16 | 17 | false 18 | 19 | 20 | 21 | 22 | 20 23 | 20 24 | 260 25 | 40 26 | 27 | 28 | 29 | 30 | 75 31 | true 32 | 33 | 34 | 35 | Would you like to precalculate VOI contrours? 36 | 37 | 38 | Qt::AlignCenter 39 | 40 | 41 | true 42 | 43 | 44 | 45 | 46 | true 47 | 48 | 49 | 50 | 50 51 | 100 52 | 200 53 | 40 54 | 55 | 56 | 57 | QDialogButtonBox::No|QDialogButtonBox::Yes 58 | 59 | 60 | true 61 | 62 | 63 | 64 | 65 | 66 | 30 67 | 60 68 | 240 69 | 30 70 | 71 | 72 | 73 | 74 | false 75 | false 76 | 77 | 78 | 79 | This may take a few minutes, but it will speed up viewing the patient anatomy. 80 | 81 | 82 | Qt::AlignCenter 83 | 84 | 85 | true 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /pytripgui/view/data_sample.py: -------------------------------------------------------------------------------- 1 | import math 2 | from enum import Enum 3 | from pathlib import Path 4 | from matplotlib.axes import Axes 5 | from matplotlib.backend_bases import MouseEvent 6 | from numpy import ndarray 7 | 8 | from PyQt5 import QtWidgets, uic 9 | 10 | from pytrip import CtxCube 11 | from pytripgui.view.qt_gui import UiMainWindow 12 | 13 | 14 | class DataSample(QtWidgets.QWidget): 15 | def __init__(self, parent: UiMainWindow, axes: Axes): 16 | super().__init__(parent) 17 | ui_path = Path(Path(__file__).parent, "data_sample.ui").resolve() 18 | uic.loadUi(ui_path, self) 19 | 20 | self.axes: Axes = axes 21 | 22 | self.mode: DataSample.Mode = self.Mode.ctx 23 | self.perspective: DataSample.Perspective = self.Perspective.transversal 24 | self.data: ndarray = ndarray(shape=()) 25 | self.cube: CtxCube = CtxCube() 26 | self.x_offset: float = 0 27 | self.y_offset: float = 0 28 | self.z_offset: float = 0 29 | self.slice_no: int = 0 30 | 31 | self.position_widget.hide() 32 | self.doselet_widget.hide() 33 | 34 | def update_perspective(self, perspective_name: str) -> None: 35 | self.perspective = self.Perspective(perspective_name) 36 | 37 | if self.perspective == self.Perspective.transversal: 38 | self.x_offset = self.cube.xoffset 39 | self.y_offset = self.cube.yoffset 40 | elif self.perspective == self.Perspective.sagittal: 41 | self.x_offset = self.cube.yoffset 42 | self.y_offset = self.cube.zoffset 43 | else: 44 | self.x_offset = self.cube.xoffset 45 | self.y_offset = self.cube.zoffset 46 | 47 | def update_sample(self, event: MouseEvent) -> None: 48 | if event.xdata and event.inaxes == self.axes: 49 | self.update_position(event.xdata, event.ydata) 50 | self.position_widget.show() 51 | 52 | if self.mode != self.Mode.ctx: 53 | self.update_doselet(event.xdata, event.ydata) 54 | self.doselet_widget.show() 55 | else: 56 | self.position_widget.hide() 57 | self.doselet_widget.hide() 58 | 59 | def update_position(self, xdata: float, ydata: float) -> None: 60 | if self.perspective == self.Perspective.transversal: 61 | x_position = xdata 62 | y_position = ydata 63 | z_position = self.slice_no * self.cube.slice_distance 64 | elif self.perspective == self.Perspective.sagittal: 65 | x_position = self.slice_no * self.cube.pixel_size 66 | y_position = xdata 67 | z_position = ydata 68 | else: 69 | x_position = xdata 70 | y_position = self.slice_no * self.cube.pixel_size 71 | z_position = ydata 72 | 73 | x_position = "{:.2f}".format(round(x_position, 2)) 74 | y_position = "{:.2f}".format(round(y_position, 2)) 75 | z_position = "{:.2f}".format(round(z_position, 2)) 76 | 77 | self.xPosition_label.setText("(" + x_position + ",") 78 | self.yPosition_label.setText(y_position + ",") 79 | self.zPosition_label.setText(z_position + ")") 80 | 81 | def update_doselet(self, xdata: float, ydata: float) -> None: 82 | x = math.floor((xdata - self.x_offset) / self.cube.pixel_size) 83 | if self.perspective == self.Perspective.transversal: 84 | y = math.floor((ydata - self.y_offset) / self.cube.pixel_size) 85 | else: 86 | y = math.floor((ydata - self.y_offset) / self.cube.slice_distance) 87 | 88 | self.doseletData_label.setText(str(self.data[y][x])) 89 | 90 | def update_cube(self, cube: CtxCube) -> None: 91 | # update cube when changing the patient 92 | self.cube = cube 93 | 94 | def update_mode(self, mode_name: str) -> None: 95 | self.mode = self.Mode(mode_name) 96 | 97 | if self.mode == self.Mode.dose: 98 | self.doseletDescription_label.setText("Dose:") 99 | self.doseletUnit_label.setText("%") 100 | elif self.mode == self.Mode.let: 101 | self.doseletDescription_label.setText("LET:") 102 | self.doseletUnit_label.setText("keV / µm") 103 | 104 | def update_slice_no(self, slice_no: int) -> None: 105 | self.slice_no = slice_no 106 | 107 | def update_doselet_data(self, data: ndarray) -> None: 108 | self.data = data 109 | 110 | class Perspective(Enum): 111 | transversal = "Transversal" 112 | sagittal = "Sagittal" 113 | coronal = "Coronal" 114 | 115 | class Mode(Enum): 116 | ctx = "Ctx" 117 | dose = "Dose" 118 | let = "Let" 119 | -------------------------------------------------------------------------------- /pytripgui/view/dialogs.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from PyQt5.QtWidgets import QFileDialog 4 | from PyQt5.QtWidgets import QMessageBox 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class MyDialogs: 10 | """ 11 | Class for holding dialogs using along with PyTRiP 12 | """ 13 | def __init__(self): 14 | pass 15 | 16 | @staticmethod 17 | def show_error(text="Unspecified Error"): 18 | msg = QMessageBox() 19 | msg.setIcon(QMessageBox.Critical) 20 | msg.setText("Error") 21 | msg.setInformativeText(text) 22 | msg.setWindowTitle("Error") 23 | msg.exec_() 24 | 25 | @staticmethod 26 | def _filter(ftype): 27 | """ 28 | Internal function to generate text string for setting up default files. 29 | """ 30 | if ftype == "*": 31 | return "AllFiles (*)" 32 | if ftype == "dicom": 33 | return "Dicom Files (*.dcm, *.ima)" # TODO check if needed, and correct, probably only dirs are loaded. 34 | if ftype == "ctx": 35 | return "CtxCube Files (*.ctx)" 36 | if ftype == "hed": 37 | return "Header Files (*.hed)" 38 | if ftype == "dos": 39 | return "DosCube Files (*.dos)" 40 | if ftype == "let": 41 | return "LETCube Files (*.dosemlet.dos)" # TODO use suffix from pytrip.something module. 42 | return "AllFiles (*)" 43 | 44 | @staticmethod 45 | def openDirectoryDialog(app, title="", ddir=""): 46 | """ 47 | :params dir str: default where to look for file 48 | """ 49 | 50 | options = QFileDialog.Options() 51 | options |= QFileDialog.ShowDirsOnly 52 | options |= QFileDialog.DontResolveSymlinks 53 | options |= QFileDialog.DontUseNativeDialog 54 | 55 | filename = QFileDialog.getExistingDirectory(app, title, ddir, options=options) 56 | 57 | if filename: 58 | logger.debug(filename) 59 | return filename 60 | 61 | options = QFileDialog.Options() 62 | 63 | @staticmethod 64 | def openFileNameDialog(app, title="", ddir="", ftype=""): 65 | """ 66 | :params path str: default where to look for file 67 | :params type str: default suffix to look for 68 | """ 69 | 70 | filters = MyDialogs._filter(ftype) 71 | 72 | options = QFileDialog.Options() 73 | options |= QFileDialog.DontUseNativeDialog 74 | fileName, _ = QFileDialog.getOpenFileName(app, title, ddir, filters, options=options) 75 | if fileName: 76 | logger.debug(fileName) 77 | return fileName 78 | 79 | @staticmethod 80 | def saveFileNameDialog(app, title="", ddir="", ftype=""): 81 | """ 82 | :params path str: default where to look for file 83 | :params type str: default suffix to look for 84 | """ 85 | 86 | filters = MyDialogs._filter(ftype) 87 | 88 | options = QFileDialog.Options() 89 | options |= QFileDialog.DontUseNativeDialog 90 | fileName, _ = QFileDialog.getSaveFileName(app, title, ddir, filters, options=options) 91 | if fileName: 92 | logger.debug(fileName) 93 | return fileName 94 | 95 | @staticmethod 96 | def saveDirectoryDialog(app, title="", ddir=""): 97 | """ 98 | :params title str: title for dialog 99 | :params ddir str: default dir 100 | """ 101 | 102 | options = QFileDialog.Options() 103 | options |= QFileDialog.DontUseNativeDialog 104 | ddir = QFileDialog.getExistingDirectory(app, title, ddir, options=options) 105 | return ddir 106 | 107 | @staticmethod 108 | def openFileNamesDialog(app): 109 | options = QFileDialog.Options() 110 | options |= QFileDialog.DontUseNativeDialog 111 | files, _ = QFileDialog.getOpenFileNames(app, 112 | "QFileDialog.getOpenFileNames()", 113 | "", 114 | "All Files (*);;Python Files (*.py)", 115 | options=options) 116 | if files: 117 | logger.debug(files) 118 | return files 119 | 120 | @staticmethod 121 | def saveFileDialog(app): 122 | options = QFileDialog.Options() 123 | options |= QFileDialog.DontUseNativeDialog 124 | fileName, _ = QFileDialog.getSaveFileName(app, 125 | "QFileDialog.getSaveFileName()", 126 | "", 127 | "All Files (*);;Text Files (*.txt)", 128 | options=options) 129 | if fileName: 130 | logger.debug(fileName) 131 | return fileName 132 | -------------------------------------------------------------------------------- /pytripgui/view/execute.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ExecuteDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 612 10 | 343 11 | 12 | 13 | 14 | 15 | 0 16 | 10 17 | 18 | 19 | 20 | 21 | 500 22 | 0 23 | 24 | 25 | 26 | 27 | 16777215 28 | 16777215 29 | 30 | 31 | 32 | GUI Executor 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | false 48 | 49 | 50 | QDialogButtonBox::Ok 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /pytripgui/view/execute_config.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | tripConfig 4 | 5 | 6 | 7 | 0 8 | 0 9 | 459 10 | 118 11 | 12 | 13 | 14 | TRiP98 Configuration 15 | 16 | 17 | 18 | 19 | 20 | Execute on 21 | 22 | 23 | 24 | 25 | 26 | true 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | QDialogButtonBox::Abort|QDialogButtonBox::Ok 37 | 38 | 39 | false 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /pytripgui/view/execute_config_view.py: -------------------------------------------------------------------------------- 1 | from pytripgui.view.qt_gui import UiExecuteConfigDialog 2 | from pytripgui.view.qt_view_adapter import ComboBox 3 | 4 | 5 | class ExecuteConfigView: 6 | def __init__(self, model, ui): 7 | self._ui = UiExecuteConfigDialog(ui) 8 | self.config = None 9 | 10 | self._configs = ComboBox(self._ui.configs_comboBox) 11 | self._configs.fill(model, lambda config: config.name) 12 | self._setup_ok_and_cancel_buttons_callbacks() 13 | 14 | def _setup_ok_and_cancel_buttons_callbacks(self): 15 | self.set_ok_callback(self._execute) 16 | self.set_cancel_callback(self._exit) 17 | 18 | def set_ok_callback(self, fun): 19 | self._ui.accept_buttonBox.accepted.connect(fun) 20 | 21 | def set_cancel_callback(self, fun): 22 | self._ui.accept_buttonBox.rejected.connect(fun) 23 | 24 | def _execute(self): 25 | self.config = self._configs.current_data 26 | self._exit() 27 | 28 | def _exit(self): 29 | self._ui.close() 30 | 31 | def show(self): 32 | self._ui.show() 33 | self._ui.exec_() 34 | -------------------------------------------------------------------------------- /pytripgui/view/loading_file_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 300 10 | 150 11 | 12 | 13 | 14 | Loading files 15 | 16 | 17 | false 18 | 19 | 20 | 21 | 22 | 50 23 | 20 24 | 200 25 | 60 26 | 27 | 28 | 29 | TextLabel 30 | 31 | 32 | 33 | 34 | false 35 | 36 | 37 | 38 | 120 39 | 80 40 | 60 41 | 60 42 | 43 | 44 | 45 | QDialogButtonBox::Ok 46 | 47 | 48 | true 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /pytripgui/view/main_view.py: -------------------------------------------------------------------------------- 1 | from pytripgui.view.qt_gui import UiMainWindow 2 | from pytripgui.view.plot_volhist import VolHist 3 | 4 | from pytripgui.canvas_vc.canvas_view import CanvasView 5 | 6 | import logging 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class MainView: 12 | """ 13 | Viewer class for matplotlib 2D plotting widget 14 | """ 15 | def __init__(self): 16 | self.ui = UiMainWindow() 17 | 18 | self.ui.dvh = VolHist(parent=self.ui.tab_dvh) 19 | self.ui.lvh = VolHist(parent=self.ui.tab_lvh) 20 | 21 | self.ui.setWindowTitle("PyTRiPGUI") 22 | -------------------------------------------------------------------------------- /pytripgui/view/plot_volhist.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from PyQt5.QtWidgets import QVBoxLayout 4 | from PyQt5.QtWidgets import QSizePolicy 5 | from PyQt5 import QtCore 6 | 7 | from matplotlib.figure import Figure 8 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas 9 | 10 | # from controller.plot_cont import PlotController 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class VolHist(FigureCanvas): 16 | """ 17 | Viewer class for matplotlib figure for Volume Histograms 18 | """ 19 | def __init__(self, parent=None, width=6, height=4, dpi=110): 20 | """ 21 | Init canvas. 22 | """ 23 | 24 | self.fig = Figure(figsize=(width, height), dpi=dpi) 25 | 26 | # Here one can adjust the position of the CTX plot area. 27 | # self.axes = self.fig.add_subplot(111) 28 | self.axes = self.fig.add_axes([0.1, 0.1, 0.85, 0.85]) 29 | self.axes.grid(True) 30 | self.axes.set_xlabel("(no data)") 31 | self.axes.set_ylabel("(no data)") 32 | 33 | FigureCanvas.__init__(self, self.fig) 34 | 35 | layout = QVBoxLayout(parent) 36 | layout.addWidget(self) 37 | parent.setLayout(layout) 38 | 39 | FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding) 40 | FigureCanvas.updateGeometry(self) 41 | 42 | # next too lines are needed in order to catch keypress events in plot canvas by mpl_connect() 43 | FigureCanvas.setFocusPolicy(self, QtCore.Qt.ClickFocus) 44 | FigureCanvas.setFocus(self) 45 | -------------------------------------------------------------------------------- /pytripgui/view/viewcanvas.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 861 10 | 405 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | Form 21 | 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 0 31 | 32 | 33 | 0 34 | 35 | 36 | 0 37 | 38 | 39 | 40 | 41 | 42 | 43 | QLayout::SetMinimumSize 44 | 45 | 46 | 47 | 48 | 49 | 50 | VOIs list 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | false 60 | 61 | 62 | 63 | 0 64 | 0 65 | 66 | 67 | 68 | 69 | 112 70 | 27 71 | 72 | 73 | 74 | 75 | Transversal 76 | 77 | 78 | 79 | 80 | Sagittal 81 | 82 | 83 | 84 | 85 | Coronal 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | Qt::Horizontal 94 | 95 | 96 | QSizePolicy::MinimumExpanding 97 | 98 | 99 | 100 | 100 101 | 20 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 0 112 | 113 | 114 | 0 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 49 124 | 125 | 126 | Qt::Vertical 127 | 128 | 129 | QSlider::TicksAbove 130 | 131 | 132 | 133 | 134 | 135 | 136 | true 137 | 138 | 139 | 140 | 0 141 | 0 142 | 143 | 144 | 145 | 146 | 200 147 | 0 148 | 149 | 150 | 151 | 152 | 250 153 | 16777215 154 | 155 | 156 | 157 | true 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | matplotlib==3.4.3 ; python_version >= '3.7' # remove once compatibility problem with matplotlib 3.5+ is restored 2 | pytrip98[remote]~=3.7 # use versions compatible with 3.7 (3.7 and later from 3.x series) 3 | PyQt5>=5.15 ; python_version >= '3.8' 4 | PyQt5<5.10 ; python_version < '3.8' 5 | PyQtChart>=5.15 ; python_version >= '3.8' 6 | PyQtChart<5.10 ; python_version < '3.8' 7 | anytree~=2.8 8 | Events~=0.4 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pycodestyle] 2 | max-line-length=120 3 | 4 | [flake8] 5 | max-line-length=120 6 | ignore = W504 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import setuptools 3 | 4 | from pytripgui.version import git_version 5 | 6 | 7 | def write_version_py(filename=os.path.join('pytripgui', 'VERSION')): 8 | cnt = """%(version)s 9 | """ 10 | 11 | GIT_REVISION = git_version() 12 | a = open(filename, 'w') 13 | try: 14 | a.write(cnt % {'version': GIT_REVISION}) 15 | finally: 16 | a.close() 17 | 18 | 19 | write_version_py() 20 | 21 | with open('README.rst') as readme_file: 22 | readme = readme_file.read() 23 | 24 | # install_requires is list of dependencies needed by pip when running `pip install` 25 | install_requires = [ 26 | "matplotlib==3.4.3 ; python_version>='3.7'", 27 | 'pytrip98[remote]~=3.7', 28 | 'anytree~=2.8', 29 | 'Events~=0.4', 30 | "PyQt5<5.10 ; python_version<'3.8'", 31 | "PyQt5>=5.15 ; python_version>='3.8'", 32 | "PyQtChart<5.10 ; python_version<'3.8'", 33 | "PyQtChart>=5.15 ; python_version>='3.8'", 34 | ] 35 | 36 | setuptools.setup( 37 | name='pytrip98gui', 38 | version=git_version(), 39 | packages=setuptools.find_packages(exclude=["tests", "tests.*"]), 40 | url='https://github.com/pytrip/pytripgui', 41 | license='GPL', 42 | author='Niels Bassler et al.', 43 | author_email='bassler@clin.au.dk', 44 | maintainer='Leszek Grzanka et al.', 45 | maintainer_email='grzanka@agh.edu.pl', 46 | description='PyTRiP GUI', 47 | long_description=readme + '\n', 48 | classifiers=[ 49 | # How mature is this project? Common values are 50 | # 3 - Alpha 51 | # 4 - Beta 52 | # 5 - Production/Stable 53 | 'Development Status :: 3 - Alpha', 54 | 55 | # Indicate who your project is intended for 56 | 'Intended Audience :: End Users/Desktop', 57 | 'Intended Audience :: Science/Research', 58 | 'Topic :: Software Development :: Libraries :: Python Modules', 59 | 'Topic :: Scientific/Engineering :: Medical Science Apps.', 60 | 'Topic :: Scientific/Engineering :: Physics', 61 | 'Operating System :: MacOS :: MacOS X', 62 | 'Operating System :: POSIX :: Linux', 63 | 'Operating System :: Microsoft :: Windows :: Windows 10', 64 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 65 | 66 | # Specify the Python versions you support here. In particular, ensure 67 | # that you indicate whether you support Python 2, Python 3 or both. 68 | 'Programming Language :: Python :: 3.6', 69 | 'Programming Language :: Python :: 3.7', 70 | 'Programming Language :: Python :: 3.8', 71 | 'Programming Language :: Python :: 3.9', 72 | 'Programming Language :: Python :: 3.10' 73 | ], 74 | package_data={'pytripgui': ['res/*', 'view/*.ui', 'VERSION']}, 75 | install_requires=install_requires, 76 | entry_points={ 77 | 'console_scripts': [ 78 | 'pytripgui=pytripgui.main:main', 79 | ], 80 | }) 81 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytrip/pytripgui/86184e798d8ce3e1a050930d37d31894959ce3b6/tests/__init__.py -------------------------------------------------------------------------------- /tests/requirements-test.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-qt ; python_version >= '3.8' 3 | pytest-qt<4.0 ; python_version < '3.8' 4 | pyautogui 5 | -------------------------------------------------------------------------------- /tests/test_kernel_dialog.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pytest 4 | from pytrip.tripexecuter import KernelModel 5 | from pytrip.tripexecuter import Projectile 6 | 7 | from pytripgui.kernel_vc import KernelController 8 | from pytripgui.kernel_vc import KernelQtView 9 | 10 | logger = logging.getLogger(__name__) 11 | logging.basicConfig(level=logging.DEBUG) 12 | 13 | 14 | @pytest.fixture 15 | def kernels(): 16 | kernels = [] 17 | # Kernel 1 18 | ker = KernelModel() 19 | ker.projectile = Projectile("C") 20 | kernels.append(ker) 21 | # Kernel 2 22 | ker = KernelModel() 23 | ker.projectile = Projectile("H") 24 | kernels.append(ker) 25 | yield kernels 26 | 27 | 28 | def test_basics(qtbot, kernels): 29 | view = KernelQtView() 30 | controller = KernelController(kernels, view) 31 | controller.set_view_from_model() 32 | 33 | qtbot.addWidget(view.ui) 34 | view.ui.show() 35 | assert view.ui.isVisible() 36 | 37 | # selecting kernel to edit 38 | current_kernel_index = 1 39 | view.ui.beamKernel_comboBox.setCurrentIndex(current_kernel_index) 40 | assert view.projectile_symbol == kernels[current_kernel_index].projectile.iupac 41 | 42 | # setting new name 43 | new_kernel_name = "Proton" 44 | view.kernel_name = new_kernel_name 45 | 46 | # clicking "OK" 47 | view.ui.accept_buttonBox.accepted.emit() 48 | assert not view.ui.isVisible() 49 | assert controller.user_clicked_save 50 | 51 | # checking if new name was saved 52 | assert kernels[current_kernel_index].name == new_kernel_name 53 | -------------------------------------------------------------------------------- /tests/test_patient_tree.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pytripgui.app_logic.patient_tree import PatientTree 4 | from pytripgui.main_window_qt_vc import MainWindowQtView 5 | 6 | logger = logging.getLogger(__name__) 7 | logging.basicConfig(level=logging.DEBUG) 8 | 9 | 10 | def test_basics(qtbot): 11 | view = MainWindowQtView() 12 | 13 | patient_tree = PatientTree(view.ui) 14 | patient_tree.set_visible(True) 15 | 16 | qtbot.addWidget(view.ui) 17 | view.show() 18 | 19 | assert view.ui.isVisible() 20 | -------------------------------------------------------------------------------- /tests/test_pytripgui.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os.path 3 | import sys 4 | 5 | import pytest 6 | from PyQt5 import QtCore, QtWidgets 7 | import pyautogui 8 | 9 | from pytrip.ctx import CtxCube 10 | from pytrip.vdx import create_sphere, VdxCube 11 | 12 | from pytripgui.app_logic.viewcanvas import ViewCanvases 13 | from pytripgui.main_window_qt_vc import MainWindowQtView, MainWindowController 14 | from pytripgui.model.main_model import MainModel 15 | from pytripgui.view.qt_gui import UiPlanDialog, UiFieldDialog 16 | 17 | logger = logging.getLogger(__name__) 18 | logging.basicConfig(level=logging.DEBUG) 19 | 20 | 21 | @pytest.fixture 22 | def window(): 23 | model = MainModel() 24 | view = MainWindowQtView() 25 | controller = MainWindowController(model, view) 26 | yield model, view, controller 27 | 28 | 29 | @pytest.fixture(scope="session") 30 | def voxelplan_header_path(tmpdir_factory): 31 | ctx = CtxCube() 32 | ctx.create_empty_cube(value=0, dimx=100, dimy=100, dimz=25, pixel_size=1, slice_distance=2, slice_offset=0) 33 | file_obj = tmpdir_factory.mktemp("data").join("patient") 34 | patient_base_path = str(file_obj) 35 | ctx.write(patient_base_path) 36 | vdx = VdxCube(cube=ctx) 37 | voi = create_sphere(cube=ctx, name="target", center=[50, 50, 25], radius=10) 38 | vdx.add_voi(voi) 39 | vdx.write(patient_base_path + '.vdx') 40 | yield patient_base_path + '.hed' 41 | 42 | 43 | def test_basics(qtbot, window): 44 | _, view, _ = window 45 | qtbot.addWidget(view.ui) 46 | view.ui.show() 47 | 48 | assert view.ui.isVisible() 49 | assert view.ui.windowTitle() == 'PyTRiPGUI' 50 | 51 | 52 | @pytest.mark.xfail(sys.version_info < (3,7), reason="issue on matplotlib with python 3.6 or older") 53 | def test_open_voxelplan(qtbot, window, voxelplan_header_path): 54 | model, view, _ = window 55 | qtbot.addWidget(view.ui) 56 | 57 | patient_dir, header_basename = os.path.split(voxelplan_header_path) 58 | 59 | def handle_file_dialog(): 60 | dialog = view.ui.findChild(QtWidgets.QFileDialog) 61 | dialog.setDirectory(QtCore.QDir(patient_dir)) 62 | 63 | pyautogui.typewrite(header_basename) 64 | pyautogui.press("enter") 65 | 66 | # solution when option QFileDialog.DontUseNativeDialog is on 67 | # it doesn't require pyautogui 68 | # dialog.findChild(QtWidgets.QLineEdit, 'fileNameEdit').setText(file) 69 | # open_button = dialog.findChildren(QtWidgets.QPushButton)[0] 70 | # qtbot.mouseClick(open_button, QtCore.Qt.LeftButton) 71 | 72 | QtCore.QTimer.singleShot(1000, handle_file_dialog) 73 | view.ui.actionOpen_Voxelplan.trigger() 74 | 75 | assert isinstance(model.viewcanvases, ViewCanvases) 76 | assert model.patient_tree.patient_tree_model.rowCount() == 1 77 | assert len(model.patient_tree.selected_item_patient().data.vdx.vois) == 1 78 | 79 | 80 | @pytest.mark.xfail(sys.version_info < (3,7), reason="issue on matplotlib with python 3.6 or older") 81 | def test_create_plan_and_field(qtbot, window, voxelplan_header_path): 82 | _, view, controller = window 83 | qtbot.addWidget(view.ui) 84 | 85 | controller.open_voxelplan(voxelplan_header_path) 86 | 87 | def handle_plan_dialog(): 88 | dialog = view.ui.findChild(UiPlanDialog) 89 | assert dialog.isVisible() 90 | dialog.findChild(QtWidgets.QTabWidget, 'tabWidget').setCurrentIndex(1) 91 | radios = dialog.targetROI_listWidget.findChildren(QtWidgets.QRadioButton) 92 | radios[0].setChecked(True) 93 | ok_button = dialog.accept_buttonBox.findChildren(QtWidgets.QPushButton)[0] 94 | qtbot.mouseClick(ok_button, QtCore.Qt.LeftButton) 95 | 96 | assert view.ui.actionNew_Plan.isEnabled() is True 97 | assert view.ui.actionCreate_field.isEnabled() is False 98 | QtCore.QTimer.singleShot(1000, handle_plan_dialog) 99 | view.ui.actionNew_Plan.trigger() 100 | 101 | def handle_field_dialog(): 102 | dialog = view.ui.findChild(UiFieldDialog) 103 | assert dialog.isVisible() 104 | qtbot.mouseClick(dialog.gantry_pushButton_p90, QtCore.Qt.LeftButton) 105 | assert dialog.gantry_doubleSpinBox.value() == 90.0 106 | ok_button = dialog.accept_ButtonBox.findChildren(QtWidgets.QPushButton)[0] 107 | qtbot.mouseClick(ok_button, QtCore.Qt.LeftButton) 108 | 109 | assert view.ui.actionCreate_field.isEnabled() is True 110 | QtCore.QTimer.singleShot(1000, handle_field_dialog) 111 | view.ui.actionCreate_field.trigger() 112 | -------------------------------------------------------------------------------- /win_innosetup.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define MyAppName "pytripgui" 5 | #define MyAppVersion "X.Y.Z" 6 | #define MyAppPublisher "Leszek Grzanka" 7 | #define MyAppURL "https://github.com/pytrip/pytripgui" 8 | #define MyAppExeName "pytripgui.exe" 9 | #define MyAppPlatform "win_64bit" 10 | #define MyAppDir = SourcePath + "\dist\pytripgui\" 11 | 12 | [Setup] 13 | ; NOTE: The value of AppId uniquely identifies this application. 14 | ; Do not use the same AppId value in installers for other applications. 15 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 16 | 17 | AppId={{7C2DBD29-FBB9-46A3-8AFF-113F0290A1EB} 18 | AppName={#MyAppName} 19 | AppVersion={#MyAppVersion} 20 | AppPublisher={#MyAppPublisher} 21 | AppPublisherURL={#MyAppURL} 22 | AppSupportURL={#MyAppURL} 23 | AppUpdatesURL={#MyAppURL} 24 | ArchitecturesAllowed=x64 25 | ArchitecturesInstallIn64BitMode=x64 26 | ; auto - depending on install mode it will be 'common' (admin mode) or 'user' (non-admin mode) 27 | ; pf - Program Files 28 | ; commonpf and userpf consts expand to different paths that user has access to 29 | DefaultDirName={autopf}\{#MyAppName} 30 | DisableProgramGroupPage=yes 31 | OutputBaseFilename={#MyAppName}_{#MyAppVersion}_{#MyAppPlatform}_setup 32 | Compression=lzma 33 | SolidCompression=yes 34 | PrivilegesRequired=lowest 35 | PrivilegesRequiredOverridesAllowed=dialog 36 | LicenseFile={#SourcePath}\GPL_LICENSE.txt 37 | UninstallDisplayName=pytripgui 38 | VersionInfoVersion=0.1.0 39 | 40 | [Icons] 41 | Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" 42 | Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon 43 | 44 | [Tasks] 45 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}" 46 | 47 | [Run] 48 | Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent 49 | 50 | [Files] 51 | Source: "{#MyAppDir}{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion 52 | Source: "{#MyAppDir}*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 53 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 54 | 55 | [Languages] 56 | Name: "english"; MessagesFile: "compiler:Default.isl" 57 | 58 | [Messages] 59 | ErrorCreatingDir=Setup was unable to create the directory "%1". If you want to install this application in this location, run the installer as administrator 60 | 61 | [Code] 62 | // https://stackoverflow.com/questions/2000296/inno-setup-how-to-automatically-uninstall-previous-installed-version/2099805#2099805 63 | ///////////////////////////////////////////////////////////////////// 64 | function GetUninstallString(): String; 65 | var 66 | sUnInstPath: String; 67 | sUnInstallString: String; 68 | begin 69 | sUnInstPath := ExpandConstant('Software\Microsoft\Windows\CurrentVersion\Uninstall\{#emit SetupSetting("AppId")}_is1'); 70 | sUnInstallString := ''; 71 | if not RegQueryStringValue(HKLM, sUnInstPath, 'UninstallString', sUnInstallString) then 72 | RegQueryStringValue(HKCU, sUnInstPath, 'UninstallString', sUnInstallString); 73 | Result := sUnInstallString; 74 | end; 75 | 76 | 77 | ///////////////////////////////////////////////////////////////////// 78 | function IsUpgrade(): Boolean; 79 | begin 80 | Result := (GetUninstallString() <> ''); 81 | end; 82 | 83 | 84 | ///////////////////////////////////////////////////////////////////// 85 | function UnInstallOldVersion(): Integer; 86 | var 87 | sUnInstallString: String; 88 | iResultCode: Integer; 89 | begin 90 | // Return Values: 91 | // 1 - uninstall string is empty 92 | // 2 - error executing the UnInstallString 93 | // 3 - successfully executed the UnInstallString 94 | 95 | // default return value 96 | Result := 0; 97 | 98 | // get the uninstall string of the old app 99 | sUnInstallString := GetUninstallString(); 100 | if sUnInstallString <> '' then begin 101 | sUnInstallString := RemoveQuotes(sUnInstallString); 102 | if Exec(sUnInstallString, '/SILENT /NORESTART /SUPPRESSMSGBOXES','', SW_HIDE, ewWaitUntilTerminated, iResultCode) then 103 | Result := 3 104 | else 105 | Result := 2; 106 | end else 107 | Result := 1; 108 | end; 109 | 110 | ///////////////////////////////////////////////////////////////////// 111 | procedure CurStepChanged(CurStep: TSetupStep); 112 | begin 113 | if (CurStep=ssInstall) then 114 | begin 115 | if (IsUpgrade()) then 116 | begin 117 | UnInstallOldVersion(); 118 | end; 119 | end; 120 | end; --------------------------------------------------------------------------------