from typing import Optional
from typing import Tuple
from rdkit.Chem.rdchem import Mol
from schrodinger.Qt.QtCore import QRectF
from schrodinger.Qt.QtCore import Qt
from schrodinger.Qt.QtGui import QPainter
from schrodinger.Qt.QtGui import QPaintEvent
from schrodinger.Qt.QtGui import QResizeEvent
from schrodinger.Qt.QtWidgets import QFrame
from schrodinger.Qt.QtWidgets import QGraphicsItem
from schrodinger.Qt.QtWidgets import QLabel
from schrodinger.Qt.QtWidgets import QSizePolicy
from schrodinger.Qt.QtWidgets import QStyleOptionGraphicsItem
from schrodinger.Qt.QtWidgets import QVBoxLayout
from schrodinger.Qt.QtWidgets import QWidget
from schrodinger.ui.qt.delegates import RdMolDelegate
from schrodinger.ui.qt.tooltips import TooltipMixin
[docs]class RDMolStructureItem(QGraphicsItem):
"""
Graphics item that displays a 2D structure image using a `RdMolDelegate`.
"""
[docs] def __init__(self, parent: Optional[QGraphicsItem] = None):
super().__init__(parent=parent)
self._rect = QRectF(0, 0, 500, 500)
self._rdmol = None
self._color = None
self._atom_idcs = None
self._image_delegate = None
self.setImageDelegate(RdMolDelegate())
[docs] def getRDMol(self) -> Optional[Mol]:
"""
:return: the ligand structure currently stored on this object
"""
return self._rdmol
[docs] def setRDMol(self, rdmol: Optional[Mol]):
"""
Assign a new ligand structure.
:param rdmol: optionally, a `Mol` instance to render into a 2D image; if
`None`, this widget will not render an image
:param update: whether to update the displayed image
"""
self._rdmol = rdmol
self.update(self._rect)
[docs] def getAtomHighlights(
self) -> Tuple[Optional[Tuple[int]], Optional[Tuple[int]]]:
"""
:return: a tuple containing the highlighted atom indices and highlight
color (in RGB form) currently stored on this object
"""
return self._atom_idcs, self._color
[docs] def setAtomHighlights(self, atom_idcs: Optional[Tuple[int]],
color: Optional[Tuple[int]]):
"""
Assign the highlighted atoms and highlighting color for the image.
:param atom_idcs: indices of the atoms to be highlighted
:param color: color (in RGB form) to use for drawing highlighted atoms
:param update: whether to update the displayed image
"""
self._atom_idcs = atom_idcs
self._color = color
self.update(self._rect)
[docs] def setImageDelegate(self, delegate: RdMolDelegate):
"""
Assign a delegate that can be used to provide 2D structure images.
:param delegate: optionally, a delegate instance. If `None`, this widget
will not render an image
:param update: whether to update the displayed image
"""
self._image_delegate = delegate
[docs] def boundingRect(self) -> QRectF:
"""
:return: the area into which the structure image is rendered
"""
return self._rect
[docs] def setBoundingRect(self, rect: QRectF):
"""
:param rect: set the area into which the structure image is rendered
"""
self._rect = rect
[docs] def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem,
widget: Optional[QWidget]):
# Override this method to properly draw the structure image
if not self._image_delegate or not self._rdmol:
return
pic = self._image_delegate.getPicture(self._rdmol,
atom_idcs=self._atom_idcs,
color=self._color)
rect = self.boundingRect()
pic_width = max(pic.boundingRect().width(), 1)
pic_height = max(pic.boundingRect().height(), 1)
scale = min(rect.size().width() / pic_width,
rect.size().height() / pic_height)
pic_center = pic.boundingRect().center() * scale
painter.translate(rect.center() - pic_center)
painter.scale(scale, scale)
painter.drawPicture(0, 0, pic)
[docs]class AbstractRDMolImageLabel(QLabel):
"""
Abstract widget that draws 2D molecule images using `RdMolDelegate`.
In order to show any image, an instance must be assigned a `RdMolDelegate`
instance (using `setImageDelegate()`) and a `Mol` instance (using
`setRDMol()`). Modifications such as parameters for atom coloring and
labeling are supported but not required.
"""
[docs] def __init__(self, parent: Optional[QWidget] = None):
super().__init__(parent=parent)
self.setFrameShape(QFrame.NoFrame)
self.setAlignment(Qt.AlignCenter)
# Put a box around this label
self.setLineWidth(1)
size_policy = self._getSizePolicy()
self.setSizePolicy(size_policy)
self._rdmol = None
self._image_delegate = None
self._image_width = None
self._image_height = None
self._atom_idcs = None
self._highlight_color = None
self._atom_labels = None
size = 200
self.setImageSize(width=size, height=size)
def _getSizePolicy(self) -> QSizePolicy:
"""
Return a size policy for this widget.
This method is only expected to be called during initialization. Can be
overridden to modify the applied size policy.
:return: a size policy to use for this widget
"""
size_policy = QSizePolicy()
size_policy.setHorizontalStretch(0)
size_policy.setVerticalStretch(0)
return size_policy
[docs] def paintEvent(self, event: QPaintEvent):
# Override `paintEvent()` to control `QPicture` size.
br = self.picture().boundingRect()
cr = self.contentsRect()
img_w, img_h = br.width(), br.height()
painter = QPainter()
painter.begin(self)
self.drawFrame(painter)
width_ratio = self._image_width / img_w
height_ratio = self._image_height / img_h
scale = min(width_ratio, height_ratio)
painter.scale(scale, scale)
x_offset = (cr.width() - (img_w * scale)) // 2
y_offset = (cr.height() - (img_h * scale)) // 2
painter.drawPicture(cr.x() + x_offset - br.x(),
cr.y() + y_offset - br.y(), self.picture())
painter.end()
[docs] def setImageDelegate(self,
delegate: Optional[RdMolDelegate],
update: bool = True):
"""
Assign a delegate that can be used to provide 2D structure images.
:param delegate: optionally, a delegate instance. If `None`, this widget
will not render an image
:param update: whether to update the displayed image
"""
self._image_delegate = delegate
if update:
self._updateImage()
[docs] def setRDMol(self, rdmol: Optional[Mol], update: bool = True):
"""
Assign a new ligand structure.
:param rdmol: optionally, a `Mol` instance to render into a 2D image; if
`None`, this widget will not render an image
:param update: whether to update the displayed image
"""
self._rdmol = rdmol
if update:
self._updateImage()
[docs] def getImageSize(self) -> Tuple[int, int]:
"""
:return: a tuple containing the assigned (width, height) of the image
"""
return (self._image_width, self._image_height)
[docs] def setImageSize(self, width: int, height: int):
"""
Assign the size of the image.
"""
self._image_width = width
self._image_height = height
def _updateImage(self):
"""
Generate a new image to display in this widget if possible. Otherwise,
clear the displayed contents of this widget.
"""
if self._rdmol is None or self._image_delegate is None:
self.clear()
return
pic = self._image_delegate.getPicture(rdmol=self._rdmol,
atom_idcs=self._atom_idcs,
color=self._highlight_color,
atom_labels=self._atom_labels)
self.setPicture(pic)
[docs] def setAtomHighlights(self,
atom_idcs: Optional[Tuple[int]],
color: Optional[Tuple[int]],
update: bool = True):
"""
Assign the highlighted atoms and highlighting color for the image.
:param atom_idcs: indices of the atoms to be highlighted
:param color: color (in RGB form) to use for drawing highlighted atoms
:param update: whether to update the displayed image
"""
self._atom_idcs = atom_idcs
self._highlight_color = color
if update:
self._updateImage()
[docs] def setAtomLabels(self,
atom_labels: Optional[Tuple[str]],
update: bool = True):
"""
Assign the atom labels for the image.
:param atom_labels: a tuple of labels ordered so that the index of each
atom matches the index of the corresponding label
:param update: whether to update the displayed image
"""
self._atom_labels = atom_labels
if update:
self._updateImage()
[docs] def clearModifications(self, update: bool = True):
"""
Clear any stored image modification parameters.
:param update: whether to update the displayed image
"""
self.setAtomHighlights(atom_idcs=None, color=None, update=False)
self.setAtomLabels(atom_labels=None, update=False)
if update:
self._updateImage()
[docs]class ResizableRDMolImageLabel(AbstractRDMolImageLabel):
"""
Widget showing a 2D structure image that can be resized at will by the user.
"""
def _getSizePolicy(self) -> QSizePolicy:
size_policy = super()._getSizePolicy()
policy = QSizePolicy.MinimumExpanding
size_policy.setHorizontalPolicy(policy)
size_policy.setVerticalPolicy(policy)
return size_policy
[docs] def resizeEvent(self, event: QResizeEvent):
super().resizeEvent(event)
size = event.size()
self.setImageSize(width=size.width(), height=size.height())
[docs] def setImageSize(self, width: int, height: int):
super().setImageSize(width=width, height=height)
self.update()
[docs]class FixedSizeRDMolImageLabel(AbstractRDMolImageLabel):
"""
Widget showing a 2D structure image that can be assigned a fixed size.
"""
def _getSizePolicy(self) -> QSizePolicy:
size_policy = super()._getSizePolicy()
policy = QSizePolicy.Fixed
size_policy.setHorizontalPolicy(policy)
size_policy.setVerticalPolicy(policy)
return size_policy
[docs] def setImageSize(self, width: int, height: int):
super().setImageSize(width=width, height=height)
self.setFixedSize(width, height)