├── .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 | 
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 | 
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 | 
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------