Skip to content

Commit

Permalink
Merge pull request #21 from EBjerrum/pyside6
Browse files Browse the repository at this point in the history
Pyside6
  • Loading branch information
EBjerrum authored Jul 16, 2024
2 parents e66db87 + 3b44b69 commit 4f2336b
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 92 deletions.
2 changes: 1 addition & 1 deletion DEVELOPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

### Prerequisites

rdkit, numpy, pyside2 are installed by default.
rdkit, numpy, pyside and pqtdarktheme are installed by default.
ruff is installed using the dev tag as described below

### Installation Steps
Expand Down
26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
# rdeditor

Simple RDKit molecule editor GUI using PySide2
Simple RDKit molecule editor GUI using PySide6
![rdeditor, the RDKit molecule editor](./Screenshots/Main_window.png)

## Installation

- requirements

RDKit, NumPy and PySide2 should be automatically pip installed by the setup.py script.
RDKit, NumPy, PySide6 and pyqtdarktheme should be automatically pip installed by the setup.py script.

- installation

```bash
python setup.py install
pip install rdeditor

```

A launch script will also be added so that it can be started from the command line via the rdEditor command.

## Alternative install

Install PySide and RDKit yourself, save the content of rdeditor folder to somewhere you like and start it with
`python rdEditor.py`
A launch script will also be added so that it can be started from the command line via the `rdEditor` command.

## Usage

Expand Down Expand Up @@ -59,6 +54,17 @@ Most commonly used bond types, and atom types can be selected. A Periodic table

Access to all standard operations as well as less used atom types and bond-types.

#### Settings

Themes can be selected from the ones available on your platform (Mac/Linux/Windows)

The debug level can be selected

## Development

Instructions to set it up in editable modes and instructions for eventual contributions can be found in the [DEVELOPER.md](./DEVELOPER.md) file.
Please reach out first, there may be a relevant development branch.

## Additional Reading

I wrote a blog post with an overview of the structure of the code.
Expand All @@ -70,3 +76,5 @@ We also published a preprint on ChemRxiv: [https://chemrxiv.org/engage/chemrxiv/

- Not possible to distinguish undefined and trans when editing cis/trans double bonds
- Aromaticity perception hides double-single bonds (kekulization can do likewise)

please report issues at GitHub, it's tough getting all corners of a GUI tested.
55 changes: 55 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
[build-system]
requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
build-backend = "setuptools.build_meta"

[project]
name = "rdeditor"
description = "An RDKit based molecule editor using PySide"
readme = "README.md"
requires-python = ">=3.8"
license = {text = "LGPL"}
authors = [
{name = "Esben Jannik Bjerrum", email = "[email protected]"},
]
keywords = ["RDKit", "molecule", "editor", "pyside"]

dependencies = [
"PySide6",
"numpy",
"rdkit",
"pyqtdarktheme",
]
dynamic = ["version"]

[project.urls]
Homepage = "http://github.com/ebjerrum/rdeditor"
"Bug Tracker" = "https://github.com/ebjerrum/rdeditor/issues"
Documentation = "https://github.com/ebjerrum/rdeditor/blob/master/README.md"
"Source Code" = "https://github.com/ebjerrum/rdeditor"
"Citation" = "https://github.com/ebjerrum/rdeditor/blob/master/CITATION.md"
"ChemRxiv Preprint" = "https://chemrxiv.org/engage/chemrxiv/article-details/65e6dcfa9138d23161b2979c"
"Developer Guide" = "https://github.com/ebjerrum/rdeditor/blob/master/DEVELOPER.md"

[project.optional-dependencies]
dev = ["ruff", "wheel", "twine", "setuptools_scm"]

[project.scripts]
rdEditor = "rdeditor.rdEditor:launch"

[tool.setuptools]
packages = ["rdeditor"]
zip-safe = false

[tool.setuptools.package-data]
rdeditor = [
"icon_themes/dark/*",
"icon_themes/dark/application/*",
"icon_themes/light/*",
"icon_themes/light/application/*",
]

[tool.setuptools_scm]
write_to = "rdeditor/_version.py"

[tool.ruff]
extend = "ruff.toml"
19 changes: 10 additions & 9 deletions rdeditor/molEditWidget.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#!/usr/bin/python
# Import required modules
from __future__ import print_function
from PySide2 import QtCore, QtGui, QtSvg, QtWidgets
from PySide6 import QtCore, QtGui, QtSvg, QtWidgets
import sys
import logging
from warnings import warn
Expand All @@ -19,7 +18,7 @@

from rdeditor.molViewWidget import MolWidget

from types import *
# from types import *

from rdeditor.ptable import symboltoint

Expand Down Expand Up @@ -159,7 +158,7 @@ def setRing(self, ringtype):
self.logger.error(f"Currently only {self.available_rings} are supported.")

def setBond(self, bondtype):
if type(bondtype) == Chem.rdchem.BondType:
if isinstance(bondtype, Chem.rdchem.BondType):
self._chementitytype = "bond"
self._chementity = bondtype

Expand Down Expand Up @@ -286,21 +285,23 @@ def get_molobject(self, event):
return self.SVG_to_coord(x_svg, y_svg)

def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
if event.button() is QtCore.Qt.LeftButton:
clicked = self.get_molobject(event)
if type(clicked) == Chem.rdchem.Atom:
if isinstance(clicked, Chem.rdchem.Atom):
self.logger.debug(
"You clicked atom %i, with atomic number %i" % (clicked.GetIdx(), clicked.GetAtomicNum())
)
# Call the atom_click function
self.atom_click(clicked)
# self.add_atom(self.pen, clicked)
elif type(clicked) == Chem.rdchem.Bond:
elif isinstance(clicked, Chem.rdchem.Bond):
self.logger.debug("You clicked bond %i with type %s" % (clicked.GetIdx(), clicked.GetBondType()))
self.bond_click(clicked)
elif type(clicked) == Point2D:
elif isinstance(clicked, Point2D):
self.logger.debug("Canvas Click")
self.canvas_click(clicked)
else:
self.logger.error(f"Clicked entity, {clicked} of unknown type {type(clicked)}")

# Lookup tables to relate actions to context type with action type #TODO more clean to use Dictionaries??
def atom_click(self, atom):
Expand Down Expand Up @@ -654,4 +655,4 @@ def backupMol(self):
myApp = QtWidgets.QApplication(sys.argv)
molblockview = MolWidget(mol)
molblockview.show()
myApp.exec_()
myApp.exec()
9 changes: 5 additions & 4 deletions rdeditor/molViewWidget.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#!/usr/bin/python
# Import required modules
from __future__ import print_function
from PySide2 import QtCore, QtGui, QtSvg, QtWidgets
from PySide6 import QtCore, QtGui, QtSvg, QtWidgets, QtSvgWidgets
import sys
from types import *

# from types import *
import logging

import numpy as np
Expand All @@ -18,7 +19,7 @@


# The Viewer Class
class MolWidget(QtSvg.QSvgWidget):
class MolWidget(QtSvgWidgets.QSvgWidget):
def __init__(self, mol=None, parent=None):
# Also init the super class
super(MolWidget, self).__init__(parent)
Expand Down Expand Up @@ -286,4 +287,4 @@ def getMolSvg(self):
molview.selectAtom(1)
molview.selectedAtoms = [1, 2, 3]
molview.show()
myApp.exec_()
myApp.exec()
7 changes: 3 additions & 4 deletions rdeditor/ptable_widget.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import print_function
import sys
import logging

from PySide2 import QtGui, QtCore, QtWidgets
from PySide6 import QtGui, QtCore, QtWidgets

from rdeditor.ptable import ptable

Expand All @@ -25,7 +24,7 @@ def initUI(self, actionGroup):
# for atomname in self.editor.atomtypes.keys(): Gives unsorted list
for key in self.ptable.keys():
atomname = self.ptable[key]["Symbol"]
action = QtWidgets.QAction(
action = QtGui.QAction(
"%s" % atomname,
self,
statusTip="Set atomtype to %s" % atomname,
Expand Down Expand Up @@ -75,7 +74,7 @@ def main():
pt = PTable()
pt.selectAtomtype("N")
pt.show()
sys.exit(app.exec_())
sys.exit(app.exec())


if __name__ == "__main__":
Expand Down
73 changes: 48 additions & 25 deletions rdeditor/rdEditor.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
#!/usr/bin/env python
from __future__ import print_function


# Import required modules
import sys
import time
import os
from PySide2.QtGui import *
from PySide2.QtWidgets import *
from PySide2.QtCore import QByteArray
from PySide2.QtCore import QSettings
from PySide2 import QtCore, QtGui, QtWidgets
from PySide2 import QtSvg
from PySide2.QtCore import QUrl
from PySide2.QtGui import QDesktopServices

from PySide6.QtWidgets import QMenu, QApplication, QStatusBar, QMessageBox, QFileDialog
from PySide6.QtCore import QByteArray
from PySide6.QtCore import QSettings
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6 import QtSvg
from PySide6.QtCore import QUrl
from PySide6.QtGui import QDesktopServices, QIcon, QAction, QKeySequence

import darkdetect
import qdarktheme

# Import model
Expand All @@ -37,7 +37,7 @@ def __init__(self, fileName=None, loglevel="WARNING"):
)
self.loglevels = ["Critical", "Error", "Warning", "Info", "Debug", "Notset"]
self.editor = MolEditWidget()
self.chemEntityActionGroup = QtWidgets.QActionGroup(self, exclusive=True)
self.chemEntityActionGroup = QtGui.QActionGroup(self, exclusive=True)
self.ptable = PTable(self.chemEntityActionGroup)
self._fileName = None
self.initGUI(fileName=fileName)
Expand Down Expand Up @@ -71,7 +71,7 @@ def initGUI(self, fileName=None):

self.SetupComponents()

self.infobar = QLabel("")
self.infobar = QtWidgets.QLabel("")
self.myStatusBar.addPermanentWidget(self.infobar, 0)

if self.fileName is not None:
Expand Down Expand Up @@ -217,8 +217,8 @@ def CreateMenus(self):
# Debug level sub menu

def populateThemeActions(self, menu: QMenu):
stylelist = QStyleFactory.keys() + ["Qdt light", "Qdt dark"]
self.themeActionGroup = QtWidgets.QActionGroup(self, exclusive=True)
stylelist = QtWidgets.QStyleFactory.keys() + ["Qdt light", "Qdt dark"]
self.themeActionGroup = QtGui.QActionGroup(self, exclusive=True)
self.themeActions = {}
for style_name in stylelist:
action = QAction(
Expand Down Expand Up @@ -434,19 +434,32 @@ def setTheme(self):
self.settings.setValue("theme_name", theme_name)
self.settings.sync()

def is_dark_mode(self):
"""Hack to detect if we have a dark mode running"""
app = QApplication.instance()
palette = app.palette()
# Get the color of the window background
background_color = palette.color(QtGui.QPalette.Window)
# Calculate the luminance (brightness) of the color
luminance = (
0.299 * background_color.red() + 0.587 * background_color.green() + 0.114 * background_color.blue()
) / 255
# If the luminance is below a certain threshold, it's considered dark mode
return luminance < 0.5

def applyTheme(self, theme_name):
if "dark" in theme_name:
QIcon.setThemeName("dark")
self.editor.darkmode = True
self.editor.logger.info("Resetting theme for dark theme")
self.set_dark()
elif "light" in theme_name:
self.set_light()
elif self.is_dark_mode():
self.set_dark()
else:
QIcon.setThemeName("light")
self.editor.darkmode = False
self.editor.logger.info("Resetting theme for light theme")
self.set_light()

app = QApplication.instance()
app.setStyleSheet("") # resets style
if theme_name in QStyleFactory.keys():
if theme_name in QtWidgets.QStyleFactory.keys():
app.setStyle(theme_name)
else:
if theme_name == "Qdt light":
Expand All @@ -456,6 +469,16 @@ def applyTheme(self, theme_name):

self.resetActionIcons()

def set_light(self):
QIcon.setThemeName("light")
self.editor.darkmode = False
self.editor.logger.info("Resetting theme for light theme")

def set_dark(self):
QIcon.setThemeName("dark")
self.editor.darkmode = True
self.editor.logger.info("Resetting theme for dark theme")

def openUrl(self):
url = self.sender().data()
QDesktopServices.openUrl(QUrl(url))
Expand Down Expand Up @@ -542,7 +565,7 @@ def CreateActions(self):
)

# Edit actions
self.actionActionGroup = QtWidgets.QActionGroup(self, exclusive=True)
self.actionActionGroup = QtGui.QActionGroup(self, exclusive=True)
self.selectAction = QAction(
QIcon.fromTheme("icons8-Cursor"),
"Se&lect",
Expand Down Expand Up @@ -653,7 +676,7 @@ def CreateActions(self):
self.addAction.setChecked(True)

# BondTypeActions
self.bondtypeActionGroup = QtWidgets.QActionGroup(self, exclusive=True)
self.bondtypeActionGroup = QtGui.QActionGroup(self, exclusive=True)

self.singleBondAction = QAction(
QIcon.fromTheme("icons8-Single"),
Expand Down Expand Up @@ -775,7 +798,7 @@ def CreateActions(self):
self.atomActions.append(action)

self.loglevelactions = {}
self.loglevelActionGroup = QtWidgets.QActionGroup(self, exclusive=True)
self.loglevelActionGroup = QtGui.QActionGroup(self, exclusive=True)
for key in self.loglevels:
self.loglevelactions[key] = QAction(
key,
Expand Down Expand Up @@ -817,7 +840,7 @@ def launch(loglevel="WARNING"):
mainWindow = MainWindow(fileName=sys.argv[1], loglevel=loglevel)
else:
mainWindow = MainWindow(loglevel=loglevel)
myApp.exec_()
myApp.exec()
sys.exit(0)
except NameError:
print("Name Error:", sys.exc_info()[1])
Expand Down
Loading

0 comments on commit 4f2336b

Please sign in to comment.