├── .github └── workflows │ └── actions.yml ├── .gitignore ├── CITATION.bib ├── DEVELOPER.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── Screenshots ├── Main_window.png ├── Side_bar.png └── Top_Menu.png ├── pyproject.toml ├── rdeditor ├── Mendelev_extract.py ├── __init__.py ├── icon_themes │ ├── dark │ │ ├── application │ │ │ ├── Change_E_Z.png │ │ │ ├── Change_E_Z.svg │ │ │ ├── Change_R_S.png │ │ │ ├── CleanupChem.png │ │ │ ├── RecalcCoord.png │ │ │ ├── about.png │ │ │ ├── appicon.png │ │ │ ├── atommapnumber.png │ │ │ ├── benzene.png │ │ │ ├── calc.png │ │ │ ├── convert.sh │ │ │ ├── cyclohexane.png │ │ │ ├── exit.png │ │ │ ├── grid.png │ │ │ ├── icons8-Broom.png │ │ │ ├── icons8-Bulleted List.png │ │ │ ├── icons8-Cancel.png │ │ │ ├── icons8-Carbon.png │ │ │ ├── icons8-Cursor.png │ │ │ ├── icons8-Decrease Font.png │ │ │ ├── icons8-Delete.png │ │ │ ├── icons8-Double.png │ │ │ ├── icons8-Edit.png │ │ │ ├── icons8-Exit.png │ │ │ ├── icons8-Hydrogen.png │ │ │ ├── icons8-Increase Font.png │ │ │ ├── icons8-Info.png │ │ │ ├── icons8-Left.png │ │ │ ├── icons8-Line.png │ │ │ ├── icons8-Minus.png │ │ │ ├── icons8-Molecule.png │ │ │ ├── icons8-Open.png │ │ │ ├── icons8-Oxygen.png │ │ │ ├── icons8-Physics.png │ │ │ ├── icons8-Pinch.png │ │ │ ├── icons8-Plus.png │ │ │ ├── icons8-Redo.png │ │ │ ├── icons8-Replace Atom.png │ │ │ ├── icons8-Reset.png │ │ │ ├── icons8-Right.png │ │ │ ├── icons8-Save as.png │ │ │ ├── icons8-Save.png │ │ │ ├── icons8-Scatter Plot.png │ │ │ ├── icons8-Shutdown.png │ │ │ ├── icons8-Single.png │ │ │ ├── icons8-Trash.png │ │ │ ├── icons8-Triple.png │ │ │ ├── icons8-Undo.png │ │ │ ├── icons8-copy-96.png │ │ │ ├── icons8-paste-100.png │ │ │ ├── molblock.png │ │ │ ├── next.png │ │ │ ├── open.png │ │ │ ├── plot.png │ │ │ ├── prev.png │ │ │ ├── ptable.png │ │ │ └── ptable.svg │ │ └── index.theme │ └── light │ │ ├── application │ │ ├── Change_E_Z.png │ │ ├── Change_E_Z.svg │ │ ├── Change_R_S.png │ │ ├── CleanupChem.png │ │ ├── CleanupChem.svg │ │ ├── RecalcCoord.png │ │ ├── RecalcCoord.svg │ │ ├── about.png │ │ ├── appicon copy.png │ │ ├── appicon.png │ │ ├── appicon.svg.png │ │ ├── atommapnumber.png │ │ ├── atommapnumber.svg │ │ ├── benzene.png │ │ ├── benzene.svg │ │ ├── calc.png │ │ ├── cyclohexane.png │ │ ├── cyclohexane.svg │ │ ├── exit.png │ │ ├── grid.png │ │ ├── icons8-Broom.png │ │ ├── icons8-Bulleted List.png │ │ ├── icons8-Cancel.png │ │ ├── icons8-Carbon.png │ │ ├── icons8-Cursor.png │ │ ├── icons8-Decrease Font.png │ │ ├── icons8-Delete.png │ │ ├── icons8-Double.png │ │ ├── icons8-Edit.png │ │ ├── icons8-Exit.png │ │ ├── icons8-Hydrogen.png │ │ ├── icons8-Increase Font.png │ │ ├── icons8-Info.png │ │ ├── icons8-Left.png │ │ ├── icons8-Line.png │ │ ├── icons8-Minus.png │ │ ├── icons8-Molecule.png │ │ ├── icons8-Open.png │ │ ├── icons8-Oxygen.png │ │ ├── icons8-Physics.png │ │ ├── icons8-Pinch.png │ │ ├── icons8-Plus.png │ │ ├── icons8-Redo.png │ │ ├── icons8-Replace Atom.png │ │ ├── icons8-Reset.png │ │ ├── icons8-Right.png │ │ ├── icons8-Save as.png │ │ ├── icons8-Save.png │ │ ├── icons8-Scatter Plot.png │ │ ├── icons8-Shutdown.png │ │ ├── icons8-Single.png │ │ ├── icons8-Trash.png │ │ ├── icons8-Triple.png │ │ ├── icons8-Undo.png │ │ ├── icons8-copy-96.png │ │ ├── icons8-paste-100.png │ │ ├── molblock.png │ │ ├── next.png │ │ ├── open.png │ │ ├── plot.png │ │ ├── prev.png │ │ ├── ptable.png │ │ └── ptable.svg │ │ └── index.theme ├── molEditWidget.py ├── molViewWidget.py ├── ptable.py ├── ptable_widget.py ├── rdEditor.py ├── templatehandler.py └── utilities.py └── ruff.toml /.github/workflows/actions.yml: -------------------------------------------------------------------------------- 1 | name: Format Code 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | release: 11 | types: [published, edited] 12 | 13 | jobs: 14 | format: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: Install dependencies 22 | run: pip install ruff 23 | 24 | - name: Check code with ruff 25 | run: ruff check . 26 | 27 | - name: Format code with ruff 28 | run: ruff format . 29 | 30 | deploy: 31 | needs: [format] 32 | name: deploy to pypi 33 | runs-on: ubuntu-latest 34 | # this checks that the commit is a tagged commit 35 | if: github.event_name == 'release' && (github.event.action == 'published' || github.event.action == 'edited') 36 | steps: 37 | - uses: actions/checkout@v4 38 | with: 39 | fetch-depth: 0 40 | - name: Set up Python 41 | uses: actions/setup-python@v5 42 | with: 43 | python-version: 3.8 44 | - name: Install dependencies 45 | run: | 46 | python -m pip install --upgrade pip 47 | python -m pip install build 48 | - name: Build a binary wheel and a source tarball 49 | run: | 50 | python -m build . 51 | # push all tagged versions to pypi.org 52 | - name: Publish package to PyPI 53 | uses: pypa/gh-action-pypi-publish@release/v1 54 | with: 55 | user: __token__ 56 | password: ${{ secrets.PYPI_TOKEN }} 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | 107 | #vscode 108 | *code-workspace 109 | .vscode 110 | 111 | #Notebooks 112 | *ipynb 113 | 114 | #rdeditor 115 | rdeditor/_version.py -------------------------------------------------------------------------------- /CITATION.bib: -------------------------------------------------------------------------------- 1 | 2 | @misc{bjerrum_2024, 3 | title = {Python-{Based} {Interactive} {RDKit} {Molecule} {Editing} with {rdEditor}}, 4 | url = {https://chemrxiv.org/engage/chemrxiv/article-details/65e6dcfa9138d23161b2979c}, 5 | doi = {10.26434/chemrxiv-2024-jfhmw}, 6 | abstract = {RDKit is a widely used toolkit for cheminformatics, renowned for its fast C++ backend and versatile Python API. However, in many cases, users find themselves needing to create or edit molecules in external programs, as the majority prefer not to manipulate raw SMILES strings. Unfortunately, most available tools for this purpose are either proprietary, deprecated, or not Python-based. Here we introduce rdEditor, a molecular editor entirely written in Python 3 using PySide2 graphical user interface widgets. The application is programmed in an object-oriented fashion, and the custom PySide2 widgets can be easily reused in other applications. It's important to note that this application is an editor, not a drawing program. In other words, RDKit handles the layout of the molecule entirely, and the user interacts with the molecular graph object directly, rather than drawing on a canvas with subsequent conversion to molecular format. While reusing functionality from RDKit contributes to an efficient codebase, there are some drawbacks if end-users expect to have control over the precise placement of atoms. The code is released as open-source and is available on GitHub at: https://github.com/EBjerrum/rdEditor.}, 7 | language = {en}, 8 | urldate = {2024-03-07}, 9 | publisher = {ChemRxiv}, 10 | author = {Bjerrum, Esben Jannik and Palunas, Kovas and Menke, Janosch}, 11 | month = mar, 12 | year = {2024}, 13 | } 14 | -------------------------------------------------------------------------------- /DEVELOPER.md: -------------------------------------------------------------------------------- 1 | # Developer Guide 2 | 3 | ## Installation 4 | 5 | ### Prerequisites 6 | 7 | rdkit, numpy, pyside and pqtdarktheme are installed by default. 8 | ruff is installed using the dev tag as described below 9 | 10 | ### Installation Steps 11 | 12 | 1. Clone the repository: 13 | 14 | ```bash 15 | git clone https://github.com/EBjerrum/rdeditor.git 16 | ``` 17 | 18 | 2. Navigate to the project directory: 19 | 20 | ```bash 21 | cd rdeditor 22 | ``` 23 | 24 | 3. Install rdeditor in editable mode to enable developer modifications: 25 | 26 | ```bash 27 | pip install -e .[dev] 28 | ``` 29 | 30 | 4. Optionally, set up your preferred code editor (e.g., VS Code) to format code on save using ruff. 31 | 32 | ## Automatic Code Formatting with Ruff 33 | 34 | The `rdeditor` project utilizes `ruff` as both a linter and code formatter to ensure consistent code quality and formatting standards. You can automate the code formatting process by setting up a pre-commit hook in your local Git repository or configuring VS Code to auto-format code on save using the provided `ruff.toml` specifications. 35 | 36 | ### Creating a Pre-Commit Hook 37 | 38 | You can set up a pre-commit hook in your local Git repository to automatically format code using `ruff` before each commit. Here's how to do it: 39 | 40 | 1. Navigate to the `.git/hooks` directory in your repository. 41 | 42 | 2. Create a new file named `pre-commit` if it doesn't already exist. 43 | 44 | 3. Open the `pre-commit` file in a text editor and add the following content: 45 | 46 | ```bash 47 | #!/bin/bash 48 | 49 | # Run the ruff formatter on staged changes 50 | ruff format $(git diff --cached --name-only | grep '\.py$') 51 | 52 | # Stage the formatted changes 53 | git add $(git diff --cached --name-only) 54 | ``` 55 | 56 | 4. 57 | 58 | 5. Save the file and make it executable by running the following command in your terminal: 59 | 60 | ```bash 61 | chmod +x .git/hooks/pre-commit 62 | ``` 63 | 64 | Now, each time you attempt to commit changes, the `pre-commit` hook will run the `ruff` formatter on your staged changes, ensuring consistent formatting before the commit is finalized. 65 | 66 | ### Configuring VS Code for Auto-Formatting on Save 67 | 68 | If you prefer to use VS Code, you can configure it to automatically format code using the provided `ruff.toml` specifications on save. Here's how to do it: 69 | 70 | 1. Open VS Code and navigate to the settings by clicking on the gear icon in the bottom left corner or by pressing `Ctrl + ,`. 71 | 72 | 2. In the search bar at the top, type "format on save" to find the setting. 73 | 74 | 3. Check the box next to "Editor: Format On Save" to enable auto-formatting on save. 75 | 76 | 4. Next, click on "Extensions" in the sidebar and search for "ruff" in the search bar. 77 | 78 | 5. Install the ruff extension. 79 | 80 | 6. Press `ctrl-shift-p` or select `format with...` from the right click menu in a python file. Select the option `configure default formatter...` and choose ruff. 81 | 82 | With these settings configured, VS Code will automatically format your Python code according to the specifications provided in the `ruff.toml` file each time you save a file. 83 | 84 | ## Usage 85 | 86 | [Include usage instructions here if different from README.md] 87 | 88 | ## Contributing 89 | 90 | ## Checking code with ruff 91 | 92 | Code submitted to github master branch are checked with ruff via GitHub Actions. It is thus advisable to check yourself before a pull request is made. This can be done with: 93 | 94 | `ruff check` which will inspect the code and print a list of issues. 95 | 96 | Sometimes they can be safely fixed with 97 | 98 | `ruff check --fix` and even `ruff check --fix --unsafe-fixes` but otherwise they need to be inspected and mitigated. 99 | 100 | ## Deployment to PyPi 101 | 102 | The github actions has been set to automatically deploy to pypi every time a new release is published. 103 | The release will need a new tag created (incremented), otherwise PyPi will not accept the new package. 104 | 105 | ### Development Environment Setup 106 | 107 | 1. Follow the installation steps in the INSTALL section. 108 | 2. Set up your preferred code editor (e.g., VS Code) to format code on save. 109 | 3. Optionally, configure a pre-commit hook locally to ensure code consistency before committing changes. 110 | 111 | ### Branching Strategy 112 | 113 | - Use meaningful branch names for new features, bug fixes, or enhancements (e.g., feature/add-new-tool, fix/issue-123). 114 | - Create feature branches from the main branch and submit pull requests for review. 115 | - Review and address any feedback from maintainers before merging changes. 116 | 117 | ### Code Guidelines 118 | 119 | - Follow the project's coding standards and style guide. 120 | - Write clear, concise, and well-documented code. 121 | - Include relevant comments and docstrings to explain complex logic or functionality. 122 | - Write meaningful commit messages that describe the purpose of the changes. 123 | 124 | ## Testing 125 | 126 | [Include testing instructions and guidelines here if applicable] 127 | 128 | ## Troubleshooting 129 | 130 | [Include troubleshooting tips and common issues here] 131 | 132 | ## Additional Resources 133 | 134 | [Include links to relevant documentation, tutorials, or external resources] 135 | 136 | ## Contact 137 | 138 | [Provide contact information for project maintainers or contributors] 139 | 140 | ## Changelog 141 | 142 | [Include a summary of recent changes, improvements, and bug fixes] 143 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rdeditor 2 | 3 | Simple RDKit molecule editor GUI using PySide6 4 | 5 | ![rdeditor, the RDKit molecule editor](https://github.com/EBjerrum/rdeditor/blob/master/Screenshots/Main_window.png?raw=true) 6 | 7 | ## Installation 8 | 9 | - requirements 10 | 11 | RDKit, NumPy, PySide6 and pyqtdarktheme should be automatically installed when installed via pip. 12 | 13 | - installation 14 | 15 | ```bash 16 | pip install rdeditor 17 | 18 | ``` 19 | 20 | A launch script will also be added so that it can be started from the command line via the `rdEditor` command. 21 | 22 | ## Usage 23 | 24 | Can be started from the command line with `rdEditor` or `rdEditor your_molecule.mol` to start edit an existing molecule. 25 | Interactions with the molecule are done via clicking and dragging on the canvas, atoms or bonds. A choice of tools is available. 26 | 27 | To edit a molecule, select the pen tool, and an atom, bond or template type and click on the canvas to add it. 28 | 29 | Clicking: 30 | 31 | - When clicking existing atoms or bonds, the clicked atom or bond will be modified, depending on the atom or bond type selected. 32 | - If a bondtype is selected, the bond will be added to the clicked atom with a carbon atom at the other end. 33 | - If a bondtype is selected and a bond is clicked, the bond will be changed to that type if different. 34 | - If you click multiple times on a bond with an atomtype selected, the bondtype will cycle between single, double and triple bond. 35 | 36 | Dragging: 37 | 38 | - If you click and drag from an atom, the selected atomtype will be added at the end of a single bond. 39 | - Dragging between two atoms will add a bond between them. If a bondtype is selected, the bondorder will correspond to the bondtype selected. 40 | - Dragging on the canvas with an atomtype will add two atoms of that type with a single bond between them. 41 | - Dragging on the canvas with a bondtype will simply add that bond with carbon atoms at both ends. 42 | 43 | Templates: 44 | 45 | - Templates work kind of like atoms, so if you click on an atom, the template will be added directly to that atom. 46 | - If a template is selected and dragged from an atom, the template will be added with a single bond to the clicked atom. 47 | - Some templates can also be added to bonds by clicking on the middle of the bond. 48 | - Dragging on a canvas with a template will add a carbon atom and a single bond to the template. 49 | 50 | Other actions: 51 | 52 | - most other actions (R/S, E/Z, Increase/Decrease charge, Adjust atom number) works by clikcing on existing atoms. 53 | 54 | #### Top Menu: 55 | 56 | ![top menu of rdeditor, the RDKit molecule editor](https://github.com/EBjerrum/rdeditor/blob/master/Screenshots/Top_Menu.png?raw=true) 57 | 58 | From left to right 59 | 60 | - Open: Open a molfile 61 | - Save: Save current molecule 62 | - Save As: Save current molecule with a new name 63 | 64 | - Arrow: Select tool. Click on an atom to select it, click on the canvas to deselect. Clicking on multiple atoms one after another will select them, but only the lastly clicked one will be highlighted in red and used for operations, such as bond creation to another existing atom. 65 | - Pen: Add tool. Clicking on an existing atom will add the current selected atom type to that atom with a single bond. Clicking on the canvas will add a disconnected atom. Clicking on a bond will cycle through single, double and triple bonds. 66 | - R/S: Change the stereo chemistry of the selected atom 67 | - E/Z: Change E/Z stereo of double bonds 68 | - Increase/Decrease charge: Will increase or decrease the charge of the atom clicked 69 | - Set atommap or R-group number: Will set the atommap or R-group number of the atom clicked 70 | - clean up coordinates: recalculate coordinates disregarding existing coordinates. 71 | - clean up chemistry. Sanitize and/or Kekulize the molecule. 72 | - Delete atom/bond: 73 | - Clear Canvas 74 | - Undo. 75 | 76 | #### Side Bar: 77 | 78 | ![top menu of rdeditor, the RDKit molecule editor](https://github.com/EBjerrum/rdeditor/blob/master/Screenshots/Side_bar.png?raw=true) 79 | 80 | Most commonly used bond types, and atom types can be selected. Templates and R-group (dummy atoms) are also accessible. A Periodic table is accessible for exotic atom types. 81 | 82 | #### Dropdown menus 83 | 84 | Access to all standard operations as well as less used atom types and bond-types. 85 | 86 | #### Settings 87 | 88 | Themes can be selected from the ones available on your platform (Mac/Linux/Windows). 89 | 90 | The debug level can be selected 91 | 92 | Cleanup settings can be selected if the molecule should be sanitized or kekulized during cleanup. 93 | 94 | ## Development 95 | 96 | Instructions to set it up in editable modes and instructions for eventual contributions can be found in the [DEVELOPER.md](./DEVELOPER.md) file. 97 | Please reach out first, there may be a relevant development branch. 98 | 99 | ## Additional Reading 100 | 101 | I wrote a blog post with an overview of the structure of the code. 102 | [https://www.wildcardconsulting.dk/rdeditor-an-open-source-molecular-editor-based-using-python-pyside2-and-rdkit/](https://www.wildcardconsulting.dk/rdeditor-an-open-source-molecular-editor-based-using-python-pyside2-and-rdkit/) 103 | 104 | We also published a preprint on ChemRxiv: [https://chemrxiv.org/engage/chemrxiv/article-details/65e6dcfa9138d23161b2979c](https://chemrxiv.org/engage/chemrxiv/article-details/65e6dcfa9138d23161b2979c) 105 | 106 | ## ISSUES 107 | 108 | Please report issues at GitHub, it's tough getting all corners of a GUI tested. 109 | -------------------------------------------------------------------------------- /Screenshots/Main_window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/Screenshots/Main_window.png -------------------------------------------------------------------------------- /Screenshots/Side_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/Screenshots/Side_bar.png -------------------------------------------------------------------------------- /Screenshots/Top_Menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/Screenshots/Top_Menu.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "rdeditor" 7 | description = "An RDKit based molecule editor using PySide" 8 | readme = "README.md" 9 | requires-python = ">=3.10" 10 | license = {text = "LGPL"} 11 | authors = [ 12 | {name = "Esben Jannik Bjerrum", email = "esbenjannik@rocketmail.com"}, 13 | ] 14 | keywords = ["RDKit", "molecule", "editor", "pyside"] 15 | 16 | dependencies = [ 17 | "PySide6", 18 | "numpy", 19 | "rdkit", 20 | "pyqtdarktheme", 21 | ] 22 | dynamic = ["version"] 23 | 24 | [project.urls] 25 | Homepage = "http://github.com/ebjerrum/rdeditor" 26 | "Bug Tracker" = "https://github.com/ebjerrum/rdeditor/issues" 27 | Documentation = "https://github.com/ebjerrum/rdeditor/blob/master/README.md" 28 | "Source Code" = "https://github.com/ebjerrum/rdeditor" 29 | "Citation" = "https://github.com/ebjerrum/rdeditor/blob/master/CITATION.md" 30 | "ChemRxiv Preprint" = "https://chemrxiv.org/engage/chemrxiv/article-details/65e6dcfa9138d23161b2979c" 31 | "Developer Guide" = "https://github.com/ebjerrum/rdeditor/blob/master/DEVELOPER.md" 32 | 33 | [project.optional-dependencies] 34 | dev = ["ruff", "wheel", "twine", "setuptools_scm"] 35 | 36 | [project.scripts] 37 | rdEditor = "rdeditor.rdEditor:launch" 38 | 39 | [tool.setuptools] 40 | packages = ["rdeditor"] 41 | zip-safe = false 42 | 43 | [tool.setuptools.package-data] 44 | rdeditor = [ 45 | "icon_themes/dark/*", 46 | "icon_themes/dark/application/*", 47 | "icon_themes/light/*", 48 | "icon_themes/light/application/*", 49 | ] 50 | 51 | [tool.setuptools_scm] 52 | write_to = "rdeditor/_version.py" 53 | 54 | [tool.ruff] 55 | extend = "ruff.toml" -------------------------------------------------------------------------------- /rdeditor/Mendelev_extract.py: -------------------------------------------------------------------------------- 1 | from mendeleev import element 2 | 3 | 4 | elements = {} 5 | symboltoint = {} 6 | 7 | 8 | for i in range(1, 119): 9 | e = element(i) 10 | elements[i] = {"Name": e.name, "Symbol": e.symbol, "Group": e.group_id, "Period": e.period} 11 | symboltoint[e.symbol] = i 12 | -------------------------------------------------------------------------------- /rdeditor/__init__.py: -------------------------------------------------------------------------------- 1 | # from molViewWidget import molViewWidget 2 | # from molEditWidget import MolEditWidget 3 | # from ptable_widget import PTable 4 | try: 5 | from rdeditor._version import __version__ 6 | except ImportError: # pragma: no cover 7 | __version__ = "not-installed" 8 | 9 | from .rdEditor import MainWindow 10 | -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/Change_E_Z.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/Change_E_Z.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/Change_E_Z.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 56 | 65 | E 76 | Z 87 | / 98 | 99 | 100 | -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/Change_R_S.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/Change_R_S.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/CleanupChem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/CleanupChem.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/RecalcCoord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/RecalcCoord.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/about.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/appicon.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/atommapnumber.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/atommapnumber.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/benzene.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/benzene.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/calc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/calc.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/convert.sh: -------------------------------------------------------------------------------- 1 | for file in *.png; do 2 | convert "$file" -negate "$file" 3 | done -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/cyclohexane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/cyclohexane.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/exit.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/grid.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Broom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Broom.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Bulleted List.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Bulleted List.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Cancel.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Carbon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Carbon.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Cursor.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Decrease Font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Decrease Font.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Delete.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Double.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Double.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Edit.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Exit.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Hydrogen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Hydrogen.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Increase Font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Increase Font.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Info.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Left.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Line.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Minus.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Molecule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Molecule.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Open.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Oxygen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Oxygen.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Physics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Physics.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Pinch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Pinch.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Plus.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Redo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Redo.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Replace Atom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Replace Atom.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Reset.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Right.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Save as.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Save as.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Save.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Scatter Plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Scatter Plot.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Shutdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Shutdown.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Single.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Single.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Trash.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Triple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Triple.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-Undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-Undo.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-copy-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-copy-96.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/icons8-paste-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/icons8-paste-100.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/molblock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/molblock.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/next.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/open.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/plot.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/prev.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/ptable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/dark/application/ptable.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/application/ptable.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 25 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 60 | 69 | 72 | 79 | 86 | 93 | 100 | 107 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /rdeditor/icon_themes/dark/index.theme: -------------------------------------------------------------------------------- 1 | [Icon Theme] 2 | Name=dark 3 | Directories=application, 4 | 5 | 6 | [application] 7 | Size=128 8 | Context=Applications -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/Change_E_Z.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/Change_E_Z.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/Change_E_Z.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 56 | 65 | E 76 | Z 87 | / 98 | 99 | 100 | -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/Change_R_S.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/Change_R_S.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/CleanupChem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/CleanupChem.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/CleanupChem.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 43 | 45 | 50 | 53 | 71 | 78 | 79 | 84 | 89 | 90 | 95 | 96 | -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/RecalcCoord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/RecalcCoord.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/RecalcCoord.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 43 | 45 | 50 | 55 | 60 | X,Y 71 | 78 | 85 | 86 | 91 | 92 | -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/about.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/appicon copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/appicon copy.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/appicon.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/appicon.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/appicon.svg.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/atommapnumber.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/atommapnumber.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/atommapnumber.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 42 | 44 | 49 | 54 | 59 | 60 | 65 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/benzene.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/benzene.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/benzene.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 42 | 44 | 49 | 67 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/calc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/calc.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/cyclohexane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/cyclohexane.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/cyclohexane.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 42 | 44 | 49 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/exit.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/grid.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Broom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Broom.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Bulleted List.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Bulleted List.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Cancel.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Carbon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Carbon.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Cursor.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Decrease Font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Decrease Font.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Delete.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Double.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Double.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Edit.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Exit.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Hydrogen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Hydrogen.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Increase Font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Increase Font.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Info.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Left.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Line.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Minus.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Molecule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Molecule.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Open.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Oxygen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Oxygen.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Physics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Physics.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Pinch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Pinch.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Plus.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Redo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Redo.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Replace Atom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Replace Atom.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Reset.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Right.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Save as.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Save as.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Save.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Scatter Plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Scatter Plot.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Shutdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Shutdown.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Single.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Single.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Trash.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Triple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Triple.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-Undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-Undo.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-copy-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-copy-96.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/icons8-paste-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/icons8-paste-100.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/molblock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/molblock.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/next.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/open.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/plot.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/prev.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/ptable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EBjerrum/rdeditor/cc46c84a55a0f47016356600a725e008ca71b902/rdeditor/icon_themes/light/application/ptable.png -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/application/ptable.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 25 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 60 | 69 | 72 | 79 | 86 | 93 | 100 | 107 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /rdeditor/icon_themes/light/index.theme: -------------------------------------------------------------------------------- 1 | [Icon Theme] 2 | Name=light 3 | Directories=application, 4 | 5 | 6 | [application] 7 | Size=128 8 | Context=Applications -------------------------------------------------------------------------------- /rdeditor/molEditWidget.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Import required modules 3 | from PySide6 import QtCore, QtGui, QtSvg, QtWidgets 4 | 5 | from PySide6.QtWidgets import QApplication, QMainWindow 6 | from PySide6.QtSvgWidgets import QSvgWidget 7 | from PySide6.QtCore import Qt, QPointF 8 | from PySide6.QtGui import QMouseEvent, QPainter, QPen 9 | import math 10 | 11 | 12 | import sys 13 | import logging 14 | from warnings import warn 15 | import copy 16 | 17 | 18 | import numpy as np 19 | from rdkit import Chem 20 | from rdkit.Chem import AllChem 21 | from rdkit.Chem import Draw 22 | from rdkit.Chem import rdDepictor 23 | from rdkit.Chem.Draw import rdMolDraw2D 24 | from rdkit.Geometry.rdGeometry import Point2D, Point3D 25 | 26 | # from rdkit.Chem.AllChem import GenerateDepictionMatching3DStructure 27 | 28 | from .molViewWidget import MolWidget 29 | from .templatehandler import TemplateHandler 30 | 31 | # from types import * 32 | 33 | from .ptable import symboltoint 34 | 35 | 36 | debug = True # TODO is this still used? 37 | 38 | 39 | # The Molblock editor class 40 | class MolEditWidget(MolWidget): 41 | def __init__(self, mol=None, parent=None): 42 | # Also init the super class 43 | super(MolEditWidget, self).__init__(parent) 44 | # This sets the window to delete itself when its closed, so it doesn't keep querying the model 45 | self.setAttribute(QtCore.Qt.WA_DeleteOnClose) 46 | self.is_dragging = False # If a drag event is being performed 47 | 48 | # Templater handler 49 | self.templatehandler = TemplateHandler() 50 | self.sanitize_on_cleanup = True 51 | self.kekulize_on_cleanup = True 52 | 53 | # Properties 54 | self._prevmol = None # For undo 55 | self.coordlist = None # SVG coords of the current mols atoms 56 | 57 | # Standard atom, bond and ring types 58 | self.symboltoint = symboltoint 59 | self.bondtypes = Chem.rdchem.BondType.names # A dictionary with all available rdkit bondtypes 60 | self.available_rings = self.templatehandler.templateslabels # ["ALI6", "ARO6"] 61 | 62 | # Default actions 63 | self._action = "Add" 64 | # self._chemEntityType = "bond" 65 | # self._chemEntitySubType = self.bondtypes["SINGLE"] 66 | self.chemEntity = self.bondtypes["SINGLE"] 67 | 68 | # Points to calculate the SVG to coord scaling 69 | self.points = [Point2D(0, 0), Point2D(1, 1)] 70 | 71 | # Bind signals to slots 72 | self.finishedDrawing.connect(self.update_coordlist) # When drawing finished, update coordlist of SVG atoms. 73 | 74 | # Init with a mol if passed at construction 75 | # if mol != None: 76 | self.mol = mol 77 | 78 | # Getters and Setters for properties 79 | actionChanged = QtCore.Signal(name="actionChanged") 80 | 81 | @property 82 | def action(self): 83 | return self._action 84 | 85 | @action.setter # TODO make it more explicit what actions are available here. 86 | def action(self, actionname): 87 | if actionname != self.action: 88 | self._action = actionname 89 | self.actionChanged.emit() 90 | 91 | def setAction(self, actionname): 92 | self.action = actionname 93 | 94 | # bondTypeChanged = QtCore.Signal(name="bondTypeChanged") 95 | 96 | # chemEntityTypeChanged = QtCore.Signal(name="chemEntityTypeChanged") 97 | 98 | @property 99 | def chemEntity(self): 100 | return self._chementity 101 | 102 | @chemEntity.setter 103 | def chemEntity(self, chementity): 104 | if isinstance(chementity, Chem.rdchem.BondType): # Bondtypes are also ints, but ints are not BondTypes 105 | self.setBond(chementity) 106 | elif isinstance(chementity, int): 107 | self.setAtom(chementity) 108 | elif isinstance(chementity, str): 109 | if chementity in self.bondtypes: 110 | self.setBond(chementity) 111 | elif chementity in self.available_rings: 112 | self.setRing(chementity) 113 | elif chementity in symboltoint.keys(): 114 | self.setAtom(chementity) 115 | else: 116 | self.logger.error(f"Unknown string entity type with value {chementity}") 117 | return 118 | else: 119 | self.logger.error(f"Unknown type {type(chementity)}") 120 | return 121 | # self.logger.debug(f"ChemEntity set for {chementity} of type {type(chementity)}") 122 | 123 | def setChemEntity(self, chementity): 124 | self.chemEntity = chementity 125 | 126 | # Readonly, inferred from chemEntity 127 | @property 128 | def chemEntityType(self): 129 | return self._chementitytype 130 | 131 | @property 132 | def bondtype(self): 133 | warn( 134 | ".bondtype has been deprecated, in favor of .chemEntityType and setter .chemEntity.", 135 | DeprecationWarning, 136 | stacklevel=2, 137 | ) 138 | return self._bondtype 139 | 140 | @property 141 | def ringtype(self): 142 | warn( 143 | ".ringtype has been deprecated, in favor of .chemEntityType and setter .chemEntity.", 144 | DeprecationWarning, 145 | stacklevel=2, 146 | ) 147 | return self._ringtype 148 | 149 | @bondtype.setter 150 | def bondtype(self, bondtype): 151 | warn( 152 | ".bondtype has been deprecated, in favor of .chemEntityType and setter .chemEntity.", 153 | DeprecationWarning, 154 | stacklevel=2, 155 | ) 156 | if bondtype != self.bondtype: 157 | self._bondtype = bondtype 158 | # self.bondTypeChanged.emit() 159 | 160 | @ringtype.setter 161 | def ringtype(self, ringtype): 162 | warn( 163 | ".ringtype has been deprecated, in favor of .chemEntityType and setter .chemEntity.", 164 | DeprecationWarning, 165 | stacklevel=2, 166 | ) 167 | if ringtype != self.ringtype: 168 | self._ringtype = ringtype 169 | 170 | def setRing(self, ringtype): 171 | if ringtype in self.available_rings: 172 | self._chementitytype = "ring" 173 | self._chementity = ringtype 174 | else: 175 | self.logger.error(f"Currently only {self.available_rings} are supported.") 176 | 177 | def setBond(self, bondtype): 178 | if isinstance(bondtype, Chem.rdchem.BondType): 179 | self._chementitytype = "bond" 180 | self._chementity = bondtype 181 | 182 | elif isinstance(bondtype, str): 183 | assert bondtype in self.bondtypes.keys(), "Bondtype %s not known" % bondtype 184 | self._chementitytype = "bond" 185 | self._chementity = self.bondtypes[bondtype] 186 | else: 187 | self.logger.error("Bondtype must be string or rdchem.BondType, not %s" % type(bondtype)) 188 | 189 | @property 190 | def atomtype(self): 191 | warn( 192 | ".atomtype has been deprecated, in favor of .chemEntityType and setter .chemEntity.", 193 | DeprecationWarning, 194 | stacklevel=2, 195 | ) 196 | return self._atomtype 197 | 198 | @atomtype.setter 199 | def atomtype(self, atomtype): 200 | warn( 201 | ".atomtype has been deprecated, in favor of .chemEntityType and setter .chemEntity.", 202 | DeprecationWarning, 203 | stacklevel=2, 204 | ) 205 | if atomtype != self.atomtype: 206 | self._atomtype = atomtype 207 | 208 | def setAtom(self, atomtype): 209 | self.logger.debug("Setting atomtype selection to %s" % atomtype) 210 | if atomtype in self.symboltoint.keys(): 211 | self.logger.debug("Atomtype found in keys") 212 | # self.atomtype = self.symboltoint[atomtype] 213 | self._chementitytype = "atom" 214 | self._chementity = self.symboltoint[atomtype] 215 | elif isinstance(atomtype, int): 216 | if atomtype in self.symboltoint.values(): 217 | self._chementitytype = "atom" 218 | self._chementity = atomtype 219 | else: 220 | self.logger.error(f"Atom number {atomtype} not known.") 221 | else: 222 | self.logger.error("Atomtype must be string or integer, not %s" % type(atomtype)) 223 | 224 | # Function to translate from SVG coords to atom coords using scaling calculated from atomcoords (0,0) and (1,1) 225 | # Returns rdkit Point2D 226 | def SVG_to_coord(self, x_svg, y_svg): 227 | if self.drawer is not None: 228 | scale0 = self.drawer.GetDrawCoords(self.points[0]) 229 | scale1 = self.drawer.GetDrawCoords(self.points[1]) 230 | 231 | ax = scale1.x - scale0.x 232 | bx = scale0.x 233 | 234 | ay = scale1.y - scale0.y 235 | by = scale0.y 236 | 237 | return Point2D((x_svg - bx) / ax, (y_svg - by) / ay) 238 | else: 239 | return Point2D(0.0, 0.0) 240 | 241 | def update_coordlist(self): 242 | if self.mol is not None: 243 | self.coordlist = np.array([list(self.drawer.GetDrawCoords(i)) for i in range(self.mol.GetNumAtoms())]) 244 | self.logger.debug("Current coordlist:\n%s" % self.coordlist) 245 | else: 246 | self.coordlist = None 247 | 248 | def get_nearest_atom(self, x_svg, y_svg): 249 | if self.mol is not None and self.mol.GetNumAtoms() > 0: 250 | atomsvgcoords = np.array([x_svg, y_svg]) 251 | # find distance, https://codereview.stackexchange.com/questions/28207/finding-the-closest-point-to-a-list-of-points 252 | deltas = self.coordlist - atomsvgcoords 253 | dist_2 = np.einsum("ij,ij->i", deltas, deltas) 254 | min_idx = np.argmin(dist_2) 255 | return min_idx, dist_2[min_idx] ** 0.5 256 | else: 257 | return None, 1e10 # Return ridicilous long distance so that its not chosen 258 | 259 | def get_nearest_bond(self, x_svg, y_svg): 260 | if self.mol is not None and len(self.mol.GetBonds()) > 0: 261 | bondlist = [] 262 | for bond in self.mol.GetBonds(): 263 | bi = bond.GetBeginAtomIdx() 264 | ei = bond.GetEndAtomIdx() 265 | avgcoords = np.mean(self.coordlist[[bi, ei]], axis=0) 266 | bondlist.append(avgcoords) 267 | 268 | bondlist = np.array(bondlist) 269 | # if not bondlist: # If there's no bond 270 | # return None, 1e10 271 | atomsvgcoords = np.array([x_svg, y_svg]) 272 | deltas = bondlist - atomsvgcoords 273 | dist_2 = np.einsum("ij,ij->i", deltas, deltas) 274 | min_idx = np.argmin(dist_2) 275 | return min_idx, dist_2[min_idx] ** 0.5 276 | else: 277 | return None, 1e10 # Return ridicilous long distance so that its not chosen 278 | 279 | # Function that translates coodinates from an event into a molobject 280 | def get_molobject(self, event): 281 | # Recalculate to SVG coords 282 | viewbox = self.renderer().viewBox() 283 | size = self.size() 284 | 285 | x = event.pos().x() 286 | y = event.pos().y() 287 | # Rescale, divide by the size of the widget, multiply by the size of the viewbox + offset. 288 | x_svg = float(x) / size.width() * viewbox.width() + viewbox.left() 289 | y_svg = float(y) / size.height() * viewbox.height() + viewbox.top() 290 | self.logger.debug("SVG_coords:\t%s\t%s" % (x_svg, y_svg)) 291 | # Identify Nearest atomindex 292 | atom_idx, atom_dist = self.get_nearest_atom(x_svg, y_svg) 293 | bond_idx, bond_dist = self.get_nearest_bond(x_svg, y_svg) 294 | self.logger.debug("Distances to atom %0.2F, bond %0.2F" % (atom_dist, bond_dist)) 295 | # If not below a given threshold, then it was not clicked 296 | if min([atom_dist, bond_dist]) < 14.0: 297 | if atom_dist < bond_dist: 298 | return self.mol.GetAtomWithIdx(int(atom_idx)) 299 | else: 300 | return self.mol.GetBondWithIdx(int(bond_idx)) 301 | else: 302 | # Translate SVG to Coords 303 | return self.SVG_to_coord(x_svg, y_svg) 304 | 305 | def mousePressEvent(self, event): 306 | if event.button() is QtCore.Qt.LeftButton: 307 | # For visual feedback on the dragging event 308 | self.press_pos = event.position() 309 | self.current_pos = event.position() 310 | self.is_dragging = True 311 | 312 | # For chemistry 313 | self.start_molobject = self.get_molobject(event) 314 | 315 | def mouseMoveEvent(self, event: QMouseEvent): 316 | if self.is_dragging: 317 | self.current_pos = event.position() 318 | self.update() 319 | 320 | def mouseReleaseEvent(self, event): 321 | if event.button() is QtCore.Qt.LeftButton: 322 | end_mol_object = self.get_molobject(event) 323 | if self.is_same_object(self.start_molobject, end_mol_object): 324 | self.event_handler(self.start_molobject, None) # Click events has None as second object 325 | else: 326 | self.event_handler( 327 | self.start_molobject, end_mol_object 328 | ) # Drag events has different objects as start and end 329 | self.start_molobject = None 330 | 331 | self.is_dragging = False 332 | self.update() # Final repaint to clear the line 333 | 334 | def paintEvent(self, event): 335 | super().paintEvent(event) # Render the SVG (Molecule) 336 | 337 | # Paint a line from where the canvas was clicked to the current position. 338 | if self.is_dragging: 339 | painter = QPainter(self) 340 | pen = QPen(Qt.gray, 4, Qt.SolidLine) 341 | painter.setPen(pen) 342 | painter.drawLine(self.press_pos, self.current_pos) 343 | 344 | def is_same_object(self, object1, object2): 345 | if isinstance(object1, Chem.rdchem.Atom) and isinstance(object2, Chem.rdchem.Atom): 346 | return object1.GetIdx() == object2.GetIdx() 347 | if isinstance(object1, Chem.rdchem.Bond) and isinstance(object2, Chem.rdchem.Bond): 348 | return object1.GetIdx() == object2.GetIdx() 349 | if isinstance(object1, Point2D) and isinstance(object2, Point2D): 350 | distance = (object1 - object2).Length() 351 | self.logger.debug(f"Dragged distance on Canvas {distance}") 352 | if distance < 0.1: 353 | return True 354 | return False 355 | 356 | # def clicked_handler(self, clicked): 357 | # try: 358 | # self.event_handler(clicked, None) 359 | # except Exception as e: 360 | # self.logger.error(f"Error in clicked_handler: {e}") 361 | 362 | # def drag_handler(self, object1, object2): 363 | # try: 364 | # self.event_handler(object1, object2) 365 | # except Exception as e: 366 | # self.logger.error(f"Error in drag_handler: {e}") 367 | 368 | def event_handler(self, object1, object2): 369 | # Matches which objects are clicked/dragged and what chemical type and action is selected 370 | # With click events, the second object is None 371 | # Canvas clicks and drags are Point2D objects 372 | match (object1, object2, self.chemEntityType, self.action): 373 | # Atom click events 374 | # different enitity types 375 | case (Chem.rdchem.Atom(), None, "atom", "Add"): 376 | self.replace_on_atom(object1) 377 | case (Chem.rdchem.Atom(), None, "ring", "Add"): 378 | self.add_ring_to_atom(object1) 379 | case (Chem.rdchem.Atom(), None, "bond", "Add"): 380 | self.add_bond_to_atom(object1) 381 | 382 | # Atom click with differentactions 383 | case (Chem.rdchem.Atom(), None, _, "Remove"): 384 | self.remove_atom(object1) 385 | case (Chem.rdchem.Atom(), None, _, "Select"): 386 | self.select_atom_add(object1) 387 | case (Chem.rdchem.Atom(), None, _, "Increase Charge"): 388 | self.increase_charge(object1) 389 | case (Chem.rdchem.Atom(), None, _, "Decrease Charge"): 390 | self.decrease_charge(object1) 391 | case (Chem.rdchem.Atom(), None, _, "Number Atom"): 392 | self.number_atom(object1) 393 | case (Chem.rdchem.Atom(), None, _, "RStoggle"): 394 | self.toogleRS(object1) 395 | 396 | # Bond click events 397 | case (Chem.rdchem.Bond(), None, _, "Add"): 398 | self.add_to_bond(object1) 399 | case (Chem.rdchem.Bond(), None, _, "Remove"): 400 | self.remove_bond(object1) 401 | case (Chem.rdchem.Bond(), None, _, "Select"): 402 | self.select_bond(object1) 403 | case (Chem.rdchem.Bond(), None, _, "Replace"): 404 | self.replace_on_bond(object1) 405 | case (Chem.rdchem.Bond(), None, _, "EZtoggle"): 406 | self.toogleEZ(object1) 407 | 408 | # Canvas click events 409 | case (Point2D(), None, _, "Add"): 410 | self.add_canvas_entity(object1) 411 | case (Point2D(), None, _, "Select"): 412 | self.clearAtomSelection() 413 | 414 | # Drag events 415 | # Atom to Atom 416 | case (Chem.rdchem.Atom(), Chem.rdchem.Atom(), _, "Add"): 417 | self.add_bond_between_atoms(object1, object2) 418 | 419 | # Atom to Canvas actions 420 | case (Chem.rdchem.Atom(), Point2D(), "atom", "Add"): 421 | self.add_atom_to_atom(object1) 422 | case (Chem.rdchem.Atom(), Point2D(), "ring", "Add"): 423 | self.add_bonded_ring_to_atom(object1) 424 | case (Chem.rdchem.Atom(), Point2D(), "bond", "Add"): 425 | self.add_bond_to_atom(object1) 426 | # Drag on canvas 427 | case (Point2D(), Point2D(), _, "Add"): 428 | self.canvas_drag(object1, object2) 429 | 430 | # Default case for undefined actions 431 | case _: 432 | self.logger.warning( 433 | f"Undefined action for combination: " 434 | f"{(type(object1), type(object2), self.chemEntityType, self.action)}" 435 | ) 436 | 437 | def atom_click(self, atom): 438 | self.logger.warn("atom_click is deprecated. Use event_handler instead.", DeprecationWarning, stacklevel=2) 439 | 440 | def atom_drag(self, atom): 441 | self.logger.warn("atom_drag is deprecated. Use event_handler instead.", DeprecationWarning, stacklevel=2) 442 | 443 | def bond_click(self, bond): 444 | self.logger.warn("bond_click is deprecated. Use event_handler instead.", DeprecationWarning, stacklevel=2) 445 | 446 | def canvas_click(self, point): 447 | self.logger.warn("canvas_click is deprecated. Use event_handler instead.", DeprecationWarning, stacklevel=2) 448 | 449 | def add_to_atom(self, atom): 450 | self.logger.warn("add_to_atom is deprecated. Use event_handler instead.", DeprecationWarning, stacklevel=2) 451 | 452 | def getNewAtom(self, chemEntity): 453 | newatom = Chem.rdchem.Atom(chemEntity) 454 | if newatom.GetAtomicNum() == 0: 455 | newatom.SetProp("dummyLabel", "R") 456 | return newatom 457 | 458 | def add_atom_to_atom(self, atom, chemEntity=None): 459 | if not chemEntity: 460 | chemEntity = self.chemEntity 461 | rwmol = Chem.rdchem.RWMol(self.mol) 462 | newatom = self.getNewAtom(chemEntity) 463 | newidx = rwmol.AddAtom(newatom) 464 | newbond = rwmol.AddBond(atom.GetIdx(), newidx, Chem.rdchem.BondType.SINGLE) 465 | self.mol = rwmol 466 | return self.mol.GetAtomWithIdx(newidx) 467 | 468 | def add_bond_to_atom(self, atom): 469 | rwmol = Chem.rdchem.RWMol(self.mol) 470 | newatom = Chem.rdchem.Atom(6) 471 | newidx = rwmol.AddAtom(newatom) 472 | newbond = rwmol.AddBond(atom.GetIdx(), newidx, order=self.chemEntity) 473 | self.mol = rwmol 474 | 475 | def add_bonded_ring_to_atom(self, atom): 476 | new_atom = self.add_atom_to_atom(atom, chemEntity="C") 477 | self.add_ring_to_atom(new_atom) 478 | 479 | def add_ring_to_atom(self, atom): 480 | mol = self.templatehandler.apply_template_to_atom(atom, self.chemEntity) 481 | self.mol = mol 482 | 483 | def add_to_bond(self, bond): 484 | if self.chemEntityType == "atom": 485 | self.toggle_bond(bond) 486 | if self.chemEntityType == "ring": 487 | self.add_ring_to_bond(bond) 488 | if self.chemEntityType == "bond": 489 | self.replace_bond(bond) 490 | 491 | def add_ring_to_bond(self, bond): 492 | mol = self.templatehandler.apply_template_to_bond(bond, self.chemEntity) 493 | self.mol = mol 494 | 495 | def add_canvas_entity(self, point): 496 | if self.chemEntityType == "atom": 497 | self.add_canvas_atom(point) 498 | if self.chemEntityType == "ring": 499 | self.add_canvas_ring(point) 500 | if self.chemEntityType == "bond": 501 | self.add_canvas_bond(point) 502 | 503 | def add_canvas_atom(self, point, chemEntity=None): 504 | if chemEntity is None: 505 | chemEntity = self.chemEntity 506 | rwmol = Chem.rdchem.RWMol(self.mol) 507 | if rwmol.GetNumAtoms() == 0: 508 | point.x = 0.0 509 | point.y = 0.0 510 | newatom = self.getNewAtom(chemEntity) 511 | newidx = rwmol.AddAtom(newatom) 512 | # This should only trigger if we have an empty canvas 513 | if not rwmol.GetNumConformers(): 514 | rdDepictor.Compute2DCoords(rwmol) 515 | conf = rwmol.GetConformer(0) 516 | p3 = Point3D(point.x, point.y, 0) 517 | conf.SetAtomPosition(newidx, p3) 518 | self.mol = rwmol 519 | return self.mol.GetAtomWithIdx(newidx) 520 | 521 | def add_canvas_bond(self, point, point2=None): 522 | rwmol = Chem.rdchem.RWMol(self.mol) 523 | if rwmol.GetNumAtoms() == 0: 524 | point.x = 0.0 525 | point.y = 0.0 526 | 527 | atom_0 = rwmol.AddAtom(Chem.rdchem.Atom(6)) 528 | atom_1 = rwmol.AddAtom(Chem.rdchem.Atom(6)) 529 | newidx = rwmol.AddBond(atom_0, atom_1, order=self.chemEntity) 530 | 531 | # This should only trigger if we have an empty canvas 532 | if not rwmol.GetNumConformers(): 533 | rdDepictor.Compute2DCoords(rwmol) 534 | conf = rwmol.GetConformer(0) 535 | p3 = Point3D(point.x, point.y, 0) 536 | conf.SetAtomPosition(atom_0, p3) 537 | if point2: 538 | p3 = Point3D(point2.x, point2.y, 0) 539 | conf.SetAtomPosition(atom_1, p3) 540 | self.mol = rwmol 541 | 542 | def add_canvas_ring(self, point): 543 | mol = self.templatehandler.apply_template_to_canvas(self.mol, point, self.chemEntity) 544 | self.mol = mol 545 | 546 | def remove_atom(self, atom): 547 | rwmol = Chem.rdchem.RWMol(self.mol) 548 | rwmol.RemoveAtom(atom.GetIdx()) 549 | self.clearAtomSelection() # Removing atoms updates Idx'es 550 | self.mol = rwmol 551 | 552 | def select_atom(self, atom): 553 | self.selectAtom(atom.GetIdx()) 554 | # TODO make an unselect atom function 555 | 556 | def select_atom_add(self, atom): 557 | selidx = atom.GetIdx() 558 | if selidx in self._selectedAtoms: 559 | self.unselectAtom(selidx) 560 | else: 561 | self.selectAtomAdd(selidx) 562 | 563 | def replace_on_atom(self, atom): 564 | if self.chemEntityType == "atom": 565 | self.replace_atom(atom) 566 | else: 567 | pass 568 | 569 | def replace_atom(self, atom): 570 | rwmol = Chem.rdchem.RWMol(self.mol) 571 | newatom = self.getNewAtom(self.chemEntity) 572 | rwmol.ReplaceAtom(atom.GetIdx(), newatom) 573 | self.mol = rwmol 574 | 575 | # Double step action 576 | def add_bond_to_last_selected(self, atom): 577 | self.logger.warn( 578 | "add_bond_to_last_selected is deprecated. Use event_handler instead.", DeprecationWarning, stacklevel=2 579 | ) 580 | 581 | def add_bond_between_atoms(self, atom1, atom2): 582 | rwmol = Chem.rdchem.RWMol(self.mol) 583 | neighborIdx = [atm.GetIdx() for atm in atom1.GetNeighbors()] 584 | if atom2.GetIdx() not in neighborIdx: # check if bond already exists 585 | bondType = self.chemEntity if self.chemEntityType == "bond" else Chem.rdchem.BondType.SINGLE 586 | rwmol.AddBond(atom1.GetIdx(), atom2.GetIdx(), order=bondType) 587 | self.mol = rwmol 588 | 589 | def canvas_drag(self, point1, point2): 590 | if self.chemEntityType == "atom": 591 | self.canvas_drag_atom(point1, point2) 592 | if self.chemEntityType == "ring": 593 | self.canvas_drag_ring(point1, point2) 594 | if self.chemEntityType == "bond": 595 | self.canvas_drag_bond(point1, point2) 596 | 597 | def canvas_drag_atom(self, point1, point2): 598 | # In essence adding a bond, but can be between non-carbon atoms, and make behaviour more consistent 599 | # i.e. if drag-drawing) 600 | rwmol = Chem.rdchem.RWMol(self.mol) 601 | newatom = self.getNewAtom(self.chemEntity) 602 | newatom2 = self.getNewAtom(self.chemEntity) 603 | newidx = rwmol.AddAtom(newatom) 604 | newidx2 = rwmol.AddAtom(newatom2) 605 | newbond = rwmol.AddBond(newidx, newidx2, Chem.rdchem.BondType.SINGLE) 606 | self.mol = rwmol 607 | 608 | def canvas_drag_ring(self, point1, point2): 609 | # TODO, in principle to be consistent we should be adding a two templates with a bond in between?? 610 | newatom = self.add_canvas_atom(point1, chemEntity="C") 611 | self.add_bonded_ring_to_atom(newatom) 612 | 613 | def canvas_drag_bond(self, point1, point2): 614 | self.add_canvas_bond(point1, point2) 615 | 616 | def toogleRS(self, atom): 617 | self.backupMol() 618 | # atom = self._mol.GetAtomWithIdx(atom.GetIdx()) 619 | stereotype = atom.GetChiralTag() 620 | self.logger.debug("Current stereotype of clicked atom %s" % stereotype) 621 | stereotypes = [ 622 | Chem.rdchem.ChiralType.CHI_TETRAHEDRAL_CCW, 623 | # Chem.rdchem.ChiralType.CHI_OTHER, this one doesn't show a wiggly bond 624 | Chem.rdchem.ChiralType.CHI_UNSPECIFIED, 625 | Chem.rdchem.ChiralType.CHI_TETRAHEDRAL_CW, 626 | Chem.rdchem.ChiralType.CHI_TETRAHEDRAL_CCW, 627 | ] 628 | newidx = np.argmax(np.array(stereotypes) == stereotype) + 1 629 | atom.SetChiralTag(stereotypes[newidx]) 630 | self.logger.debug("New stereotype set to %s" % atom.GetChiralTag()) 631 | # rdDepictor.Compute2DCoords(self._mol) 632 | # self._mol.ClearComputedProps() 633 | self._mol.UpdatePropertyCache(strict=False) 634 | rdDepictor.Compute2DCoords(self._mol) 635 | self.molChanged.emit() 636 | 637 | def assert_stereo_atoms(self, bond): 638 | if len(bond.GetStereoAtoms()) == 0: 639 | # get atoms and idx's of bond 640 | bondatoms = [bond.GetBeginAtom(), bond.GetEndAtom()] 641 | bondidx = [atom.GetIdx() for atom in bondatoms] 642 | 643 | # Figure out the atom idx's of the neigbor atoms, that are NOT the other end of the bond 644 | stereoatoms = [] 645 | for bondatom in bondatoms: 646 | neighboridxs = [atom.GetIdx() for atom in bondatom.GetNeighbors()] 647 | neighboridx = [idx for idx in neighboridxs if idx not in bondidx][0] 648 | stereoatoms.append(neighboridx) 649 | # Set the bondstereoatoms 650 | bond.SetStereoAtoms(stereoatoms[0], stereoatoms[1]) 651 | self.logger.debug(f"Setting StereoAtoms to {stereoatoms}") 652 | else: 653 | pass 654 | 655 | def assign_stereo_atoms(self, mol: Chem.Mol): 656 | self.logger.debug("Identifying stereo atoms") 657 | mol_copy = copy.deepcopy(mol) 658 | Chem.SanitizeMol(mol_copy, sanitizeOps=Chem.rdmolops.SanitizeFlags.SANITIZE_SYMMRINGS) 659 | Chem.rdmolops.FindPotentialStereoBonds(mol_copy, cleanIt=True) 660 | for i, bond in enumerate(mol_copy.GetBonds()): 661 | stereoatoms = list( 662 | set(bond.GetStereoAtoms()) 663 | ) # Is FindPotentialStereoBonds are run successively, the list is simply expanded. 664 | if stereoatoms: 665 | try: 666 | mol.GetBondWithIdx(i).SetStereoAtoms(stereoatoms[0], stereoatoms[1]) 667 | except RuntimeError: 668 | mol.GetBondWithIdx(i).SetStereoAtoms( 669 | stereoatoms[1], stereoatoms[0] 670 | ) # Not sure why this can get the wrong way. Seem to now work correctly for Absisic Acid 671 | 672 | def toogleEZ(self, bond: Chem.Bond): 673 | self.backupMol() 674 | 675 | stereotype = bond.GetStereo() # TODO, when editing the molecule, we could change the CIP rules? 676 | # so stereo assignment need to be updated on other edits as well? 677 | self.logger.debug("Current stereotype of clicked atom %s" % stereotype) 678 | self.logger.debug(f"StereoAtoms are {list(bond.GetStereoAtoms())}") 679 | self.logger.debug(f"Bond properties are {bond.GetPropsAsDict(includePrivate=True, includeComputed=True)}") 680 | 681 | self.assign_stereo_atoms(self._mol) # TODO, make something that ONLY works on a single bond? 682 | 683 | stereocycler = { 684 | Chem.rdchem.BondStereo.STEREONONE: Chem.rdchem.BondStereo.STEREOTRANS, 685 | Chem.rdchem.BondStereo.STEREOE: Chem.rdchem.BondStereo.STEREOCIS, 686 | Chem.rdchem.BondStereo.STEREOTRANS: Chem.rdchem.BondStereo.STEREOCIS, 687 | Chem.rdchem.BondStereo.STEREOZ: Chem.rdchem.BondStereo.STEREOANY, 688 | Chem.rdchem.BondStereo.STEREOCIS: Chem.rdchem.BondStereo.STEREOANY, 689 | Chem.rdchem.BondStereo.STEREOANY: Chem.rdchem.BondStereo.STEREONONE, 690 | } 691 | 692 | newstereotype = stereocycler[stereotype] 693 | bond.SetStereo(newstereotype) 694 | 695 | self.logger.debug("New stereotype set to %s" % bond.GetStereo()) 696 | self.logger.debug(f"StereoAtoms are {list(bond.GetStereoAtoms())}") 697 | self.logger.debug(f"Bond properties are {bond.GetPropsAsDict(includePrivate=True, includeComputed=True)}") 698 | 699 | self.logger.debug(f"StereoAtoms are {list(bond.GetStereoAtoms())}") 700 | self.logger.debug(f"Bond properties are {bond.GetPropsAsDict(includePrivate=True, includeComputed=True)}") 701 | 702 | self.molChanged.emit() 703 | 704 | # Bond actions 705 | def toggle_bond(self, bond): 706 | self.backupMol() 707 | bondtype = bond.GetBondType() 708 | bondtypes = [ 709 | Chem.rdchem.BondType.TRIPLE, 710 | Chem.rdchem.BondType.SINGLE, 711 | Chem.rdchem.BondType.DOUBLE, 712 | Chem.rdchem.BondType.TRIPLE, 713 | ] 714 | # Find the next type in the list based on current 715 | # If current is not in list? Then it selects the first and add 1 => SINGLE 716 | newidx = np.argmax(np.array(bondtypes) == bondtype) + 1 717 | newtype = bondtypes[newidx] 718 | bond.SetBondType(newtype) 719 | self.molChanged.emit() 720 | 721 | def replace_on_bond(self, bond): 722 | if self.chemEntityType == "atom": 723 | self.toggle_bond(bond) 724 | if self.chemEntityType == "ring": 725 | self.toggle_bond(bond) 726 | if self.chemEntityType == "bond": 727 | self.replace_bond(bond) 728 | 729 | def replace_bond(self, bond): 730 | self.backupMol() 731 | self.logger.debug("Replacing bond %s" % bond) 732 | bond.SetBondType(self.chemEntity) 733 | self.molChanged.emit() 734 | 735 | # self.remove_bond(bond) 736 | def remove_bond(self, bond): 737 | rwmol = Chem.rdchem.RWMol(self.mol) 738 | rwmol.RemoveBond(bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()) 739 | self.mol = rwmol 740 | 741 | def increase_charge(self, atom): 742 | self.backupMol() 743 | atom.SetFormalCharge(atom.GetFormalCharge() + 1) 744 | self.molChanged.emit() 745 | 746 | def decrease_charge(self, atom): 747 | self.backupMol() 748 | atom.SetFormalCharge(atom.GetFormalCharge() - 1) 749 | self.molChanged.emit() 750 | 751 | def number_atom(self, atom: Chem.Atom): 752 | atomMapNumber = atom.GetIntProp("molAtomMapNumber") if atom.HasProp("molAtomMapNumber") else 0 753 | (atomMapNumber, ok) = QtWidgets.QInputDialog.getInt( 754 | self, "Number Atom", "Atom number", value=atomMapNumber, minValue=0 755 | ) 756 | 757 | if not ok: 758 | return 759 | 760 | self.backupMol() 761 | if atomMapNumber == 0: 762 | atom.ClearProp("molAtomMapNumber") 763 | else: 764 | atom.SetProp("molAtomMapNumber", str(atomMapNumber)) 765 | self.molChanged.emit() 766 | 767 | # self.select_bond(bond) 768 | def select_bond(self, bond): 769 | self.logger.debug("Select_bond not implemented") # TODO 770 | 771 | def undo(self): 772 | self.mol = self._prevmol 773 | 774 | def backupMol(self): 775 | self._prevmol = copy.deepcopy(self.mol) 776 | 777 | def cleanup_mol(self): 778 | mol = copy.deepcopy(self.mol) 779 | if self.sanitize_on_cleanup: 780 | Chem.SanitizeMol(mol) 781 | if self.kekulize_on_cleanup: 782 | Chem.Kekulize(mol) 783 | # if Chem.MolToCXSmiles(self.mol) != Chem.MolToCXSmiles(mol): 784 | self.mol = mol 785 | 786 | 787 | if __name__ == "__main__": 788 | mol = Chem.MolFromSmiles("CCN(C)C1CCCCC1S") 789 | rdDepictor.Compute2DCoords(mol) 790 | myApp = QtWidgets.QApplication(sys.argv) 791 | molblockview = MolWidget(mol) 792 | molblockview.show() 793 | myApp.exec() 794 | -------------------------------------------------------------------------------- /rdeditor/molViewWidget.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Import required modules 3 | from __future__ import print_function 4 | from PySide6 import QtCore, QtGui, QtSvg, QtWidgets, QtSvgWidgets 5 | import sys 6 | import copy 7 | 8 | # from types import * 9 | import logging 10 | 11 | import numpy as np 12 | from rdkit import Chem 13 | from rdkit.Chem import AllChem 14 | from rdkit.Chem import Draw 15 | from rdkit.Chem import rdDepictor 16 | from rdkit.Chem.Draw import rdMolDraw2D 17 | from rdkit.Geometry.rdGeometry import Point2D 18 | 19 | from .utilities import validate_rgb 20 | 21 | 22 | # The Viewer Class 23 | class MolWidget(QtSvgWidgets.QSvgWidget): 24 | def __init__(self, mol=None, parent=None, moldrawoptions: rdMolDraw2D.MolDrawOptions = None): 25 | # Also init the super class 26 | super(MolWidget, self).__init__(parent) 27 | 28 | # logging 29 | logging.basicConfig() 30 | self.logger = logging.getLogger() 31 | self.loglevel = logging.WARNING 32 | 33 | # This sets the window to delete itself when its closed, so it doesn't keep lingering in the background 34 | self.setAttribute(QtCore.Qt.WA_DeleteOnClose) 35 | # Private Properties 36 | self._mol = None # The molecule 37 | self._drawmol = None # Molecule for drawing 38 | self.drawer = None # drawing object for producing SVG 39 | self._selectedAtoms = [] # List of selected atoms 40 | self._darkmode = False 41 | 42 | # Color settings 43 | self._unsanitizable_background_colour = None # (1, 0.75, 0.75) 44 | self._last_selected_highlight_colour = (1, 0.2, 0.2) 45 | self._selected_highlight_colour = (1, 0.5, 0.5) 46 | 47 | # Sanitization Settings 48 | self._kekulize = False 49 | self._sanitize = False 50 | self._updatepropertycache = False 51 | 52 | # Draw options 53 | if moldrawoptions is None: 54 | self._moldrawoptions = rdMolDraw2D.MolDraw2DSVG(300, 300).drawOptions() 55 | self._moldrawoptions.prepareMolsBeforeDrawing = True 56 | self._moldrawoptions.addStereoAnnotation = True 57 | self._moldrawoptions.unspecifiedStereoIsUnknown = False 58 | self._moldrawoptions.fixedBondLength = 25 59 | else: 60 | self._moldrawoptions = moldrawoptions 61 | 62 | # Bind signales to slots for automatic actions 63 | self.molChanged.connect(self.sanitize_draw) 64 | self.selectionChanged.connect(self.draw) 65 | self.drawSettingsChanged.connect(self.draw) 66 | self.sanitizeSignal.connect(self.changeSanitizeStatus) 67 | 68 | # Initialize class with the mol passed 69 | self.mol = mol 70 | 71 | ##Properties and their wrappers 72 | @property 73 | def loglevel(self): 74 | return self.logger.level 75 | 76 | @loglevel.setter 77 | def loglevel(self, loglvl): 78 | self.logger.setLevel(loglvl) 79 | 80 | @property 81 | def darkmode(self): 82 | return self._darkmode 83 | 84 | @darkmode.setter 85 | def darkmode(self, value: bool): 86 | self._darkmode = bool(value) 87 | self.draw() 88 | 89 | @property 90 | def moldrawoptions(self): 91 | """Returns the current drawing options. 92 | If settings aremanipulated directly, a drawSettingsChanged signal is not emitted, 93 | consider using setDrawOption instead.""" 94 | return self._moldrawoptions 95 | 96 | @moldrawoptions.setter 97 | def moldrawoptions(self, value): 98 | self._moldrawoptions = value 99 | self.drawSettingsChanged.emit() 100 | 101 | def getDrawOption(self, attribute): 102 | return getattr(self._moldrawoptions, attribute) 103 | 104 | def setDrawOption(self, attribute, value): 105 | setattr(self._moldrawoptions, attribute, value) 106 | self.drawSettingsChanged.emit() 107 | 108 | # Getter and setter for mol 109 | molChanged = QtCore.Signal(name="molChanged") 110 | 111 | @property 112 | def mol(self): 113 | return self._mol 114 | 115 | @mol.setter 116 | def mol(self, mol): 117 | if mol is None: 118 | mol = Chem.MolFromSmiles("") 119 | if mol != self._mol: 120 | # assert isinstance(mol, Chem.Mol) 121 | if self._mol is not None: 122 | self._prevmol = copy.deepcopy(self._mol) # Chem.Mol(self._mol.ToBinary()) # Copy 123 | 124 | # Fix pseudo atoms 125 | atom: Chem.Atom 126 | for atom in mol.GetAtoms(): 127 | if atom.GetAtomicNum() == 0: 128 | if not atom.HasProp("dummyLabel") or atom.GetProp("dummyLabel") == "*": 129 | atom.SetProp("dummyLabel", "R") 130 | else: 131 | print(atom.GetPropsAsDict()) 132 | 133 | # # TODO make this failsafe 134 | # if self._updatepropertycache: 135 | # try: 136 | # mol.UpdatePropertyCache(strict=False) 137 | # except Exception as e: 138 | # self.sanitizeSignal.emit("UpdatePropertyCache FAIL") 139 | # self.logger.error("Update Property Cache failed") 140 | 141 | # if self._sanitize: 142 | # Chem.SanitizeMol(mol) 143 | # if self._kekulize: 144 | # Chem.Kekulize(mol) 145 | self._mol = mol 146 | self.molChanged.emit() 147 | 148 | def setMol(self, mol): 149 | self.mol = mol 150 | 151 | # Handling of selections 152 | selectionChanged = QtCore.Signal(name="selectionChanged") 153 | 154 | def selectAtomAdd(self, atomidx): 155 | if atomidx not in self._selectedAtoms: 156 | self._selectedAtoms.append(atomidx) 157 | self.selectionChanged.emit() 158 | 159 | def selectAtom(self, atomidx): 160 | self._selectedAtoms = [atomidx] 161 | self.selectionChanged.emit() 162 | 163 | def unselectAtom(self, atomidx): 164 | self.selectedAtoms.remove(atomidx) 165 | self.selectionChanged.emit() 166 | 167 | def clearAtomSelection(self): 168 | if self._selectedAtoms != []: 169 | self._selectedAtoms = [] 170 | self.selectionChanged.emit() 171 | 172 | @property 173 | def selectedAtoms(self): 174 | return self._selectedAtoms 175 | 176 | @selectedAtoms.setter 177 | def selectedAtoms(self, atomlist): 178 | if atomlist != self._selectedAtoms: 179 | assert isinstance(atomlist, list), "selectedAtoms should be a list of integers" 180 | assert all(isinstance(item, int) for item in atomlist), "selectedAtoms should be a list of integers" 181 | self._selectedAtoms = atomlist 182 | self.selectionChanged.emit() 183 | 184 | def setSelectedAtoms(self, atomlist): 185 | self.selectedAtoms = atomlist 186 | 187 | drawSettingsChanged = QtCore.Signal(name="drawSettingsChanged") 188 | 189 | @property 190 | def unsanitizable_background_colour(self): 191 | return self._unsanitizable_background_colour 192 | 193 | @unsanitizable_background_colour.setter 194 | def unsanitizable_background_colour(self, colour): 195 | if colour != self._unsanitizable_background_colour: 196 | if colour is not None: 197 | assert validate_rgb(colour) 198 | self._unsanitizable_background_colour = colour 199 | self.drawSettingsChanged.emit() 200 | 201 | @property 202 | def last_selected_highlight_colour(self): 203 | return self._last_selected_highlight_colour 204 | 205 | @last_selected_highlight_colour.setter 206 | def last_selected_highlight_colour(self, colour): 207 | assert validate_rgb(colour) 208 | if colour != self._last_selected_highlight_colour: 209 | self._last_selected_highlight_colour = colour 210 | self.drawSettingsChanged.emit() 211 | 212 | @property 213 | def selected_highlight_colour(self): 214 | return self._selected_highlight_colour 215 | 216 | @selected_highlight_colour.setter 217 | def selected_highlight_colour(self, colour): 218 | if colour != self._selected_highlight_colour: 219 | assert validate_rgb(colour) 220 | self._selected_highlight_colour = colour 221 | self.drawSettingsChanged.emit() 222 | 223 | # Actions and functions 224 | @QtCore.Slot() 225 | def draw(self): 226 | self.logger.debug("Updating SVG") 227 | svg = self.getMolSvg() 228 | self.load(QtCore.QByteArray(svg.encode("utf-8"))) 229 | 230 | @QtCore.Slot() 231 | def sanitize_draw(self): 232 | # self.computeNewCoords() 233 | self.sanitizeDrawMol() 234 | self.draw() 235 | 236 | @QtCore.Slot() 237 | def changeSanitizeStatus(self, value): 238 | self.logger.debug(f"changeBorder called with value {value}") 239 | if value.upper() == "SANITIZABLE": 240 | self.molecule_sanitizable = True 241 | else: 242 | self.molecule_sanitizable = False 243 | 244 | def computeNewCoords(self, ignoreExisting=False, canonOrient=False): 245 | """Computes new coordinates for the molecule taking into account all 246 | existing positions (feeding these to the rdkit coordinate generation as 247 | prev_coords). 248 | """ 249 | # This code is buggy when you are not using the CoordGen coordinate 250 | # generation system, so we enable it here 251 | rdDepictor.SetPreferCoordGen(True) 252 | prev_coords = {} 253 | if self._mol.GetNumConformers() == 0: 254 | self.logger.debug("No Conformers found, computing all 2D coords") 255 | elif ignoreExisting: 256 | self.logger.debug("Ignoring existing conformers, computing all 2D coords") 257 | else: 258 | assert self._mol.GetNumConformers() == 1 259 | self.logger.debug("1 Conformer found, computing 2D coords not in found conformer") 260 | conf = self._mol.GetConformer(0) 261 | for a in self._mol.GetAtoms(): 262 | pos3d = conf.GetAtomPosition(a.GetIdx()) 263 | if (pos3d.x, pos3d.y) == (0, 0): 264 | continue 265 | prev_coords[a.GetIdx()] = Point2D(pos3d.x, pos3d.y) 266 | self.logger.debug("Coordmap %s" % prev_coords) 267 | self.logger.debug("canonOrient %s" % canonOrient) 268 | rdDepictor.Compute2DCoords(self._mol, coordMap=prev_coords, canonOrient=canonOrient) 269 | 270 | def canon_coords_and_draw(self): 271 | self.logger.debug("Recalculating coordinates") 272 | self.computeNewCoords(canonOrient=True, ignoreExisting=True) 273 | self._drawmol = copy.deepcopy(self._mol) # Chem.Mol(self._mol.ToBinary()) 274 | self.draw() 275 | 276 | def updateStereo(self): 277 | self.logger.debug("Updating stereo info") 278 | for atom in self.mol.GetAtoms(): 279 | if atom.HasProp("_CIPCode"): 280 | atom.ClearProp("_CIPCode") 281 | for bond in self.mol.GetBonds(): 282 | if bond.HasProp("_CIPCode"): 283 | bond.ClearProp("_CIPCode") 284 | Chem.rdmolops.SetDoubleBondNeighborDirections(self.mol) 285 | self.mol.UpdatePropertyCache(strict=False) 286 | Chem.rdCIPLabeler.AssignCIPLabels(self.mol) 287 | 288 | sanitizeSignal = QtCore.Signal(str, name="sanitizeSignal") 289 | 290 | def updateStereo(self): 291 | self.logger.debug("Updating stereo info") 292 | for atom in self.mol.GetAtoms(): 293 | if atom.HasProp("_CIPCode"): 294 | atom.ClearProp("_CIPCode") 295 | for bond in self.mol.GetBonds(): 296 | if bond.HasProp("_CIPCode"): 297 | bond.ClearProp("_CIPCode") 298 | Chem.rdmolops.SetDoubleBondNeighborDirections(self.mol) 299 | self.mol.UpdatePropertyCache(strict=False) 300 | Chem.rdCIPLabeler.AssignCIPLabels(self.mol) 301 | 302 | @QtCore.Slot() 303 | def sanitizeDrawMol(self, kekulize=False, drawkekulize=False): 304 | self.updateStereo() 305 | self.computeNewCoords() 306 | # self._drawmol_test = Chem.Mol(self._mol.ToBinary()) # Is this necessary? 307 | # self._drawmol = Chem.Mol(self._mol.ToBinary()) # Is this necessary? 308 | self._drawmol_test = copy.deepcopy(self._mol) # Is this necessary? 309 | self._drawmol = copy.deepcopy(self._mol) # Is this necessary? 310 | try: 311 | Chem.SanitizeMol(self._drawmol_test) 312 | self.sanitizeSignal.emit("Sanitizable") 313 | except Exception as e: 314 | self.sanitizeSignal.emit("UNSANITIZABLE") 315 | self.logger.warning("Unsanitizable") 316 | # try: 317 | # self._drawmol.UpdatePropertyCache(strict=False) 318 | # except Exception as e: 319 | # self.sanitizeSignal.emit("UpdatePropertyCache FAIL") 320 | # self.logger.error("Update Property Cache failed") 321 | # # Kekulize 322 | # if kekulize: 323 | # try: 324 | # Chem.Kekulize(self._drawmol) 325 | # except Exception as e: 326 | # self.logger.warning("Unkekulizable") 327 | # try: 328 | # self._drawmol = rdMolDraw2D.PrepareMolForDrawing(self._drawmol, kekulize=drawkekulize) 329 | # except ValueError: # <- can happen on a kekulization failure 330 | # self._drawmol = rdMolDraw2D.PrepareMolForDrawing(self._drawmol, kekulize=False) 331 | self._drawmol = rdMolDraw2D.PrepareMolForDrawing(self._drawmol, kekulize=False) 332 | 333 | finishedDrawing = QtCore.Signal(name="finishedDrawing") 334 | 335 | def getMolSvg(self): 336 | self.drawer = rdMolDraw2D.MolDraw2DSVG(300, 300) 337 | # TODO, what if self._drawmol doesn't exist? 338 | if self._drawmol is not None: 339 | # Chiral tags on R/S 340 | # chiraltags = Chem.FindMolChiralCenters(self._drawmol) 341 | self.drawer.SetDrawOptions(self._moldrawoptions) 342 | opts = self.drawer.drawOptions() 343 | if self._darkmode: 344 | rdMolDraw2D.SetDarkMode(opts) 345 | if (not self.molecule_sanitizable) and self.unsanitizable_background_colour: 346 | opts.setBackgroundColour(self.unsanitizable_background_colour) 347 | # opts.prepareMolsBeforeDrawing = True 348 | # opts.addStereoAnnotation = True # Show R/S and E/Z 349 | # opts.unspecifiedStereoIsUnknown = True # Show wiggly bond at undefined stereo centre 350 | # for tag in chiraltags: 351 | # idx = tag[0] 352 | # opts.atomLabels[idx] = self._drawmol.GetAtomWithIdx(idx).GetSymbol() + ":" + tag[1] 353 | 354 | if len(self._selectedAtoms) > 0: 355 | colors = {atom_idx: self.selected_highlight_colour for atom_idx in self._selectedAtoms} 356 | colors[self._selectedAtoms[-1]] = ( 357 | self.last_selected_highlight_colour 358 | ) # Color lastly selected an optionally different color 359 | self.drawer.DrawMolecule( 360 | self._drawmol, 361 | highlightAtoms=self._selectedAtoms, 362 | highlightAtomColors=colors, 363 | ) 364 | else: 365 | self.drawer.DrawMolecule(self._drawmol) 366 | self.drawer.FinishDrawing() 367 | self.finishedDrawing.emit() # Signal that drawer has finished 368 | svg = self.drawer.GetDrawingText().replace("svg:", "") 369 | return svg 370 | 371 | 372 | if __name__ == "__main__": 373 | # model = SDmodel() 374 | # model.loadSDfile('dhfr_3d.sd') 375 | mol = Chem.MolFromSmiles("CCN(C)c1ccccc1S") 376 | # rdDepictor.Compute2DCoords(mol) 377 | myApp = QtWidgets.QApplication(sys.argv) 378 | molview = MolWidget(mol) 379 | molview.selectAtom(1) 380 | molview.selectedAtoms = [1, 2, 3] 381 | molview.show() 382 | myApp.exec() 383 | -------------------------------------------------------------------------------- /rdeditor/ptable.py: -------------------------------------------------------------------------------- 1 | # This can probably be extracted from: 2 | # https://pypi.python.org/pypi/mendeleev/0.2.8#downloads 3 | 4 | ptable = { 5 | 1: {"Group": 1, "Name": "Hydrogen", "Period": 1, "Symbol": "H"}, 6 | 2: {"Group": 18, "Name": "Helium", "Period": 1, "Symbol": "He"}, 7 | 3: {"Group": 1, "Name": "Lithium", "Period": 2, "Symbol": "Li"}, 8 | 4: {"Group": 2, "Name": "Beryllium", "Period": 2, "Symbol": "Be"}, 9 | 5: {"Group": 13, "Name": "Boron", "Period": 2, "Symbol": "B"}, 10 | 6: {"Group": 14, "Name": "Carbon", "Period": 2, "Symbol": "C"}, 11 | 7: {"Group": 15, "Name": "Nitrogen", "Period": 2, "Symbol": "N"}, 12 | 8: {"Group": 16, "Name": "Oxygen", "Period": 2, "Symbol": "O"}, 13 | 9: {"Group": 17, "Name": "Fluorine", "Period": 2, "Symbol": "F"}, 14 | 10: {"Group": 18, "Name": "Neon", "Period": 2, "Symbol": "Ne"}, 15 | 11: {"Group": 1, "Name": "Sodium", "Period": 3, "Symbol": "Na"}, 16 | 12: {"Group": 2, "Name": "Magnesium", "Period": 3, "Symbol": "Mg"}, 17 | 13: {"Group": 13, "Name": "Aluminum", "Period": 3, "Symbol": "Al"}, 18 | 14: {"Group": 14, "Name": "Silicon", "Period": 3, "Symbol": "Si"}, 19 | 15: {"Group": 15, "Name": "Phosphorus", "Period": 3, "Symbol": "P"}, 20 | 16: {"Group": 16, "Name": "Sulfur", "Period": 3, "Symbol": "S"}, 21 | 17: {"Group": 17, "Name": "Chlorine", "Period": 3, "Symbol": "Cl"}, 22 | 18: {"Group": 18, "Name": "Argon", "Period": 3, "Symbol": "Ar"}, 23 | 19: {"Group": 1, "Name": "Potassium", "Period": 4, "Symbol": "K"}, 24 | 20: {"Group": 2, "Name": "Calcium", "Period": 4, "Symbol": "Ca"}, 25 | 21: {"Group": 3, "Name": "Scandium", "Period": 4, "Symbol": "Sc"}, 26 | 22: {"Group": 4, "Name": "Titanium", "Period": 4, "Symbol": "Ti"}, 27 | 23: {"Group": 5, "Name": "Vanadium", "Period": 4, "Symbol": "V"}, 28 | 24: {"Group": 6, "Name": "Chromium", "Period": 4, "Symbol": "Cr"}, 29 | 25: {"Group": 7, "Name": "Manganese", "Period": 4, "Symbol": "Mn"}, 30 | 26: {"Group": 8, "Name": "Iron", "Period": 4, "Symbol": "Fe"}, 31 | 27: {"Group": 9, "Name": "Cobalt", "Period": 4, "Symbol": "Co"}, 32 | 28: {"Group": 10, "Name": "Nickel", "Period": 4, "Symbol": "Ni"}, 33 | 29: {"Group": 11, "Name": "Copper", "Period": 4, "Symbol": "Cu"}, 34 | 30: {"Group": 12, "Name": "Zinc", "Period": 4, "Symbol": "Zn"}, 35 | 31: {"Group": 13, "Name": "Gallium", "Period": 4, "Symbol": "Ga"}, 36 | 32: {"Group": 14, "Name": "Germanium", "Period": 4, "Symbol": "Ge"}, 37 | 33: {"Group": 15, "Name": "Arsenic", "Period": 4, "Symbol": "As"}, 38 | 34: {"Group": 16, "Name": "Selenium", "Period": 4, "Symbol": "Se"}, 39 | 35: {"Group": 17, "Name": "Bromine", "Period": 4, "Symbol": "Br"}, 40 | 36: {"Group": 18, "Name": "Krypton", "Period": 4, "Symbol": "Kr"}, 41 | 37: {"Group": 1, "Name": "Rubidium", "Period": 5, "Symbol": "Rb"}, 42 | 38: {"Group": 2, "Name": "Strontium", "Period": 5, "Symbol": "Sr"}, 43 | 39: {"Group": 3, "Name": "Yttrium", "Period": 5, "Symbol": "Y"}, 44 | 40: {"Group": 4, "Name": "Zirconium", "Period": 5, "Symbol": "Zr"}, 45 | 41: {"Group": 5, "Name": "Niobium", "Period": 5, "Symbol": "Nb"}, 46 | 42: {"Group": 6, "Name": "Molybdenum", "Period": 5, "Symbol": "Mo"}, 47 | 43: {"Group": 7, "Name": "Technetium", "Period": 5, "Symbol": "Tc"}, 48 | 44: {"Group": 8, "Name": "Ruthenium", "Period": 5, "Symbol": "Ru"}, 49 | 45: {"Group": 9, "Name": "Rhodium", "Period": 5, "Symbol": "Rh"}, 50 | 46: {"Group": 10, "Name": "Palladium", "Period": 5, "Symbol": "Pd"}, 51 | 47: {"Group": 11, "Name": "Silver", "Period": 5, "Symbol": "Ag"}, 52 | 48: {"Group": 12, "Name": "Cadmium", "Period": 5, "Symbol": "Cd"}, 53 | 49: {"Group": 13, "Name": "Indium", "Period": 5, "Symbol": "In"}, 54 | 50: {"Group": 14, "Name": "Tin", "Period": 5, "Symbol": "Sn"}, 55 | 51: {"Group": 15, "Name": "Antimony", "Period": 5, "Symbol": "Sb"}, 56 | 52: {"Group": 16, "Name": "Tellurium", "Period": 5, "Symbol": "Te"}, 57 | 53: {"Group": 17, "Name": "Iodine", "Period": 5, "Symbol": "I"}, 58 | 54: {"Group": 18, "Name": "Xenon", "Period": 5, "Symbol": "Xe"}, 59 | 55: {"Group": 1, "Name": "Cesium", "Period": 6, "Symbol": "Cs"}, 60 | 56: {"Group": 2, "Name": "Barium", "Period": 6, "Symbol": "Ba"}, 61 | 57: {"Group": None, "Name": "Lanthanum", "Period": 6, "Symbol": "La"}, 62 | 58: {"Group": None, "Name": "Cerium", "Period": 6, "Symbol": "Ce"}, 63 | 59: {"Group": None, "Name": "Praseodymium", "Period": 6, "Symbol": "Pr"}, 64 | 60: {"Group": None, "Name": "Neodymium", "Period": 6, "Symbol": "Nd"}, 65 | 61: {"Group": None, "Name": "Promethium", "Period": 6, "Symbol": "Pm"}, 66 | 62: {"Group": None, "Name": "Samarium", "Period": 6, "Symbol": "Sm"}, 67 | 63: {"Group": None, "Name": "Europium", "Period": 6, "Symbol": "Eu"}, 68 | 64: {"Group": None, "Name": "Gadolinium", "Period": 6, "Symbol": "Gd"}, 69 | 65: {"Group": None, "Name": "Terbium", "Period": 6, "Symbol": "Tb"}, 70 | 66: {"Group": None, "Name": "Dysprosium", "Period": 6, "Symbol": "Dy"}, 71 | 67: {"Group": None, "Name": "Holmium", "Period": 6, "Symbol": "Ho"}, 72 | 68: {"Group": None, "Name": "Erbium", "Period": 6, "Symbol": "Er"}, 73 | 69: {"Group": None, "Name": "Thulium", "Period": 6, "Symbol": "Tm"}, 74 | 70: {"Group": None, "Name": "Ytterbium", "Period": 6, "Symbol": "Yb"}, 75 | 71: {"Group": 3, "Name": "Lutetium", "Period": 6, "Symbol": "Lu"}, 76 | 72: {"Group": 4, "Name": "Hafnium", "Period": 6, "Symbol": "Hf"}, 77 | 73: {"Group": 5, "Name": "Tantalum", "Period": 6, "Symbol": "Ta"}, 78 | 74: {"Group": 6, "Name": "Tungsten", "Period": 6, "Symbol": "W"}, 79 | 75: {"Group": 7, "Name": "Rhenium", "Period": 6, "Symbol": "Re"}, 80 | 76: {"Group": 8, "Name": "Osmium", "Period": 6, "Symbol": "Os"}, 81 | 77: {"Group": 9, "Name": "Iridium", "Period": 6, "Symbol": "Ir"}, 82 | 78: {"Group": 10, "Name": "Platinum", "Period": 6, "Symbol": "Pt"}, 83 | 79: {"Group": 11, "Name": "Gold", "Period": 6, "Symbol": "Au"}, 84 | 80: {"Group": 12, "Name": "Mercury", "Period": 6, "Symbol": "Hg"}, 85 | 81: {"Group": 13, "Name": "Thallium", "Period": 6, "Symbol": "Tl"}, 86 | 82: {"Group": 14, "Name": "Lead", "Period": 6, "Symbol": "Pb"}, 87 | 83: {"Group": 15, "Name": "Bismuth", "Period": 6, "Symbol": "Bi"}, 88 | 84: {"Group": 16, "Name": "Polonium", "Period": 6, "Symbol": "Po"}, 89 | 85: {"Group": 17, "Name": "Astatine", "Period": 6, "Symbol": "At"}, 90 | 86: {"Group": 18, "Name": "Radon", "Period": 6, "Symbol": "Rn"}, 91 | 87: {"Group": 1, "Name": "Francium", "Period": 7, "Symbol": "Fr"}, 92 | 88: {"Group": 2, "Name": "Radium", "Period": 7, "Symbol": "Ra"}, 93 | 89: {"Group": None, "Name": "Actinium", "Period": 7, "Symbol": "Ac"}, 94 | 90: {"Group": None, "Name": "Thorium", "Period": 7, "Symbol": "Th"}, 95 | 91: {"Group": None, "Name": "Protactinium", "Period": 7, "Symbol": "Pa"}, 96 | 92: {"Group": None, "Name": "Uranium", "Period": 7, "Symbol": "U"}, 97 | 93: {"Group": None, "Name": "Neptunium", "Period": 7, "Symbol": "Np"}, 98 | 94: {"Group": None, "Name": "Plutonium", "Period": 7, "Symbol": "Pu"}, 99 | 95: {"Group": None, "Name": "Americium", "Period": 7, "Symbol": "Am"}, 100 | 96: {"Group": None, "Name": "Curium", "Period": 7, "Symbol": "Cm"}, 101 | 97: {"Group": None, "Name": "Berkelium", "Period": 7, "Symbol": "Bk"}, 102 | 98: {"Group": None, "Name": "Californium", "Period": 7, "Symbol": "Cf"}, 103 | 99: {"Group": None, "Name": "Einsteinium", "Period": 7, "Symbol": "Es"}, 104 | 100: {"Group": None, "Name": "Fermium", "Period": 7, "Symbol": "Fm"}, 105 | 101: {"Group": None, "Name": "Mendelevium", "Period": 7, "Symbol": "Md"}, 106 | 102: {"Group": None, "Name": "Nobelium", "Period": 7, "Symbol": "No"}, 107 | 103: {"Group": 3, "Name": "Lawrencium", "Period": 7, "Symbol": "Lr"}, 108 | 104: {"Group": 4, "Name": "Rutherfordium", "Period": 7, "Symbol": "Rf"}, 109 | 105: {"Group": 5, "Name": "Dubnium", "Period": 7, "Symbol": "Db"}, 110 | 106: {"Group": 6, "Name": "Seaborgium", "Period": 7, "Symbol": "Sg"}, 111 | 107: {"Group": 7, "Name": "Bohrium", "Period": 7, "Symbol": "Bh"}, 112 | 108: {"Group": 8, "Name": "Hassium", "Period": 7, "Symbol": "Hs"}, 113 | 109: {"Group": 9, "Name": "Meitnerium", "Period": 7, "Symbol": "Mt"}, 114 | 110: {"Group": 10, "Name": "Darmstadtium", "Period": 7, "Symbol": "Ds"}, 115 | 111: {"Group": 11, "Name": "Roentgenium", "Period": 7, "Symbol": "Rg"}, 116 | 112: {"Group": 12, "Name": "Copernicium", "Period": 7, "Symbol": "Cn"}, 117 | 113: {"Group": 13, "Name": "Nihonium", "Period": 7, "Symbol": "Nh"}, 118 | 114: {"Group": 14, "Name": "Flerovium", "Period": 7, "Symbol": "Fl"}, 119 | 115: {"Group": 15, "Name": "Moscovium", "Period": 7, "Symbol": "Mc"}, 120 | 116: {"Group": 16, "Name": "Livermorium", "Period": 7, "Symbol": "Lv"}, 121 | 117: {"Group": 17, "Name": "Tennessine", "Period": 7, "Symbol": "Ts"}, 122 | 118: {"Group": 18, "Name": "Oganesson", "Period": 7, "Symbol": "Og"}, 123 | } 124 | 125 | symboltoint = { 126 | "R": 0, 127 | "Ac": 89, 128 | "Ag": 47, 129 | "Al": 13, 130 | "Am": 95, 131 | "Ar": 18, 132 | "As": 33, 133 | "At": 85, 134 | "Au": 79, 135 | "B": 5, 136 | "Ba": 56, 137 | "Be": 4, 138 | "Bh": 107, 139 | "Bi": 83, 140 | "Bk": 97, 141 | "Br": 35, 142 | "C": 6, 143 | "Ca": 20, 144 | "Cd": 48, 145 | "Ce": 58, 146 | "Cf": 98, 147 | "Cl": 17, 148 | "Cm": 96, 149 | "Cn": 112, 150 | "Co": 27, 151 | "Cr": 24, 152 | "Cs": 55, 153 | "Cu": 29, 154 | "Db": 105, 155 | "Ds": 110, 156 | "Dy": 66, 157 | "Er": 68, 158 | "Es": 99, 159 | "Eu": 63, 160 | "F": 9, 161 | "Fe": 26, 162 | "Fl": 114, 163 | "Fm": 100, 164 | "Fr": 87, 165 | "Ga": 31, 166 | "Gd": 64, 167 | "Ge": 32, 168 | "H": 1, 169 | "He": 2, 170 | "Hf": 72, 171 | "Hg": 80, 172 | "Ho": 67, 173 | "Hs": 108, 174 | "I": 53, 175 | "In": 49, 176 | "Ir": 77, 177 | "K": 19, 178 | "Kr": 36, 179 | "La": 57, 180 | "Li": 3, 181 | "Lr": 103, 182 | "Lu": 71, 183 | "Lv": 116, 184 | "Mc": 115, 185 | "Md": 101, 186 | "Mg": 12, 187 | "Mn": 25, 188 | "Mo": 42, 189 | "Mt": 109, 190 | "N": 7, 191 | "Na": 11, 192 | "Nb": 41, 193 | "Nd": 60, 194 | "Ne": 10, 195 | "Nh": 113, 196 | "Ni": 28, 197 | "No": 102, 198 | "Np": 93, 199 | "O": 8, 200 | "Og": 118, 201 | "Os": 76, 202 | "P": 15, 203 | "Pa": 91, 204 | "Pb": 82, 205 | "Pd": 46, 206 | "Pm": 61, 207 | "Po": 84, 208 | "Pr": 59, 209 | "Pt": 78, 210 | "Pu": 94, 211 | "Ra": 88, 212 | "Rb": 37, 213 | "Re": 75, 214 | "Rf": 104, 215 | "Rg": 111, 216 | "Rh": 45, 217 | "Rn": 86, 218 | "Ru": 44, 219 | "S": 16, 220 | "Sb": 51, 221 | "Sc": 21, 222 | "Se": 34, 223 | "Sg": 106, 224 | "Si": 14, 225 | "Sm": 62, 226 | "Sn": 50, 227 | "Sr": 38, 228 | "Ta": 73, 229 | "Tb": 65, 230 | "Tc": 43, 231 | "Te": 52, 232 | "Th": 90, 233 | "Ti": 22, 234 | "Tl": 81, 235 | "Tm": 69, 236 | "Ts": 117, 237 | "U": 92, 238 | "V": 23, 239 | "W": 74, 240 | "Xe": 54, 241 | "Y": 39, 242 | "Yb": 70, 243 | "Zn": 30, 244 | "Zr": 40, 245 | } 246 | -------------------------------------------------------------------------------- /rdeditor/ptable_widget.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import sys 4 | import logging 5 | 6 | from PySide6 import QtGui, QtCore, QtWidgets 7 | 8 | from .ptable import ptable 9 | 10 | 11 | class PTable(QtWidgets.QWidget): 12 | def __init__(self, actionGroup): 13 | super(PTable, self).__init__() 14 | self.ptable = ptable 15 | self.initUI(actionGroup) 16 | # logging 17 | self.logger = logging.getLogger() 18 | 19 | def initUI(self, actionGroup): 20 | grid = QtWidgets.QGridLayout() 21 | # Create actions dictionary and group dictionary 22 | # self.atomActionGroup = QtWidgets.QActionGroup(self, exclusive=True) 23 | self.atomActions = {} 24 | # for atomname in self.editor.atomtypes.keys(): Gives unsorted list 25 | for key in self.ptable.keys(): 26 | atomname = self.ptable[key]["Symbol"] 27 | action = QtGui.QAction( 28 | "%s" % atomname, 29 | self, 30 | statusTip="Set atomtype to %s" % atomname, 31 | triggered=self.atomtypePush, 32 | objectName=atomname, 33 | checkable=True, 34 | ) 35 | # self.atomActionGroup.addAction(action) 36 | actionGroup.addAction(action) 37 | self.atomActions[atomname] = action 38 | button = QtWidgets.QToolButton() 39 | button.setDefaultAction(action) 40 | button.setFocusPolicy(QtCore.Qt.NoFocus) 41 | button.setMaximumWidth(40) 42 | 43 | if self.ptable[key]["Group"] is not None: 44 | grid.addWidget(button, self.ptable[key]["Period"], self.ptable[key]["Group"]) 45 | else: 46 | if key < 72: 47 | grid.addWidget(button, 9, key - 54) 48 | else: 49 | grid.addWidget(button, 10, key - 86) 50 | self.atomActions["R"] = QtGui.QAction( 51 | "R", 52 | self, 53 | statusTip="Set atomtype to R", 54 | triggered=self.atomtypePush, 55 | objectName="R", 56 | checkable=True, 57 | ) 58 | actionGroup.addAction(self.atomActions["R"]) 59 | # Ensure spacing between main table and actinides/lathanides 60 | grid.addWidget(QtWidgets.QLabel(""), 8, 1) 61 | 62 | self.setLayout(grid) 63 | 64 | self.move(300, 150) 65 | self.setWindowTitle("Periodic Table") 66 | 67 | atomtypeChanged = QtCore.Signal(str, name="atomtypeChanged") 68 | 69 | def atomtypePush(self): 70 | sender = self.sender() 71 | self.atomtypeChanged.emit(sender.objectName()) 72 | 73 | # For setting the new atomtype 74 | def selectAtomtype(self, atomname): 75 | if atomname in self.atomActions.keys(): 76 | self.atomActions[atomname].setChecked(True) 77 | else: 78 | self.debug.error("Unknown atomtype or key error: %s" % atomname) 79 | 80 | 81 | def main(): 82 | app = QtWidgets.QApplication(sys.argv) 83 | pt = PTable() 84 | pt.selectAtomtype("N") 85 | pt.show() 86 | sys.exit(app.exec()) 87 | 88 | 89 | if __name__ == "__main__": 90 | main() 91 | -------------------------------------------------------------------------------- /rdeditor/rdEditor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Import required modules 4 | import sys 5 | import os 6 | import copy 7 | 8 | from PySide6.QtWidgets import QMenu, QApplication, QStatusBar, QMessageBox, QFileDialog 9 | from PySide6.QtCore import QSettings 10 | from PySide6 import QtCore, QtGui, QtWidgets 11 | from PySide6.QtCore import QUrl 12 | from PySide6.QtGui import QDesktopServices, QIcon, QAction, QKeySequence 13 | 14 | import qdarktheme 15 | 16 | # Import model 17 | from . import __version__ 18 | from .molEditWidget import MolEditWidget 19 | from .ptable_widget import PTable 20 | 21 | from rdkit import Chem 22 | import qdarktheme 23 | 24 | 25 | # The main window class 26 | class MainWindow(QtWidgets.QMainWindow): 27 | # Constructor function 28 | def __init__(self, fileName=None, loglevel="WARNING"): 29 | super(MainWindow, self).__init__() 30 | self.pixmappath = os.path.abspath(os.path.dirname(__file__)) + "/pixmaps/" 31 | QtGui.QIcon.setThemeSearchPaths( 32 | # QtGui.QIcon.themeSearchPaths() + 33 | [os.path.abspath(os.path.dirname(__file__)) + "/icon_themes/"] 34 | ) 35 | self.loglevels = ["Critical", "Error", "Warning", "Info", "Debug", "Notset"] 36 | # RDKit draw options, tooltip, default value is read from molViewWidget 37 | self._drawopts_actions = [ 38 | ( 39 | "prepareMolsBeforeDrawing", 40 | "Prepare molecules before drawing (i.e. fix stereochemistry and annotations)", 41 | ), 42 | ( 43 | "addStereoAnnotation", 44 | "Add stereo annotation (R/S and E/Z)", 45 | ), 46 | ( 47 | "unspecifiedStereoIsUnknown", 48 | "Show wiggly bond at potential undefined chiral stereo centres " 49 | + "and cross bonds for undefined doublebonds", 50 | ), 51 | ] 52 | 53 | self.editor = MolEditWidget() 54 | self.chemEntityActionGroup = QtGui.QActionGroup(self, exclusive=True) 55 | self.ptable = PTable(self.chemEntityActionGroup) 56 | self._fileName = None 57 | self.initGUI(fileName=fileName) 58 | self.applySettings() 59 | self.ptable.atomtypeChanged.connect(self.setAtomTypeName) 60 | # self.singleBondAction.trigger() 61 | self.ptable.atomActions["C"].trigger() 62 | 63 | # Properties 64 | @property 65 | def fileName(self): 66 | return self._fileName 67 | 68 | @fileName.setter 69 | def fileName(self, filename): 70 | if filename != self._fileName: 71 | self._fileName = filename 72 | self.setWindowTitle(str(filename)) 73 | 74 | def initGUI(self, fileName=None): 75 | self.setWindowTitle("rdEditor") 76 | self.setWindowIcon(QIcon.fromTheme("appicon")) 77 | self.setGeometry(100, 100, 200, 150) 78 | 79 | self.center = self.editor 80 | self.center.setFixedSize(650, 650) 81 | self.setCentralWidget(self.center) 82 | self.fileName = fileName 83 | 84 | self.filters = "MOL Files (*.mol *.mol);;SMILES Files (*.smi *.smi);;Any File (*)" 85 | 86 | self.SetupComponents() 87 | 88 | self.infobar = QtWidgets.QLabel("") 89 | self.myStatusBar.addPermanentWidget(self.infobar, 0) 90 | 91 | if self.fileName is not None: 92 | self.editor.logger.info("Loading molecule from %s" % self.fileName) 93 | self.loadFile() 94 | 95 | self.editor.sanitizeSignal.connect(self.infobar.setText) 96 | 97 | self.show() 98 | 99 | def getAllActionsInMenu(self, qmenu: QMenu): 100 | all_actions = [] 101 | 102 | # Iterate through actions in the current menu 103 | for action in qmenu.actions(): 104 | if isinstance(action, QAction): 105 | if action.icon(): 106 | all_actions.append(action) 107 | elif isinstance(action, QMenu): # If the action is a submenu, recursively get its actions 108 | all_actions.extend(self.getAllActionsInMenu(action)) 109 | 110 | return all_actions 111 | 112 | def getAllIconActions(self, qapp: QApplication): 113 | all_actions = [] 114 | 115 | # Iterate through all top-level widgets in the application 116 | for widget in qapp.topLevelWidgets(): 117 | # Find all menus in the widget 118 | menus = widget.findChildren(QMenu) 119 | for menu in menus: 120 | # Recursively get all actions from each menu 121 | all_actions.extend(self.getAllActionsInMenu(menu)) 122 | 123 | return all_actions 124 | 125 | def resetActionIcons(self): 126 | actions_with_icons = list(set(self.getAllIconActions(QApplication))) 127 | for action in actions_with_icons: 128 | icon_name = action.icon().name() 129 | self.editor.logger.debug(f"reset icon {icon_name}") 130 | action.setIcon(QIcon.fromTheme(icon_name)) 131 | 132 | def applySettings(self): 133 | self.settings = QSettings("Cheminformania.com", "rdEditor") 134 | theme_name = self.settings.value("theme_name", "Fusion") 135 | 136 | self.applyTheme(theme_name) 137 | self.themeActions[theme_name].setChecked(True) 138 | 139 | loglevel = self.settings.value("loglevel", "Error") 140 | 141 | action = self.loglevelactions.get(loglevel, None) 142 | if action: 143 | action.trigger() 144 | 145 | sanitize_on_cleanup = self.settings.value("sanitize_on_cleanup", True, type=bool) 146 | self.editor.sanitize_on_cleanup = sanitize_on_cleanup 147 | self.cleanupSettingActions["sanitize_on_cleanup"].setChecked(sanitize_on_cleanup) 148 | 149 | kekulize_on_cleanup = self.settings.value("kekulize_on_cleanup", True, type=bool) 150 | self.editor.kekulize_on_cleanup = kekulize_on_cleanup 151 | self.cleanupSettingActions["kekulize_on_cleanup"].setChecked(kekulize_on_cleanup) 152 | 153 | # Draw options 154 | for key, statusTip in self._drawopts_actions: 155 | viewer_value = self.editor.getDrawOption(key) 156 | settings_value = self.settings.value(f"drawoptions/{key}", viewer_value, type=bool) 157 | if settings_value != viewer_value: 158 | self.editor.setDrawOption(key, settings_value) 159 | self.drawOptionsActions[key].setChecked(settings_value) 160 | 161 | if self.settings.contains("drawoptions/fixedBondLength"): 162 | fixedBondLength = self.settings.value("drawoptions/fixedBondLength", 15, type=int) 163 | self.editor.setDrawOption("fixedBondLength", fixedBondLength) 164 | 165 | # Function to setup status bar, central widget, menu bar, tool bar 166 | def SetupComponents(self): 167 | self.myStatusBar = QStatusBar() 168 | # self.molcounter = QLabel("-/-") 169 | # self.myStatusBar.addPermanentWidget(self.molcounter, 0) 170 | self.setStatusBar(self.myStatusBar) 171 | self.myStatusBar.showMessage("Ready", 10000) 172 | 173 | self.CreateActions() 174 | self.CreateMenus() 175 | self.CreateToolBars() 176 | 177 | # Actual menu bar item creation 178 | def CreateMenus(self): 179 | self.fileMenu = self.menuBar().addMenu("&File") 180 | # self.edit_menu = self.menuBar().addMenu("&Edit") 181 | 182 | self.toolMenu = self.menuBar().addMenu("&Tools") 183 | self.atomtypeMenu = self.menuBar().addMenu("&AtomTypes") 184 | self.bondtypeMenu = self.menuBar().addMenu("&BondTypes") 185 | self.templateMenu = self.menuBar().addMenu("Tem&plates") 186 | self.settingsMenu = self.menuBar().addMenu("&Settings") 187 | self.helpMenu = self.menuBar().addMenu("&Help") 188 | 189 | self.fileMenu.addAction(self.openAction) 190 | self.fileMenu.addAction(self.saveAction) 191 | self.fileMenu.addAction(self.saveAsAction) 192 | self.fileMenu.addSeparator() 193 | self.fileMenu.addAction(self.copyAction) 194 | self.fileMenu.addAction(self.pasteAction) 195 | self.fileMenu.addSeparator() 196 | self.fileMenu.addAction(self.exitAction) 197 | 198 | self.toolMenu.addAction(self.selectAction) 199 | self.toolMenu.addAction(self.addAction) 200 | # self.toolMenu.addAction(self.addBondAction) 201 | # self.toolMenu.addAction(self.replaceAction) 202 | self.toolMenu.addAction( 203 | self.rsAction 204 | ) # TODO, R/S and E/Z could be changed for a single action? it really depends if an atom or a bond is clicked! 205 | self.toolMenu.addAction(self.ezAction) 206 | self.toolMenu.addAction(self.increaseChargeAction) 207 | self.toolMenu.addAction(self.decreaseChargeAction) 208 | self.toolMenu.addAction(self.numberAtom) 209 | 210 | self.toolMenu.addSeparator() 211 | self.toolMenu.addAction(self.cleanCoordinatesAction) 212 | self.toolMenu.addAction(self.cleanupMolAction) 213 | self.toolMenu.addSeparator() 214 | self.toolMenu.addAction(self.undoAction) 215 | self.toolMenu.addSeparator() 216 | self.toolMenu.addAction(self.removeAction) 217 | self.toolMenu.addAction(self.clearCanvasAction) 218 | 219 | # Atomtype menu 220 | for action in self.atomActions: 221 | self.atomtypeMenu.addAction(action) 222 | self.specialatommenu = self.atomtypeMenu.addMenu("All Atoms") 223 | for atomnumber in self.ptable.ptable.keys(): 224 | atomname = self.ptable.ptable[atomnumber]["Symbol"] 225 | self.specialatommenu.addAction(self.ptable.atomActions[atomname]) 226 | 227 | # Bondtype Menu 228 | self.bondtypeMenu.addAction(self.singleBondAction) 229 | self.bondtypeMenu.addAction(self.doubleBondAction) 230 | self.bondtypeMenu.addAction(self.tripleBondAction) 231 | self.bondtypeMenu.addSeparator() 232 | # Bondtype Special types 233 | self.specialbondMenu = self.bondtypeMenu.addMenu("Special Bonds") 234 | for key in self.bondActions.keys(): 235 | self.specialbondMenu.addAction(self.bondActions[key]) 236 | 237 | # Templates menu 238 | for key in self.templateActions.keys(): 239 | self.templateMenu.addAction(self.templateActions[key]) 240 | 241 | # Settings menu 242 | self.themeMenu = self.settingsMenu.addMenu("Theme") 243 | self.populateThemeActions(self.themeMenu) 244 | self.loglevelMenu = self.settingsMenu.addMenu("Logging Level") 245 | for loglevel in self.loglevels: 246 | self.loglevelMenu.addAction(self.loglevelactions[loglevel]) 247 | self.cleanupMenu = self.settingsMenu.addMenu("Cleanup") 248 | for key, action in self.cleanupSettingActions.items(): 249 | self.cleanupMenu.addAction(action) 250 | self.drawOptionsMenu = self.settingsMenu.addMenu("Drawing Options") 251 | for key, statusTip in self._drawopts_actions: 252 | self.drawOptionsMenu.addAction(self.drawOptionsActions[key]) 253 | 254 | # Help menu 255 | self.helpMenu.addAction(self.aboutAction) 256 | self.helpMenu.addSeparator() 257 | self.helpMenu.addAction(self.openChemRxiv) 258 | self.helpMenu.addAction(self.openRepository) 259 | self.helpMenu.addSeparator() 260 | self.helpMenu.addAction(self.aboutQtAction) 261 | 262 | # actionListAction = QAction( 263 | # "List Actions", self, triggered=lambda: print(set(self.get_all_icon_actions_in_application(QApplication))) 264 | # ) 265 | # self.helpMenu.addAction(actionListAction) 266 | 267 | # Debug level sub menu 268 | 269 | def populateThemeActions(self, menu: QMenu): 270 | stylelist = QtWidgets.QStyleFactory.keys() + ["Qdt light", "Qdt dark"] 271 | self.themeActionGroup = QtGui.QActionGroup(self, exclusive=True) 272 | self.themeActions = {} 273 | for style_name in stylelist: 274 | action = QAction( 275 | style_name, 276 | self, 277 | objectName=style_name, 278 | triggered=self.setTheme, 279 | checkable=True, 280 | ) 281 | self.themeActionGroup.addAction(action) 282 | self.themeActions[style_name] = action 283 | menu.addAction(action) 284 | 285 | def CreateToolBars(self): 286 | self.mainToolBar = self.addToolBar("Main") 287 | # Main action bar 288 | self.mainToolBar.addAction(self.openAction) 289 | self.mainToolBar.addAction(self.saveAction) 290 | self.mainToolBar.addAction(self.saveAsAction) 291 | self.mainToolBar.addSeparator() 292 | self.mainToolBar.addAction(self.selectAction) 293 | self.mainToolBar.addAction(self.addAction) 294 | # self.mainToolBar.addAction(self.addBondAction) 295 | # self.mainToolBar.addAction(self.replaceAction) 296 | self.mainToolBar.addAction(self.rsAction) 297 | self.mainToolBar.addAction(self.ezAction) 298 | self.mainToolBar.addAction(self.increaseChargeAction) 299 | self.mainToolBar.addAction(self.decreaseChargeAction) 300 | self.mainToolBar.addAction(self.numberAtom) 301 | self.mainToolBar.addSeparator() 302 | self.mainToolBar.addAction(self.cleanCoordinatesAction) 303 | self.mainToolBar.addAction(self.cleanupMolAction) 304 | 305 | self.mainToolBar.addSeparator() 306 | self.mainToolBar.addAction(self.removeAction) 307 | self.mainToolBar.addAction(self.clearCanvasAction) 308 | # Bond types TODO are they necessary as can be toggled?? 309 | self.mainToolBar.addSeparator() 310 | self.mainToolBar.addAction(self.undoAction) 311 | # Side Toolbar 312 | self.sideToolBar = QtWidgets.QToolBar(self) 313 | self.addToolBar(QtCore.Qt.LeftToolBarArea, self.sideToolBar) 314 | self.sideToolBar.addAction(self.singleBondAction) 315 | self.sideToolBar.addAction(self.doubleBondAction) 316 | self.sideToolBar.addAction(self.tripleBondAction) 317 | self.sideToolBar.addSeparator() 318 | self.sideToolBar.addAction(self.templateActions["benzene"]) 319 | self.sideToolBar.addAction(self.templateActions["cyclohexane"]) 320 | self.sideToolBar.addSeparator() 321 | self.sideToolBar.addAction(self.ptable.atomActions["R"]) 322 | self.sideToolBar.addSeparator() 323 | for action in self.atomActions: 324 | self.sideToolBar.addAction(action) 325 | self.sideToolBar.addAction(self.openPtableAction) 326 | 327 | def loadSmilesFile(self, filename): 328 | self.fileName = filename 329 | with open(self.fileName, "r") as file: 330 | lines = file.readlines() 331 | if len(lines) > 1: 332 | self.editor.logger.warning("The SMILES file contains more than one line.") 333 | self.statusBar().showMessage("The SMILES file contains more than one line.") 334 | return None 335 | smiles = lines[0].strip() 336 | mol = Chem.MolFromSmiles(smiles) 337 | self.editor.mol = mol 338 | self.statusBar().showMessage(f"SMILES file {filename} opened") 339 | 340 | def loadMolFile(self, filename): 341 | self.fileName = filename 342 | mol = Chem.MolFromMolFile(str(self.fileName), sanitize=False, strictParsing=False) 343 | self.editor.mol = mol 344 | self.statusBar().showMessage(f"Mol file {filename} opened") 345 | 346 | def openFile(self): 347 | self.fileName, _ = QFileDialog.getOpenFileName(self, caption="Open file", filter=self.filters) 348 | return self.loadFile() 349 | 350 | def loadFile(self): 351 | if not self.fileName: 352 | self.editor.logger.warning("No file selected.") 353 | self.statusBar().showMessage("No file selected.") 354 | return 355 | if self.fileName.lower().endswith(".mol"): 356 | self.loadMolFile(self.fileName) 357 | elif self.fileName.lower().endswith(".smi"): 358 | self.loadSmilesFile(self.fileName) 359 | else: 360 | self.editor.logger.warning("Unknown file format. Assuming file as .mol format.") 361 | self.statusBar().showMessage("Unknown file format. Assuming file as .mol format.") 362 | self.loadMolFile(self.fileName) 363 | self.fileName += ".mol" 364 | 365 | def saveFile(self): 366 | if self.fileName is not None: 367 | Chem.MolToMolFile(self.editor.mol, str(self.fileName)) 368 | else: 369 | self.saveAsFile() 370 | 371 | def saveAsFile(self): 372 | self.fileName, self.filterName = QFileDialog.getSaveFileName(self, filter=self.filters) 373 | if self.fileName != "": 374 | if self.filterName == "MOL Files (*.mol *.mol)": 375 | if not self.fileName.lower().endswith(".mol"): 376 | self.fileName = self.fileName + ".mol" 377 | Chem.MolToMolFile(self.editor.mol, str(self.fileName)) 378 | self.statusBar().showMessage("File saved as MolFile", 2000) 379 | elif self.filterName == "SMILES Files (*.smi *.smi)": 380 | if not self.fileName.lower().endswith(".smi"): 381 | self.fileName = self.fileName + ".smi" 382 | smiles = Chem.MolToSmiles(self.editor.mol) 383 | with open(self.fileName, "w") as file: 384 | file.write(smiles + "\n") 385 | self.statusBar().showMessage("File saved as SMILES", 2000) 386 | else: 387 | self.statusBar().showMessage("Invalid file format", 2000) 388 | 389 | def copy(self): 390 | selected_text = Chem.MolToSmiles(self.editor.mol, isomericSmiles=True) 391 | clipboard = QApplication.clipboard() 392 | clipboard.setText(selected_text) 393 | 394 | def paste(self): 395 | clipboard = QApplication.clipboard() 396 | text = clipboard.text() 397 | mol = Chem.MolFromSmiles(text, sanitize=False) 398 | if mol: 399 | try: 400 | Chem.SanitizeMol(copy.deepcopy(mol)) # ).ToBinary())) 401 | except Exception as e: 402 | self.editor.logger.warning(f"Pasted SMILES is not sanitizable: {e}") 403 | 404 | self.editor.assign_stereo_atoms(mol) 405 | Chem.rdmolops.SetBondStereoFromDirections(mol) 406 | 407 | self.editor.mol = mol 408 | else: 409 | self.editor.logger.warning(f"Failed to parse the content of the clipboard as a SMILES: {repr(text)}") 410 | 411 | def clearCanvas(self): 412 | self.editor.clearAtomSelection() 413 | self.editor.mol = None 414 | self.fileName = None 415 | self.statusBar().showMessage("Canvas Cleared") 416 | 417 | def closeEvent(self, event): 418 | self.editor.logger.debug("closeEvent triggered") 419 | self.exitFile() 420 | event.ignore() 421 | 422 | def exitFile(self): 423 | response = self.msgApp("Confirmation", "This will quit the application. Do you want to Continue?") 424 | if response == "Y": 425 | self.ptable.close() 426 | exit(0) 427 | else: 428 | self.editor.logger.debug("Abort closing") 429 | 430 | # Function to show Diaglog box with provided Title and Message 431 | def msgApp(self, title, msg): 432 | userInfo = QMessageBox.question(self, title, msg) 433 | if userInfo == QMessageBox.Yes: 434 | return "Y" 435 | if userInfo == QMessageBox.No: 436 | return "N" 437 | self.close() 438 | 439 | def aboutHelp(self): 440 | QMessageBox.about( 441 | self, 442 | "About Simple Molecule Editor", 443 | f"""A Simple Molecule Editor where you can edit molecules\n 444 | Based on RDKit! http://www.rdkit.org/\n 445 | Some icons from http://icons8.com\n 446 | Source code: https://github.com/EBjerrum/rdeditor\n 447 | Version: {__version__} 448 | """, 449 | ) 450 | 451 | def setAction(self): 452 | sender = self.sender() 453 | self.editor.setAction(sender.objectName()) 454 | self.myStatusBar.showMessage("Action %s selected" % sender.objectName()) 455 | 456 | # TODO, the various setTypes could be unified, as the editor now understands a single chementity 457 | def setRingType(self): 458 | sender = self.sender() 459 | self.editor.setChemEntity(sender.objectName()) 460 | self.myStatusBar.showMessage("Ringtype %s selected" % sender.objectName()) 461 | 462 | def setBondType(self): 463 | sender = self.sender() 464 | self.editor.setChemEntity(sender.objectName()) 465 | self.myStatusBar.showMessage("Bondtype %s selected" % sender.objectName()) 466 | 467 | def setAtomType(self): 468 | sender = self.sender() 469 | self.editor.setChemEntity(sender.objectName()) 470 | # self.editor.setRingType(None) 471 | self.myStatusBar.showMessage("Atomtype %s selected" % sender.objectName()) 472 | 473 | def setAtomTypeName(self, atomname): 474 | self.editor.setChemEntity(str(atomname)) 475 | self.myStatusBar.showMessage("Atomtype %s selected" % atomname) 476 | 477 | def openPtable(self): 478 | self.ptable.show() 479 | 480 | def setLogLevel(self): 481 | loglevel = self.sender().objectName().split(":")[-1] # .upper() 482 | self.editor.logger.setLevel(loglevel.upper()) 483 | self.editor.logger.log(self.editor.logger.getEffectiveLevel(), f"loglevel set to {loglevel}") 484 | self.settings.setValue("loglevel", loglevel) 485 | self.settings.sync() 486 | 487 | def setDrawOption(self): 488 | sender = self.sender() 489 | option = sender.objectName() 490 | self.editor.setDrawOption(option, sender.isChecked()) 491 | self.settings.setValue(f"drawoptions/{option}", sender.isChecked()) 492 | self.settings.sync() 493 | 494 | def setTheme(self): 495 | sender = self.sender() 496 | theme_name = sender.objectName() 497 | self.myStatusBar.showMessage(f"Setting theme or style to {theme_name}") 498 | self.applyTheme(theme_name) 499 | self.settings.setValue("theme_name", theme_name) 500 | self.settings.sync() 501 | 502 | def is_dark_mode(self): 503 | """Hack to detect if we have a dark mode running""" 504 | app = QApplication.instance() 505 | palette = app.palette() 506 | # Get the color of the window background 507 | background_color = palette.color(QtGui.QPalette.Window) 508 | # Calculate the luminance (brightness) of the color 509 | luminance = ( 510 | 0.299 * background_color.red() + 0.587 * background_color.green() + 0.114 * background_color.blue() 511 | ) / 255 512 | # If the luminance is below a certain threshold, it's considered dark mode 513 | return luminance < 0.5 514 | 515 | def applyTheme(self, theme_name): 516 | if "dark" in theme_name: 517 | self.set_dark() 518 | elif "light" in theme_name: 519 | self.set_light() 520 | elif self.is_dark_mode(): 521 | self.set_dark() 522 | else: 523 | self.set_light() 524 | 525 | app = QApplication.instance() 526 | app.setStyleSheet("") # resets style 527 | if theme_name in QtWidgets.QStyleFactory.keys(): 528 | app.setStyle(theme_name) 529 | else: 530 | if theme_name == "Qdt light": 531 | qdarktheme.setup_theme("light") 532 | elif theme_name == "Qdt dark": 533 | qdarktheme.setup_theme("dark") 534 | 535 | self.resetActionIcons() 536 | 537 | def set_light(self): 538 | QIcon.setThemeName("light") 539 | self.editor.darkmode = False 540 | self.editor.logger.info("Resetting theme for light theme") 541 | 542 | def set_dark(self): 543 | QIcon.setThemeName("dark") 544 | self.editor.darkmode = True 545 | self.editor.logger.info("Resetting theme for dark theme") 546 | 547 | def openUrl(self): 548 | url = self.sender().data() 549 | QDesktopServices.openUrl(QUrl(url)) 550 | 551 | def set_setting(self): 552 | action = self.sender() 553 | if isinstance(action, QAction): 554 | setting_name = action.objectName() 555 | if hasattr(self.editor, setting_name): 556 | if getattr(self.editor, setting_name) != action.isChecked(): 557 | setattr(self.editor, setting_name, action.isChecked()) 558 | self.editor.logger.error(f"Changed editor setting {setting_name} to {action.isChecked()}") 559 | self.settings.setValue(setting_name, action.isChecked()) 560 | self.settings.sync() 561 | else: 562 | self.editor.logger.error(f"Error, could not find setting, {setting_name}, on editor object!") 563 | 564 | # Function to create actions for menus and toolbars 565 | def CreateActions(self): 566 | self.openAction = QAction( 567 | QIcon.fromTheme("open"), 568 | "O&pen", 569 | self, 570 | shortcut=QKeySequence.Open, 571 | statusTip="Open an existing file", 572 | triggered=self.openFile, 573 | ) 574 | 575 | self.saveAction = QAction( 576 | QIcon.fromTheme("icons8-Save"), 577 | "S&ave", 578 | self, 579 | shortcut=QKeySequence.Save, 580 | statusTip="Save file", 581 | triggered=self.saveFile, 582 | ) 583 | 584 | self.saveAsAction = QAction( 585 | QIcon.fromTheme("icons8-Save as"), 586 | "Save As", 587 | self, 588 | shortcut=QKeySequence.SaveAs, 589 | statusTip="Save file as ..", 590 | triggered=self.saveAsFile, 591 | ) 592 | 593 | self.exitAction = QAction( 594 | QIcon.fromTheme("icons8-Shutdown"), 595 | "E&xit", 596 | self, 597 | shortcut="Ctrl+Q", 598 | statusTip="Exit the Application", 599 | triggered=self.exitFile, 600 | ) 601 | 602 | self.aboutAction = QAction( 603 | QIcon.fromTheme("about"), 604 | "A&bout", 605 | self, 606 | statusTip="Displays info about text editor", 607 | triggered=self.aboutHelp, 608 | ) 609 | 610 | self.aboutQtAction = QAction( 611 | "About &Qt", 612 | self, 613 | statusTip="Show the Qt library's About box", 614 | triggered=QApplication.aboutQt, 615 | ) 616 | 617 | self.openPtableAction = QAction( 618 | QIcon.fromTheme("ptable"), 619 | "O&pen Periodic Table", 620 | self, 621 | shortcut=QKeySequence.Open, 622 | statusTip="Open the periodic table for atom type selection", 623 | triggered=self.openPtable, 624 | ) 625 | 626 | # Copy-Paste actions 627 | self.copyAction = QAction( 628 | QIcon.fromTheme("icons8-copy-96"), 629 | "Copy SMILES", 630 | self, 631 | shortcut=QKeySequence.Copy, 632 | statusTip="Copy the current molecule as a SMILES string", 633 | triggered=self.copy, 634 | ) 635 | 636 | self.pasteAction = QAction( 637 | QIcon.fromTheme("icons8-paste-100"), 638 | "Paste SMILES", 639 | self, 640 | shortcut=QKeySequence.Paste, 641 | statusTip="Paste the clipboard and parse assuming it is a SMILES string", 642 | triggered=self.paste, 643 | ) 644 | 645 | # Edit actions 646 | self.actionActionGroup = QtGui.QActionGroup(self, exclusive=True) 647 | self.selectAction = QAction( 648 | QIcon.fromTheme("icons8-Cursor"), 649 | "Se&lect", 650 | self, 651 | shortcut="Ctrl+L", 652 | statusTip="Select Atoms", 653 | triggered=self.setAction, 654 | objectName="Select", 655 | checkable=True, 656 | ) 657 | self.actionActionGroup.addAction(self.selectAction) 658 | 659 | self.addAction = QAction( 660 | QIcon.fromTheme("icons8-Edit"), 661 | "&Add", 662 | self, 663 | shortcut="Ctrl+A", 664 | statusTip="Add Atoms", 665 | triggered=self.setAction, 666 | objectName="Add", 667 | checkable=True, 668 | ) 669 | self.actionActionGroup.addAction(self.addAction) 670 | 671 | self.addBondAction = QAction( 672 | QIcon.fromTheme("icons8-Pinch"), 673 | "Add &Bond", 674 | self, 675 | shortcut="Ctrl+B", 676 | statusTip="Add Bond", 677 | triggered=self.setAction, 678 | objectName="Add Bond", 679 | checkable=True, 680 | ) 681 | self.actionActionGroup.addAction(self.addBondAction) 682 | 683 | self.replaceAction = QAction( 684 | QIcon.fromTheme("icons8-Replace Atom"), 685 | "&Replace", 686 | self, 687 | shortcut="Ctrl+R", 688 | statusTip="Replace Atom/Bond", 689 | triggered=self.setAction, 690 | objectName="Replace", 691 | checkable=True, 692 | ) 693 | self.actionActionGroup.addAction(self.replaceAction) 694 | 695 | self.rsAction = QAction( 696 | QIcon.fromTheme("Change_R_S"), 697 | "To&ggle R/S", 698 | self, 699 | shortcut="Ctrl+G", 700 | statusTip="Toggle Stereo Chemistry", 701 | triggered=self.setAction, 702 | objectName="RStoggle", 703 | checkable=True, 704 | ) 705 | self.actionActionGroup.addAction(self.rsAction) 706 | 707 | self.ezAction = QAction( 708 | QIcon.fromTheme("Change_E_Z"), 709 | "Toggle &E/Z", 710 | self, 711 | shortcut="Ctrl+E", 712 | statusTip="Toggle Bond Stereo Chemistry", 713 | triggered=self.setAction, 714 | objectName="EZtoggle", 715 | checkable=True, 716 | ) 717 | self.actionActionGroup.addAction(self.ezAction) 718 | 719 | self.removeAction = QAction( 720 | QIcon.fromTheme("icons8-Cancel"), 721 | "D&elete", 722 | self, 723 | shortcut="Ctrl+D", 724 | statusTip="Delete Atom or Bond", 725 | triggered=self.setAction, 726 | objectName="Remove", 727 | checkable=True, 728 | ) 729 | self.actionActionGroup.addAction(self.removeAction) 730 | 731 | self.increaseChargeAction = QAction( 732 | QIcon.fromTheme("icons8-Increase Font"), 733 | "I&ncrease Charge", 734 | self, 735 | shortcut="Ctrl++", 736 | statusTip="Increase Atom Charge", 737 | triggered=self.setAction, 738 | objectName="Increase Charge", 739 | checkable=True, 740 | ) 741 | self.actionActionGroup.addAction(self.increaseChargeAction) 742 | 743 | self.decreaseChargeAction = QAction( 744 | QIcon.fromTheme("icons8-Decrease Font"), 745 | "D&ecrease Charge", 746 | self, 747 | shortcut="Ctrl+-", 748 | statusTip="Decrease Atom Charge", 749 | triggered=self.setAction, 750 | objectName="Decrease Charge", 751 | checkable=True, 752 | ) 753 | self.actionActionGroup.addAction(self.decreaseChargeAction) 754 | 755 | self.numberAtom = QAction( 756 | QIcon.fromTheme("atommapnumber"), 757 | "Set atommap or R-group number", 758 | self, 759 | statusTip="Set atommap or R-group number", 760 | triggered=self.setAction, 761 | objectName="Number Atom", 762 | checkable=True, 763 | ) 764 | self.actionActionGroup.addAction(self.numberAtom) 765 | self.addAction.setChecked(True) 766 | 767 | # BondTypeActions 768 | self.bondtypeActionGroup = QtGui.QActionGroup(self, exclusive=True) 769 | 770 | self.singleBondAction = QAction( 771 | QIcon.fromTheme("icons8-Single"), 772 | "S&ingle Bond", 773 | self, 774 | shortcut="Ctrl+1", 775 | statusTip="Set bondtype to SINGLE", 776 | triggered=self.setBondType, 777 | objectName="SINGLE", 778 | checkable=True, 779 | ) 780 | self.chemEntityActionGroup.addAction(self.singleBondAction) 781 | 782 | self.doubleBondAction = QAction( 783 | QIcon.fromTheme("icons8-Double"), 784 | "Double Bond", 785 | self, 786 | shortcut="Ctrl+2", 787 | statusTip="Set bondtype to DOUBLE", 788 | triggered=self.setBondType, 789 | objectName="DOUBLE", 790 | checkable=True, 791 | ) 792 | self.chemEntityActionGroup.addAction(self.doubleBondAction) 793 | 794 | self.tripleBondAction = QAction( 795 | QIcon.fromTheme("icons8-Triple"), 796 | "Triple Bond", 797 | self, 798 | shortcut="Ctrl+3", 799 | statusTip="Set bondtype to TRIPLE", 800 | triggered=self.setBondType, 801 | objectName="TRIPLE", 802 | checkable=True, 803 | ) 804 | self.chemEntityActionGroup.addAction(self.tripleBondAction) 805 | 806 | # Build dictionary of ALL available bondtypes in RDKit 807 | self.bondActions = {} 808 | for key in self.editor.bondtypes.keys(): 809 | action = QAction( 810 | "%s" % key, 811 | self, 812 | statusTip="Set bondtype to %s" % key, 813 | triggered=self.setBondType, 814 | objectName=key, 815 | checkable=True, 816 | ) 817 | self.chemEntityActionGroup.addAction(action) 818 | self.bondActions[key] = action 819 | # Replace defined actions 820 | 821 | self.bondActions["SINGLE"] = self.singleBondAction 822 | self.bondActions["DOUBLE"] = self.doubleBondAction 823 | self.bondActions["TRIPLE"] = self.tripleBondAction 824 | 825 | # self.singleBondAction.setChecked(True) 826 | 827 | # Template Actions 828 | self.templateActions = {} 829 | 830 | # TODO can we add these automatically, i.e. if theres a similar named icon available? 831 | self.templateActions["benzene"] = QAction( 832 | QIcon.fromTheme("benzene"), 833 | "Benzene Ring", 834 | self, 835 | shortcut="Ctrl+4", 836 | statusTip="Draw Benzene", 837 | triggered=self.setRingType, 838 | objectName="benzene", 839 | checkable=True, 840 | ) 841 | 842 | self.templateActions["cyclohexane"] = QAction( 843 | QIcon.fromTheme("cyclohexane"), 844 | "Cyclohexane", 845 | self, 846 | shortcut="Ctrl+5", 847 | statusTip="Draw Cyclohexane", 848 | triggered=self.setRingType, 849 | objectName="cyclohexane", 850 | checkable=True, 851 | ) 852 | 853 | for key in self.editor.available_rings: 854 | if key not in self.templateActions: 855 | action = QAction( 856 | key, 857 | self, 858 | statusTip=f"Set template to {key}", 859 | triggered=self.setRingType, 860 | objectName=key, 861 | checkable=True, 862 | ) 863 | self.templateActions[key] = action 864 | 865 | for action in self.templateActions.values(): 866 | self.chemEntityActionGroup.addAction(action) 867 | 868 | # Misc Actions 869 | self.undoAction = QAction( 870 | QIcon.fromTheme("prev"), 871 | "U&ndo", 872 | self, 873 | shortcut="Ctrl+Z", 874 | statusTip="Undo/Redo changes to molecule Ctrl+Z", 875 | triggered=self.editor.undo, 876 | objectName="undo", 877 | ) 878 | 879 | self.clearCanvasAction = QAction( 880 | QIcon.fromTheme("icons8-Trash"), 881 | "C&lear Canvas", 882 | self, 883 | shortcut="Ctrl+X", 884 | statusTip="Clear Canvas (no warning)", 885 | triggered=self.clearCanvas, 886 | objectName="Clear Canvas", 887 | ) 888 | 889 | self.cleanCoordinatesAction = QAction( 890 | QIcon.fromTheme("RecalcCoord"), 891 | "Recalculate coordinates &F", 892 | self, 893 | shortcut="Ctrl+F", 894 | statusTip="Re-calculates coordinates and redraw", 895 | triggered=self.editor.canon_coords_and_draw, 896 | objectName="Recalculate Coordinates", 897 | ) 898 | 899 | self.cleanupMolAction = QAction( 900 | QIcon.fromTheme("CleanupChem"), 901 | "Cleanup Chemistry", 902 | self, 903 | # shortcut="Ctrl+F", 904 | statusTip="Sanitizes and Kekulizes molecule according to settings", 905 | triggered=self.editor.cleanup_mol, 906 | objectName="Cleanup Mol", 907 | ) 908 | 909 | self.cleanupSettingActions = {} 910 | 911 | self.sanitizeSettingAction = QAction( 912 | # QIcon.fromTheme("icons8-Broom"), 913 | "Sanitize molecule on cleanup", 914 | self, 915 | # shortcut="Ctrl+F", 916 | statusTip="Perform Sanitization during chemistry cleanup", 917 | triggered=self.set_setting, 918 | checkable=True, 919 | # checked=self.editor.sanitize_on_cleanup, 920 | objectName="sanitize_on_cleanup", 921 | ) 922 | self.cleanupSettingActions["sanitize_on_cleanup"] = self.sanitizeSettingAction 923 | 924 | self.kekulizeSettingAction = QAction( 925 | # QIcon.fromTheme("icons8-Broom"), 926 | "Kekulize molecule on cleanup", 927 | self, 928 | # shortcut="Ctrl+F", 929 | statusTip="Perform Kekulization after chemistry cleanup", 930 | triggered=self.set_setting, 931 | checkable=True, 932 | # checked=self.editor.kekulize_on_cleanup, 933 | objectName="kekulize_on_cleanup", 934 | ) 935 | self.cleanupSettingActions["kekulize_on_cleanup"] = self.kekulizeSettingAction 936 | 937 | # Atom Actions in actiongroup, reuse from ptable widget 938 | self.atomActions = [] 939 | for atomname in ["H", "B", "C", "N", "O", "F", "P", "S", "Cl", "Br", "I"]: 940 | action = self.ptable.atomActions[atomname] 941 | self.atomActions.append(action) 942 | 943 | self.loglevelactions = {} 944 | self.loglevelActionGroup = QtGui.QActionGroup(self, exclusive=True) 945 | for key in self.loglevels: 946 | self.loglevelactions[key] = QAction( 947 | key, 948 | self, 949 | statusTip="Set logging level to %s" % key, 950 | triggered=self.setLogLevel, 951 | objectName="loglevel:%s" % key, 952 | checkable=True, 953 | ) 954 | self.loglevelActionGroup.addAction(self.loglevelactions[key]) 955 | 956 | self.drawOptionsActions = {} 957 | for key, statusTip in self._drawopts_actions: 958 | self.drawOptionsActions[key] = QAction( 959 | key, self, statusTip=statusTip, triggered=self.setDrawOption, objectName=key, checkable=True 960 | ) 961 | # self.drawOptionsActionGroup.addAction(self.drawOptionsActions[key]) 962 | 963 | self.openChemRxiv = QAction( 964 | QIcon.fromTheme("icons8-Exit"), 965 | "ChemRxiv Preprint", 966 | self, 967 | # shortcut="Ctrl+F", 968 | statusTip="Opens the ChemRxiv preprint", 969 | triggered=self.openUrl, 970 | data="https://doi.org/10.26434/chemrxiv-2024-jfhmw", 971 | ) 972 | 973 | self.openRepository = QAction( 974 | QIcon.fromTheme("icons8-Exit"), 975 | "GitHub repository", 976 | self, 977 | # shortcut="Ctrl+F", 978 | statusTip="Opens the GitHub repository", 979 | triggered=self.openUrl, 980 | data="https://github.com/EBjerrum/rdeditor", 981 | ) 982 | 983 | 984 | def launch(loglevel="WARNING"): 985 | "Function that launches the mainWindow Application" 986 | # Exception Handling 987 | try: 988 | myApp = QApplication(sys.argv) 989 | if len(sys.argv) > 1: 990 | mainWindow = MainWindow(fileName=sys.argv[1], loglevel=loglevel) 991 | else: 992 | mainWindow = MainWindow(loglevel=loglevel) 993 | myApp.exec() 994 | sys.exit(0) 995 | except NameError: 996 | print("Name Error:", sys.exc_info()[1]) 997 | except SystemExit: 998 | print("Closing Window...") 999 | except Exception: 1000 | print(sys.exc_info()[1]) 1001 | 1002 | 1003 | if __name__ == "__main__": 1004 | launch(loglevel="DEBUG") 1005 | -------------------------------------------------------------------------------- /rdeditor/templatehandler.py: -------------------------------------------------------------------------------- 1 | from rdkit import Chem 2 | from rdkit.Chem import AllChem 3 | from rdkit.Geometry.rdGeometry import Point2D, Point3D 4 | from rdkit.Chem import rdDepictor 5 | 6 | 7 | class TemplateHandler: 8 | reverse = False 9 | templates = { 10 | "benzene": { 11 | "canvas": "C1=CC=CC=C1", 12 | "atom": "[998*:1]>>[beginisotope*:1]1=C-C=C-C=C-1", 13 | "sp3": "[998*:1]-[999*:2]>>[beginisotope*:1]1-[endisotope*:2]=C-C=C-C=1", 14 | "sp2": "[998*:1]~[999*:2]>>[beginisotope*:1]1~[endisotope*:2]-C=C-C=C-1", 15 | "aromatic": "[998*:1]~[999*:2]>>[beginisotope*:1]1:[beginisotope*:2]:C:C:C:C:1", 16 | }, 17 | "cyclohexane": { 18 | "canvas": "C1CCCCC1", 19 | "atom": "[998*:1]>>[beginisotope*:1]1CCCCC1", 20 | "sp3": "[998*:1]-[999*:2]>>[beginisotope*:1]1-[endisotope*:2]-C-C-C-C-1", 21 | "sp2": "[998*:1]~[999*:2]>>[beginisotope*:1]1~[endisotope*:2]-C-C-C-C-1", 22 | "aromatic": "[998*:1]~[999*:2]>>[beginisotope*:1]1:[beginisotope*:2]-C-C-C-C-1", 23 | }, 24 | "cyclopentane": { 25 | "canvas": "C1CCCC1", 26 | "atom": "[998*:1]>>[beginisotope*:1]1CCCC1", 27 | "sp3": "[998*:1]-[999*:2]>>[beginisotope*:1]1-[endisotope*:2]-C-C-C-1", 28 | "sp2": "[998*:1]~[999*:2]>>[beginisotope*:1]1~[endisotope*:2]-C-C-C-1", 29 | "aromatic": "[998*:1]~[999*:2]>>[beginisotope*:1]1:[beginisotope*:2]-C-C-C-1", 30 | }, 31 | "cyclobutane": { 32 | "canvas": "C1CCC1", 33 | "atom": "[998*:1]>>[beginisotope*:1]1CCC1", 34 | "sp3": "[998*:1]-[999*:2]>>[beginisotope*:1]1-[endisotope*:2]-C-C-1", 35 | "sp2": "[998*:1]~[999*:2]>>[beginisotope*:1]1~[endisotope*:2]-C-C-1", 36 | "aromatic": "[998*:1]~[999*:2]>>[beginisotope*:1]1:[beginisotope*:2]-C-C-1", 37 | }, 38 | "cyclopropane": { 39 | "canvas": "C1CC1", 40 | "atom": "[998*:1]>>[beginisotope*:1]1CC1", 41 | "sp3": "[998*:1]-[999*:2]>>[beginisotope*:1]1-[endisotope*:2]-C-1", 42 | "sp2": "[998*:1]~[999*:2]>>[beginisotope*:1]1~[endisotope*:2]-C-1", 43 | "aromatic": "[998*:1]~[999*:2]>>[beginisotope*:1]1:[beginisotope*:2]-C-1", 44 | }, 45 | "carboxylic acid": { 46 | "canvas": "C(=O)[O]", 47 | "atom": "[998*:1]>>[beginisotope*:1](=O)[O]", 48 | }, 49 | # These types of templates need more work, i.e. if an NC bond is clicked, the addition can be non-sanitizable 50 | # due to the explicit H (or vice versa!) 51 | # "0-pyrrole": { 52 | # "atom": "[998*:1]>>[beginisotope*:1]-[N]1-C=C-C=C-1", 53 | # "sp3": "[998*:1]-[999*:2]>>[beginisotope*:1]1-[endisotopeN:2]-C=C-C=1", 54 | # "sp2": "[998*:1]=[999*:2]>>[beginisotope*:1]1:[endisotopeN:2]:C:C:C:1", # Not Kekulized 55 | # "aromatic": "[998*:1]~[999*:2]>>[beginisotope*:1]1:[beginisotopeN:2]:c:c:c:1", 56 | # }, 57 | # "1-pyrrole": { 58 | # "atom": "[998*:1]>>[beginisotope*:1]-C1=C-C=C-[NH]1", 59 | # Chem.BondType.SINGLE: "[998*:1]-[999*:2]>>[beginisotope*:1]1:[endisotope*:2]:[NH]:C:C:1", # Not kekulized 60 | # "sp2": "[998*:1]=[999*:2]>>[beginisotope*:1]1=[endisotope*:2]-[NH]-C=C-1", 61 | # "aromatic": "[998*:1]~[999*:2]>>[beginisotope*:1]1:[beginisotope*:2]:[nH]:c:c:1", 62 | # }, 63 | # "2-pyrrole": { 64 | # "atom": "[998*:1]>>[beginisotope*:1]-C1-C=C-[NH]-C=1", 65 | # Chem.BondType.SINGLE: "[998*:1]-[999*:2]>>[beginisotope*:1]1-[endisotope*:2]=C-[NH]-C=1", 66 | # "sp2": "[998*:1]=[999*:2]>>[beginisotope*:1]1:[endisotope*:2]:C:[NH]:C:1", # Not kekulized 67 | # "aromatic": "[998*:1]~[999*:2]>>[beginisotope*:1]1:[beginisotope*:2]:c:[nH]:c:1", 68 | # }, 69 | } 70 | 71 | @property 72 | def templateslabels(self): 73 | return tuple(self.templates.keys()) 74 | 75 | def apply_template_to_atom(self, beginatom: Chem.rdchem.Atom, templatelabel: str) -> Chem.Mol: 76 | """Apply to an atom""" 77 | beginisotope = beginatom.GetIsotope() 78 | 79 | # TODO assert that the molecule doesnt have this isotope numbers! 80 | beginatom.SetIsotope(998) 81 | 82 | template_set = self.templates[templatelabel] 83 | 84 | if "atom" not in template_set: 85 | raise ValueError(f"Template {templatelabel} not supported by atom click") 86 | 87 | template = template_set["atom"] 88 | 89 | template = template.replace("beginisotope", str(beginisotope)) 90 | 91 | templateaddition = AllChem.ReactionFromSmarts(template) 92 | 93 | newmol = self.react_and_keep_fragments(beginatom.GetOwningMol(), templateaddition) 94 | 95 | beginatom.SetIsotope(beginisotope) 96 | 97 | return newmol 98 | 99 | def apply_template_to_bond(self, bond: Chem.rdchem.Bond, templatelabel: str) -> Chem.Mol: 100 | """Apply to a bond""" 101 | # Reverse is for future usage for unsymmetric templates. Ctrl-Z and reapply to add the template reversed 102 | mol = bond.GetOwningMol() 103 | # mol.UpdatePropertyCache() 104 | Chem.SetHybridization(mol) 105 | 106 | beginatom = bond.GetEndAtom() if self.reverse else bond.GetBeginAtom() 107 | endatom = bond.GetBeginAtom() if self.reverse else bond.GetEndAtom() 108 | self.reverse = not self.reverse 109 | 110 | beginisotope = ( 111 | beginatom.GetIsotope() 112 | ) # TODO, we are in principle manipulating the parent molecule here. Can it be avoided? 113 | endisotope = endatom.GetIsotope() 114 | 115 | # TODO assert that the molecule doesnt have these isotope numbers! 116 | beginatom.SetIsotope(998) 117 | endatom.SetIsotope(999) 118 | 119 | bondtype = bond.GetBondType() 120 | 121 | # TODO, what if we encounter Chem.rdchem.HybridizationType.SP2D, SP3D or Other. When do we have these? 122 | if bondtype == Chem.BondType.AROMATIC: 123 | templatesubtype = "aromatic" 124 | elif Chem.rdchem.HybridizationType.SP2 in ( 125 | bond.GetBeginAtom().GetHybridization(), 126 | bond.GetEndAtom().GetHybridization(), 127 | ): 128 | templatesubtype = "sp2" 129 | elif (bond.GetBeginAtom().GetHybridization() == Chem.rdchem.HybridizationType.SP3) and ( 130 | bond.GetEndAtom().GetHybridization() == Chem.rdchem.HybridizationType.SP3 131 | ): 132 | templatesubtype = "sp3" 133 | else: 134 | raise ValueError( 135 | f"""Bondtype {bondtype} or Atomhybridizations { 136 | (bond.GetBeginAtom().GetHybridization(), bond.GetEndAtom().GetHybridization()) 137 | } not supported""" 138 | ) 139 | 140 | template_set = self.templates[templatelabel] 141 | template = template_set[templatesubtype] 142 | 143 | template = template.replace("beginisotope", str(beginisotope)).replace("endisotope", str(endisotope)) 144 | 145 | templateaddition = AllChem.ReactionFromSmarts(template) 146 | 147 | mol = bond.GetOwningMol() 148 | 149 | newmol = self.react_and_keep_fragments(mol, templateaddition) 150 | 151 | beginatom.SetIsotope(beginisotope) 152 | endatom.SetIsotope(endisotope) 153 | 154 | if newmol: 155 | return newmol 156 | else: 157 | raise RuntimeWarning(f"Applying template returned no molecule!") 158 | 159 | def apply_template_to_canvas(self, mol: Chem.Mol, point: Point2D, templatelabel: str) -> Chem.Mol: 160 | """Apply to canvas""" 161 | template = Chem.MolFromSmiles(self.templates[templatelabel]["canvas"], sanitize=False) 162 | 163 | if mol.GetNumAtoms() == 0: 164 | point.x = 0.0 165 | point.y = 0.0 166 | 167 | combined = Chem.rdchem.RWMol(Chem.CombineMols(mol, template)) 168 | # This should only trigger if we have an empty canvas 169 | if not combined.GetNumConformers(): 170 | rdDepictor.Compute2DCoords(combined) 171 | conf = combined.GetConformer(0) 172 | p3 = Point3D(point.x, point.y, 0) 173 | conf.SetAtomPosition(mol.GetNumAtoms(), p3) 174 | return combined 175 | 176 | def react_and_keep_fragments(self, mol, rxn): 177 | """RDKit only returns the fragment of Mol object that is reacted, 178 | hence this function to keep all other fragments.""" 179 | # Split the molecule into fragments 180 | fragments = list(Chem.GetMolFrags(mol, asMols=True, sanitizeFrags=False)) 181 | 182 | # Perform the reaction on the reacting fragment 183 | for i, reacting_fragment in enumerate(fragments): 184 | products = rxn.RunReactants((reacting_fragment,)) 185 | if products: 186 | fragments.pop(i) 187 | result = products[0][0] 188 | for frag in fragments: 189 | result = Chem.CombineMols(result, frag) 190 | 191 | return result 192 | else: 193 | return None 194 | -------------------------------------------------------------------------------- /rdeditor/utilities.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | def validate_rgb(rgb: tuple) -> None: 5 | if not isinstance(rgb, tuple) or len(rgb) != 3: 6 | logging.error(f"Input must be a tuple of length 3 representing an RGB color, was {rgb}") 7 | return False 8 | # raise ValueError("Input must be a tuple of length 3 representing an RGB color") 9 | 10 | for value in rgb: 11 | if not (isinstance(value, float) or isinstance(value, int)) or value < 0 or value > 1: 12 | logging.error(f"RGB values must be floats or integers between 0 and 1, was {rgb}") 13 | return False 14 | # raise ValueError("RGB values must be floats or integers between 0 and 1") 15 | 16 | return True 17 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | line-length = 120 2 | target-version = "py39" 3 | lint.select = ["E", "W"] 4 | --------------------------------------------------------------------------------