Source code for schrodinger.ui.qt.structure2d

"""
2D structures drawing
"""

# Copyright Schrodinger, LLC. All rights reserved.

from past.utils import old_div

import numpy
from rdkit import Chem
from rdkit import Geometry

import schrodinger.application.canvas.utils as canvasutils
from schrodinger import get_maestro
from schrodinger import structure
from schrodinger.infra import canvas
from schrodinger.infra import canvas2d
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.structutils import analyze
from schrodinger.thirdparty import rdkit_adapter
from schrodinger.ui import sketcher

from . import swidgets

if not canvas.ChmLicenseShared_isValid():
    license = canvasutils.get_license(canvasutils.LICENSE_SHARED)

CONNECTION_COLORS = \
[[238, 162, 173], [191, 62, 255], [78, 238, 148], [192, 192, 192], [72, 209, 204],\
[255, 193, 37], [197, 193, 170], [202, 225, 255], [113, 113, 198], [238, 238, 0]]

maestro = get_maestro()

MAX_LIGAND_ATOMS = 200


[docs]def get_qpicture_protected(renderer, chmmol, gen_coord=True): """ Generate a QPicture for the given molecule. If the picture couldn't be generated (e.g. the molecule is too large), then a QPicture will contain the failure message text. :type renderer: Chm2DRenderer intance :param renderer: The renderer to use for generating the QPicture :type chmmol: ChmMol instance. :param chmmol: Structure to generate the picture for. :type gen_coord: bool :param gen_coord: if True (default) generate new coordinates; if False, use existing 2D coordinates. :rtype: QPicture :return: The generated picture. """ try: pic = renderer.getQPicture(chmmol, gen_coord) except Exception as e: if str(e) == "unknown": # Canvas raises 'unknown' as Exception pic = QtGui.QPicture() painter = QtGui.QPainter(pic) txt = "Failed to render" brect = painter.drawText(0, 0, 200, 200, QtCore.Qt.AlignCenter, txt) painter.end() pic.setBoundingRect(brect) else: raise e return pic
[docs]def generate_qimage_from_chmmol(chmmol, width, height, renderer=None, bg_color=None, max_scale=None, gen_coord=True): """ "Generate a 2D image in QImage format of a ChmMol molecule." :type chmmol: ChmMol instance. :param chmmol: Structure to generate the picture for. :type width: int :param width: width in pixels of the generated QImage. :type height: int :param height: height in pixels of the generated QImage. :type renderer: Chm2DRenderer intance or None :param renderer: The renderer to use for generating the QImage. If None, a new renderer will be created. :type bg_color: PyQt5.QtGui.QColor or None :param bg_color: background color filling the image around the scaled molecule. :type max_scale: float or None :param max_scale: maximum scaling of the structure image. :type gen_coord: bool :param gen_coord: if True (default) generate new coordinates; if False, use existing 2D coordinates. :rtype: QImage :return: The generated QImage. """ if renderer is None: renderer = canvas2d.Chm2DRenderer(canvas2d.ChmRender2DModel()) pic = get_qpicture_protected(renderer, chmmol, gen_coord) rect = QtCore.QRect(0, 0, width, height) if bg_color is None: model = renderer.getModel() bg_color = QtGui.QColor(model.getBackgroundColor()) qimg = QtGui.QImage(QtCore.QSize(width, height), QtGui.QImage.Format_ARGB32) with QtGui.QPainter(qimg) as painter: painter.fillRect(0, 0, width, height, bg_color) swidgets.draw_picture_into_rect(painter, pic, rect, max_scale) return qimg
[docs]def generate_qimage_from_structure(st, width, height, renderer=None, bg_color=None, max_scale=None, stereo_mode=canvas2d.ChmMmctAdaptor. StereoFromAnnotationAndGeometry_Safe, gen_coord=True): """ Generate a 2D image in QImage format of a schrodinger.structure.Structure. An intermediate ChmMol will be generated with stereochemstry deduced according to the stereo_mode parameter. :type st: Structure instance. :param st: Structure to generate the picture for. :type width: int :param width: width in pixels of the generated QImage. :type height: int :param height: height in pixels of the generated QImage. :type renderer: Chm2DRenderer intance or None :param renderer: The renderer to use for generating the QImage. :type bg_color: PyQt5.QtGui.QColor or None :param bg_color: background color filling the image around the scaled molecule. :type max_scale: float or None :param max_scale: maximum scaling of the structure image. :type stereo_mode: schrodinger.infra._canvas2d.ChmMmctAdaptor.StereoType :param stereo_mode: stereo source for internal transformation to ChmMol. :type gen_coord: bool :param gen_coord: if True generate coordinates for chmmol. :rtype: QImage :return: The generated QImage. """ if not isinstance(st, structure.Structure): raise TypeError( "'st' parameter must be a structure.Structure instance.") adaptor = canvas2d.ChmMmctAdaptor() chmmol = adaptor.create(st.handle, stereo_mode, canvas2d.ChmAtomOption.H_ExplicitOnly, False) return generate_qimage_from_chmmol(chmmol, width, height, renderer, bg_color, max_scale, gen_coord)
[docs]def get_qpicture_highlight(renderer, chmmol, atoms, bonds, color, gen_coord=False): """ Generate a QPicture for the given molecule and highlight given atoms and bonds. If the picture couldn't be generated (e.g. the molecule is too large), then a QPicture will contain the failure message text. :type renderer: Chm2DRenderer intance :param renderer: The renderer to use for generating the QPicture :type chmmol: ChmMol instance. :param chmmol: Structure to generate the picture for. :type atoms: list :param atoms: list of atoms that should be highlighted :type bonds: list :param bonds: list of bonds that should be highlighted :type color: `QtGui.QColor` :param color: color that is used to highlight atoms and bonds :type gen_coord: bool :param gen_coord: if True generate coordinates. :rtype: QPicture :return: The generated picture. """ try: pic = renderer.getQPicture(chmmol, atoms, bonds, color, gen_coord) except Exception as e: if str(e) == "unknown": # Canvas raises 'unknown' as Exception pic = QtGui.QPicture() painter = QtGui.QPainter(pic) txt = "Failed to render" brect = painter.drawText(0, 0, 200, 200, QtCore.Qt.AlignCenter, txt) painter.end() pic.setBoundingRect(brect) else: raise e return pic
[docs]def get_chmmol_bonds_from_atoms(chmmol, atoms): """ This function returns a list of bonds that connect atoms in a given list. :param chmmol: molecule structure :type chmmol: `canvas2d.ChmMol` :param atoms: list of atom indices :type atoms: list """ # FIXME: This is taken from # schrodinger.application.desmond.fep_scholar_util # per PANEL-7475 atoms = set(atoms) core_bonds = [] swigchmmol = canvas2d.convertChmMoltoSWIG(chmmol) bonds = swigchmmol.getBonds(True) for i, bond in enumerate(bonds): a1, a2 = bond.atom1(), bond.atom2() if a1.getMolIndex() in atoms and a2.getMolIndex() in atoms: core_bonds.append(i) return core_bonds
[docs]def get_rdmol_for_2d_rendering(st): """ Generate a RDKit molecule from schrodinger structure object. Also modify the newly generated RDKit molecule to make it suitable for 2D rendering. :param st: structure object :type st: structure.Structure :return: RDKit molecule :rtype: Chem.rdchem.Mol """ # `implicitH` as True speeds up the structure to RDKit molecule # conversion significantly (especially for complicated molecules on # which MCS calculation can hang). rdmol = rdkit_adapter.to_rdkit(st, sanitize=False, implicitH=True) # We do custom sanitation here in order to handle molecules with bad # valences, yet still be able to properly handle aromatic bonds. Chem.rdmolops.SanitizeMol(rdmol, Chem.rdmolops.SANITIZE_ALL) scale = sketcher.THREE_D_COORDINATES_CONVERSION_FACTOR conformer = rdmol.GetConformer() coordinates_map = {} for atom_idx in range(rdmol.GetNumAtoms()): coords = Geometry.Point2D(conformer.GetAtomPosition(atom_idx)) # invert y-axis since in 2D system axis should be oriented downwards coords.y = -coords.y # scaling is done to convert 3D coordinates (angstrom) into 2D # coordinates (pixels) coordinates_map[atom_idx] = coords * scale coords_gen_params = Chem.rdCoordGen.CoordGenParams() coords_gen_params.SetCoordMap(coordinates_map) Chem.rdCoordGen.AddCoords(rdmol, coords_gen_params) return rdmol
[docs]def get_st_image_using_sketcher(st, width=200, height=200): """ :param st: structure object :type st: structure.Structure :param width: image width :type width: int :param height: image height :type height: int :return: 2d structure image rendered using sketcher :rtype: QtGui.QImage """ renderer = sketcher.Renderer() settings = sketcher.RendererSettings() settings.width = width settings.height = height renderer.loadSettings(settings) renderer.loadStructure(st) return renderer.getImage()
[docs]def get_aligned_pictures(sts, renderer=None, atomTyping=11, core_color=None): """ Calculate the maximum common substructure (MCS) between the given ligands, and generate 2D images, aligned by the core. If no MCS was detected, the images will be unaligned. NOTE: This function becomes exponentioally slow with larger number of structures. Recommened maximum around 30 structures. :type sts: Iterable of `structure.Structure` objects :param sts: Structures to average :type renderer: Chm2DRenderer intance :param renderer: The renderer to use for generating the QPicture (optional) :type atomTyping: int :param atomTyping: Atom typing scheme to use. Default is 11. For list of available schemes, see $SCHRODINGER/utilities/canvasMCS -h :type core_color: `QColor` :param highlight_color: Optional Color to highlight the common substructure. :rtype: List of `QPicture` objects. :return: QPictures for the aligned 2D images. """ # NOTE: We are not using analyze.find_common_substructure() here, because # we also need bonds for the core and the ChmMol objects for the structures. # Constants for now, but we may choose to expose them as options later: IGNORE_HYDROGEN = 0 RESCALE = 2 FIXUP = True if len(sts) > 50: raise ValueError("Too many input CTs specified (max is 50)") if core_color is None: core_color = QtGui.QColor(0, 0, 0) # black settings = canvas.MCSsettings() settings.atomTyping = atomTyping if renderer is None: renderer = canvas2d.Chm2DRenderer(canvas2d.ChmRender2DModel()) # Convert CTs to ChmMol objects (both SWIG and SIP): adaptor = canvas2d.ChmMmctAdaptor() sip_mols = [adaptor.create(st.handle) for st in sts] swig_mols = list(map(canvas2d.convertChmMoltoSWIG, sip_mols)) results = canvas.runMCS(swig_mols, settings) template_chmmol = None template_core_atoms = None pictures = [] for chmmol, item in zip(sip_mols, results): # Chm2DCoordGen expects 0-indexed atoms and bonds: core_atoms = [index - 1 for index in item.mapAtoms] core_bonds = [index - 1 for index in item.mapBonds] if item.molID == 0: # Generate the 2D image for the first molecule (template): template_chmmol = chmmol template_core_atoms = core_atoms if item.molID == 0 or not core_atoms: canvas2d.Chm2DCoordGen.generateAndApply(chmmol) qpic = renderer.getQPicture(chmmol, [], core_bonds, core_color, False) else: # Align molecule2 to molecule1: canvas2d.Chm2DCoordGen.generateFromTemplateAndApply( chmmol, template_chmmol, core_atoms, template_core_atoms, IGNORE_HYDROGEN, RESCALE, FIXUP) qpic = renderer.getQPicture(chmmol, [], core_bonds, core_color, False) pictures.append(qpic) return pictures
[docs]def get_ligand(st): """ Return a substructure that can be rendered in a 2D image (the first ligand in `st`, unless it's also has too many atoms). :param st: the structure :type st: structure.Structure :return: the ligand structure or None if the structure has too many atoms :rtype: structure.Structure or None """ max_atoms = MAX_LIGAND_ATOMS if maestro: max_atoms = int(maestro.get_command_option("prefer", "2dmaxatoms")) if st.atom_total < max_atoms: return st lig_atoms = analyze.evaluate_asl(st, 'ligand') if len(lig_atoms) == 0: return None lig_st = st.extract(lig_atoms, copy_props=True) if lig_st.mol_total > 1: lig_st = lig_st.molecule[1].extractStructure(copy_props=True) if lig_st.atom_total < max_atoms: return lig_st
[docs]class StructurePicture(QtWidgets.QLabel): """ This is the label that normally stores the picture of the molecule. It can also store a text message. We make sure that this stays the same size, no matter what data (if any) is stored in it. """
[docs] def __init__(self, parent=None, layout=None, height=200, width=200, background='white', annotators=None): """ :type parent: QWidget :param parent: the widget that owns this widget :type layout: QLayout :param layout: The layout that this widget should be placed in :type height: int :param height: the height of this label in pixels :type width: int :param width: the width of this label in pixels :type annotators: list :param annotators: Each item of the list should be a `canvas2d.ChemViewAnnotator` object that will be applied to the `canvas2d.ChmRender2DModel` when generating the image """ QtWidgets.QLabel.__init__(self, parent) # Lock the size of this widget so it never changes sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) self.user_width = width self.user_height = height sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) self.setSizePolicy(sizePolicy) self.setMinimumSize(QtCore.QSize(width, height)) # Put a box around this label self.setFrameShape(QtWidgets.QFrame.Box) self.setLineWidth(1) # Everthing is centered horizontally and vertically self.setAlignment(QtCore.Qt.AlignCenter) # Set the background if background: self.setStyleSheet("background-color: " + background) # Prepare the structure drawing utilities self.adaptor = canvas2d.ChmMmctAdaptor() self.model = canvas2d.ChmRender2DModel() rect = canvas2d.ChmRect2D(0, 0, width - 10, height - 10) self.renderer = canvas2d.Chm2DRenderer(self.model) self.renderer.setBoundingBox(rect) if annotators: self.annotators = annotators else: self.annotators = [] # Put the widget in the GUI if layout is not None: layout.addWidget(self)
[docs] def sizeHint(self): # Ensures that the widget always remains the same size return QtCore.QSize(self.user_width, self.user_height)
[docs] def drawStructure(self, structure, StereoType=canvas2d.ChmMmctAdaptor. StereoFromAnnotationAndGeometry_Safe, hydrogenTreatment=canvas2d.ChmAtomOption.H_ExplicitOnly, wantProperties=True, wantMMStereoProps=True, readAtomBondProperties=True, allowRadicals=False): """ Makes a 2-D rendering of the structure :type structure: schrodinger.structure.Structure class object :param structure: structure to be drawn on the canvas :type StereoType: canvas2d.ChmMmctAdaptor.StereoType :param StereoType: Stereochemistry option to use. Available options: # Does not include stereochemistry info: canvas2d.ChmMmctAdaptor.NoStereo # Ignores mmstereo annotations: canvas2d.ChmMmctAdaptor.StereoFromGeometry # Silently ignores stereo information that Canvas doesn't agree with: canvas2d.ChmMmctAdaptor.StereoFromGeometry_Safe # Ignores 3d geometry: canvas2d.ChmMmctAdaptor.StereoFromAnnotation # Silently ignores stereo information that Canvas doesn't agree with: canvas2d.ChmMmctAdaptor.StereoFromAnnotation_Safe # Uses mmstereo annotaions with 3D geometry as a backup: canvas2d.ChmMmctAdaptor.StereoFromAnnotationAndGeometry # Silently ignores stereo information that Canvas doesn't agree with: canvas2d.ChmMmctAdaptor.StereoFromAnnotationAndGeometry_Safe :type hydrogenTreatment: canvas2d.ChmAtomOption.H :param hydrogenTreatment: Hydrogen treatment method. Available options: canvas2d.ChmAtomOption.H_Never canvas2d.ChmAtomOption.H_ExplicitOnly canvas2d.ChmAtomOption.H_Polar canvas2d.ChmAtomOption.H_ExplicitPolar canvas2d.ChmAtomOption.H_Chiral canvas2d.ChmAtomOption.H_ExplicitChiral canvas2d.ChmAtomOption.H_ExplicitPolarAndChiral canvas2d.ChmAtomOption.H_All :type wantProperties: bool :param wantProperties: Whether properties should be copied. :type wantMMStereoProps: bool :param wantMMStereoProps: Whether to copy mmstereo properties. :type readAtomBondProperties: bool :param readAtomBondProperties: Whether to copy atom and bond-level properties. :type allowRadicals: bool :param allowRadicals: Whether to assume that valence deficiencies represent unpaired electrons. """ # First add all the annotators in the self.annotators list self.model.clearAnnotators() for annotator in self.annotators: self.model.addAnnotator(annotator) self.renderer.setModel(self.model) chmmol = self.adaptor.create(int(structure), StereoType, hydrogenTreatment, wantProperties, wantMMStereoProps, readAtomBondProperties, allowRadicals) pic = get_qpicture_protected(self.renderer, chmmol) self.setPicture(pic) self.model.clearAnnotators()
[docs] def drawChmmol( self, chmmol, atoms=[], # noqa: M511 bonds=[], # noqa: M511 color=QtGui.QColor(255, 255, 255), # noqa: M511 gen_coord=False): """ Makes a 2-D rendering of the chmmol object with optional atoms and bonds highlighting. :type chmmol: ChmMol instance. :param chmmol: Structure to generate the picture for. :type atoms: list :param atoms: list of atoms that should be highlighted :type bonds: list :param bonds: list of bonds that should be highlighted :type color: `QtGui.QColor` :param color: color that is used to highlight atoms and bonds :type gen_coord: bool :param gen_coord: if True generate coordinates. """ self.model.clearAnnotators() for annotator in self.annotators: self.model.addAnnotator(annotator) self.renderer.setModel(self.model) pic = get_qpicture_highlight(self.renderer, chmmol, atoms, bonds, color, gen_coord) self.setPicture(pic) self.model.clearAnnotators()
[docs] def setAnnotators(self, annotators): """ This function allows to reset annotators between renderning 2-D structures. :type annotators: list :param annotators: Each item of the list should be a `canvas2d.ChemViewAnnotator` object that will be applied to the `canvas2d.ChmRender2DModel` when generating the image """ self.annotators = annotators
[docs]class StructureToolTip(StructurePicture): """ A tooltip that shows a chemical structure """ protein_present_image = None
[docs] def __init__(self, structure=None, offset=(10, 10), global_position=None, height=200, width=200, **kwargs): """ :type structure: structure object that canvas2d.ChmMmctAdaptor.create() accepts. :param structure: the structure to draw in the cell. This can be given at instantiation time if the structure will always be the same, or it can be given a show time if the structure will change dynamically. :type offset: tuple(int, int) :param offset: x and y pixel offset from the mouse pointer position to draw the upper left corner of the tooltip window :type global_position: tuple(int, int) :param global_position: global position relative to the screen to to draw the upper left corner of the tooltip window. This parameter overrides the offset parameter. :type height: int :param height: the height of this tooltip in pixels :type width: int :param width: the width of this tooltip in pixels This class is designed to be created once and shown/hidden as often as needed. However, there seems to be an issue with PyQt that eventually (in an unreproducible fashion) the tooltip window may simply showing up. It will claim to be visible with self.isVisible() and return the full window size using self.visibleRegion(), but it won't be visible. Therefore it is probably best to create a new instance each time. Instances of this class are lightweight, quick to create, and garbage collect without any apparent memory leaks. """ StructurePicture.__init__(self, height=height, width=width, **kwargs) self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.ToolTip) self.offset = offset self.top_left_text = None self.global_position = global_position if self.protein_present_image is None: self.protein_present_image = QtGui.QImage( ':/images/protein_present.png') if structure: self.drawStructure(structure) self.resize(width, height)
[docs] def paintEvent(self, event): """ Reimplmented the paint event to draw text on top of the image, using the value of self.top_left_text """ cr = self.contentsRect() br = self.picture().boundingRect() painter = QtGui.QPainter() painter.begin(self) self.drawFrame(painter) yo = old_div((cr.height() - br.height()), 2) xo = old_div((cr.width() - br.width()), 2) if self.width() < br.width() or self.height() < br.height(): width_ratio = old_div(float(self.width()), float(br.width())) height_ratio = old_div(float(self.height()), float(br.height())) scale = min(width_ratio, height_ratio) painter.scale(scale, scale) yo = old_div((cr.height() - (br.height() * scale)), 2) xo = old_div((cr.width() - (br.width() * scale)), 2) painter.drawPicture(cr.x() + xo - br.x(), cr.y() + yo - br.y(), self.picture()) painter.setBrush(QtGui.QBrush(QtCore.Qt.black, QtCore.Qt.NoBrush)) if self.top_left_text: alignment = QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft textrect = painter.boundingRect(3, 3, cr.width(), cr.height(), alignment, self.top_left_text) finalrect = painter.drawText(textrect, alignment, self.top_left_text) painter.end()
[docs] def setOffset(self, offset): """ Sets the x and y offset in pixels of the tooltip from the mouse pointer :type offset: tuple(int, int) :param offset: x and y pixel offset from the mouse pointer position to draw the upper left corner of the tooltip window """ self.offset = offset
[docs] def setGlobalPosition(self, global_position): """ Sets the x and y global position relative to the screen at which to draw the top left corner of the tooltip window. :type global_position: tuple(int, int) :param global_position: global position relative to the screen to draw the upper left corner of the tooltip window. This parameter overrides the offset parameter. """ self.global_position = global_position
def _addProteinImage(self): """ Updates the current picture to have the protein image in the top right corner. """ overlay = self.protein_present_image x_offset = self.renderer.getBoundingBox().right() - overlay.width() pic = QtGui.QPicture() pic.setBoundingRect(self.picture().boundingRect()) painter = QtGui.QPainter() painter.begin(pic) painter.drawPicture(0, 0, self.picture()) painter.drawImage(x_offset, 0, overlay) painter.end() self.setPicture(pic) def _showAtomCount(self, st): """ Sets the current picture with text as to why no 2D structure for `st` is shown. :param st: the structure :type st: structure.Structure """ pic = QtGui.QPicture() bb = self.renderer.getBoundingBox() br = QtCore.QRect(bb.top(), bb.left(), bb.width(), bb.height()) pic.setBoundingRect(br) painter = QtGui.QPainter() painter.begin(pic) txt = f'No small molecule:\n{st.atom_total} total atoms' painter.drawText(br, Qt.AlignCenter, txt) painter.end() self.setPicture(pic)
[docs] def show(self, structure=None, pic=None, top_left_text=None): """ Show the tooltip :type structure: structure object that canvas2d.ChmMmctAdaptor.create() accepts. :param structure: the structure to draw in the cell. This can be given at instantiation time if the structure will always be the same, or it can be given a show time if the structure will change dynamically. """ if top_left_text: self.top_left_text = top_left_text else: top_left_text = None if pic: self.setPicture(pic) elif structure: lig_st = get_ligand(structure) if lig_st: self.drawStructure(structure=lig_st) if structure.atom_total != lig_st.atom_total: # Inform the user that not all atoms are not shown: self._addProteinImage() else: # If no small molecule is present in the structure, at least # show how many atoms there are self._showAtomCount(structure) position = QtGui.QCursor.pos() if not self.global_position: if self.offset: position.setX(position.x() + self.offset[0]) position.setY(position.y() + self.offset[1]) else: position.setX(self.global_position[0]) position.setY(self.global_position[1]) self.move(position) QtWidgets.QLabel.show(self) self.raise_()
[docs] def finish(self): """ Hide ourselves. This slot should be connected to a signal that is emitted when the parent widget receives a leaveEvent, or called directly from the widget's leaveEvent routine. Don't destroy ourselves here even if we are being used in one-time use mode, because bus errors can result if we are created/destroyed in too short a timeframe - as can happen with tooltips. """ self.hide()
[docs]class LabeledStructureToolTip(QtWidgets.QFrame): """ Class that creates a tooltip with a 2d structure picture. An arbitrary number of labels can be added that will be displayed beneath the picture. """
[docs] def __init__(self, structure=None, offset=(2, 16), global_position=None, height=200, width=200, **kwargs): """ :type structure: `structure.Structure` or `canvas2d.ChmMol` :param structure: the structure to draw in the cell. This can be given at instantiation time if the structure will always be the same, or it can be given a show time if the structure will change dynamically. :type offset: tuple of 2 ints :param offset: x and y pixel offset from the mouse pointer position to draw the upper left corner of the tooltip window :type global_position: tuple of 2 ints :param global_position: global position relative to the screen to to draw the upper left corner of the tooltip window. This parameter overrides the offset parameter. :type height: int :param height: the height of this tooltip in pixels :type width: int :param width: the width of this tooltip in pixels """ QtWidgets.QFrame.__init__(self) self.layout = QtWidgets.QVBoxLayout(self) self.st_pic = StructurePicture(height=height, width=width, **kwargs) self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.ToolTip) self.offset = offset self.global_position = global_position self.draw2DPicture(structure) self.layout.addWidget(self.st_pic) self.resize(width, height)
[docs] def draw2DPicture(self, structure): """ Draws 2D picture of the structure. :param structure: Structure to draw :type structure: `structure.Structure` or `canvas2d.ChmMol` """ if structure: if isinstance(structure, canvas2d.ChmMol): self.st_pic.drawChmmol(structure) else: self.st_pic.drawStructure(structure)
[docs] def setGlobalPosition(self, global_position): """ Sets the x and y global position relative to the screen at which to draw the top left corner of the tooltip window. :type global_position: tuple(int, int) :param global_position: global position relative to the screen to draw the upper left corner of the tooltip window. This parameter overrides the offset parameter. """ self.global_position = global_position
[docs] def show(self, structure=None): """ Show the tooltip :type structure: `structure.Structure` or `canvas2d.ChmMol` :param structure: the structure to draw in the cell. This can be given at instantiation time if the structure will always be the same, or it can be given a show time if the structure will change dynamically. """ self.draw2DPicture(structure) position = QtGui.QCursor.pos() if not self.global_position: if self.offset: position.setX(position.x() + self.offset[0]) position.setY(position.y() + self.offset[1]) else: position.setX(self.global_position[0]) position.setY(self.global_position[1]) self.move(position) QtWidgets.QFrame.show(self) self.raise_()
[docs] def addLabel(self, text): """ Add a label containing the specified text underneath the structure picture. :param text: Text to be included in the label :type text: str """ label = QtWidgets.QLabel() label.setText(text) self.layout.addWidget(label)
[docs]class structure_scene(QtWidgets.QGraphicsScene): """ Scene which holds the structure_view object """
[docs]class structure_view(QtWidgets.QGraphicsView): """ View which holds a structure_item object """ # Signals to emit when user clicks on an atom or bond atom_clicked = QtCore.pyqtSignal(int) bond_clicked = QtCore.pyqtSignal((int, int))
[docs] def __init__(self, scene): super(structure_view, self).__init__(scene) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
[docs] def wheelEvent(self, event): pass
[docs] def resizeEvent(self, event): self.fitInView(self.scene().sceneRect(), QtCore.Qt.KeepAspectRatio)
[docs]class structure_item(QtWidgets.QGraphicsItem):
[docs] def __init__(self, rect=None): """ :type rect: QRectF :param rect: Size of the rect of the bounding box. """ if rect: self.rect = rect else: self.set_rect(QtCore.QRectF(0, 0, 500, 500)) super(structure_item, self).__init__() self.colormap = None self.chmmol = None self.pic = None self.adaptor = canvas2d.ChmMmctAdaptor() self.model2d = canvas2d.ChmRender2DModel() self.renderer = canvas2d.Chm2DRenderer(self.model2d) self.annotators = [] self.annotator_function_returns = []
[docs] def boundingRect(self): return self.rect
[docs] def clear(self): """ Clear picture from item. """ self.pic = None self.chmmol = None self.update()
[docs] def generate_picture(self, gen_coord=True): """ Generates a QPicture of the structure. This should be called after setting the structure and the accompaning annotators. :type gen_coord: bool :param gen_coord: if True generate coordinates. """ if self.colormap: self.model2d.setColorMap(self.colormap) for ann in self.annotators: self.model2d.addAnnotator(ann) self.renderer.setModel(self.model2d) renderer_rect = canvas2d.ChmRect2D(self.rect.x(), self.rect.y(), self.rect.width(), self.rect.height()) self.renderer.setBoundingBox(renderer_rect) self.pic = get_qpicture_protected(self.renderer, self.chmmol, gen_coord) ret = [] for func in self.annotator_function_returns: ret.append(func) # cleanup if self.colormap: self.model2d.clearColorMap() self.model2d.clearAnnotators() self.update() return ret
[docs] def paint(self, painter, option, widget=0): """ Overrides the paint function to draw the picture that has already been generated. This should never be called manually. """ if not self.pic: return painter.drawPicture(0, 0, self.pic)
[docs] def set_colormap(self, colormap): """ Sets the colormap for the atom and bond coloring. Otherwise, the coloring used will be that coming from the ct and/or chmmol. """ self.colormap = colormap
[docs] def set_structure(self, struct, StereoType=canvas2d.ChmMmctAdaptor. StereoFromAnnotationAndGeometry_Safe, hydrogenTreatment=canvas2d.ChmAtomOption.H_ExplicitOnly, wantProperties=True, wantMMStereoProps=True, readAtomBondProperties=True, allowRadicals=False): """ Set the structure to the given Structure object. :type struct: schrodinger.structure.Structure class object :param struct: structure to be drawn on the canvas :type StereoType: ChmMmctAdaptor.StereoType :param StereoType: Stereochemistry option to use. Avialable options: # Does not include stereochemistry info: canvas2d.ChmMmctAdaptor.NoStereo # Ignores mmstereo annotations: canvas2d.ChmMmctAdaptor.StereoFromGeometry # Silently ignores stereo information that Canvas doesn't agree with: canvas2d.ChmMmctAdaptor.StereoFromGeometry_Safe # Ignores 3d geometry: canvas2d.ChmMmctAdaptor.StereoFromAnnotation # Silently ignores stereo information that Canvas doesn't agree with: canvas2d.ChmMmctAdaptor.StereoFromAnnotation_Safe # Uses mmstereo annotaions with 3D geometry as a backup: canvas2d.ChmMmctAdaptor.StereoFromAnnotationAndGeometry # Silently ignores stereo information that Canvas doesn't agree with: canvas2d.ChmMmctAdaptor.StereoFromAnnotationAndGeometry_Safe :type hydrogenTreatment: canvas2d.ChmAtomOption.H :param hydrogenTreatment: Hydrogen treatment method. canvas2d.ChmAtomOption.H_Never canvas2d.ChmAtomOption.H_ExplicitOnly canvas2d.ChmAtomOption.H_Polar canvas2d.ChmAtomOption.H_ExplicitPolar canvas2d.ChmAtomOption.H_Chiral canvas2d.ChmAtomOption.H_ExplicitChiral canvas2d.ChmAtomOption.H_ExplicitPolarAndChiral canvas2d.ChmAtomOption.H_All :type wantProperties: bool :param wantProperties: Whether properties should be copied. :type wantMMStereoProps: bool :param wantMMStereoProps: Whether to copy mmstereo properties. :type readAtomBondProperties: bool :param readAtomBondProperties: Whether to copy atom and bond-level properties. :type allowRadicals: bool :param allowRadicals: Whether to assume that valence deficiencies represent unpaired electrons. """ self.chmmol = self.adaptor.create(int(struct), StereoType, hydrogenTreatment, wantProperties, wantMMStereoProps, readAtomBondProperties, allowRadicals)
[docs] def set_rect(self, rect): """ :type rect: QRect :param rect: size of bounding box """ self.rect = rect
[docs] def set_text(self, text, alignment=None): """ Sets text in the view. This is useful if you display text in place of a structure, in places where no structure is available. :type text: string :param text: text to be displayed :type alignment: Qt.AlignmentFlags :param alignment: alignment flags of text, defaults to QtCore.Qt.AlignVCenter|QtCore.Qt.AlignCenter """ if not alignment: alignment = QtCore.Qt.AlignVCenter | QtCore.Qt.AlignCenter picture = QtGui.QPicture() painter = QtGui.QPainter(picture) textrect = painter.boundingRect(self.boundingRect().toAlignedRect(), alignment, text) finalrect = painter.drawText(textrect, alignment, text) painter.end() picture.setBoundingRect(finalrect) self.pic = picture
[docs] def add_annotator(self, annotator): """ Adds annotator to stack. The order that these functions get added is the order that they will be applied to the picture. :type annotator: schrodinger.infra.canvas2d.ChemViewAnnotator :param annotator: Annotator which draws on top an image """ self.annotators.append(annotator)
[docs] def add_annotator_function_return(self, function): """ Add functions that need to get returned after the structure is rendered on the screen. An example of this would be a function that returns drawmol coordinates. """ self.annotator_function_returns.append(function)
[docs] def clear_annotators(self): """ Clears all annotators """ self.annotators = [] self.annotator_function_returns = []
[docs] def mousePressEvent(self, event): """ Emit a signal if the user left-clicked on an atom or a bond """ if not self.pic: return QtWidgets.QGraphicsItem.mousePressEvent(self, event) if event.button() != QtCore.Qt.LeftButton: return QtWidgets.QGraphicsItem.mousePressEvent(self, event) xval = event.pos().x() - self.pic.boundingRect().left() yval = event.pos().y() - self.pic.boundingRect().top() width = self.pic.boundingRect().width() height = self.pic.boundingRect().height() index = self.renderer.getAtomAtLocation(self.chmmol, width, height, xval, yval) + 1 try: view = self.scene().views()[0] except (IndexError, AttributeError): # If not assigned to a view or scene (though a click seems unlikely # in that situation) return QtWidgets.QGraphicsItem.mousePressEvent(self, event) if index: try: view.atom_clicked.emit(index) except AttributeError: pass else: bond = self.renderer.getBondAtLocation(self.chmmol, width, height, xval, yval) if bond: bonded_atoms = [x + 1 for x in bond] try: view.bond_clicked.emit(*bonded_atoms) except AttributeError: pass return QtWidgets.QGraphicsItem.mousePressEvent(self, event)
[docs] def getBondAtLocation(self, pos): """ Return the bond at the specified coordinates :param pos: The specified coordinates :type pos: `PyQt5.QtCore.QPoint` :return: If there is a bond at the specified coordinates, return a list of the chmmol atom indices for the two bound atoms. (Note that the chmmol atom indices are zero-indexed, so you should add one to each index if you want the `schrodinger.structure.Structure` atom indices). If there is no bond at the specified coordinates, return an empty list. :rtype: list """ pos = pos - self.pic.boundingRect().topLeft() rect = self.pic.boundingRect() return self.renderer.getBondAtLocation(self.chmmol, rect.width(), rect.height(), pos.x(), pos.y())
[docs]class ColoredArrowAnnotator(canvas2d.ChemViewAnnotator): """ This annotator allows you to add colored arrows to bonds in 2d renderings. This is a slightly less terrible port from C++. Most of the logic still keeps C++-style syntax, simply because rewriting it is not worth the effort. The interface is now more pythonic, at least. """
[docs] def __init__(self, mol, bond_info=None, draw_arrowhead=True): """ :param mol: is a chmmol :param bond_info: is a list containing individual lists of: (atom1, atom2, outlined, QColor) These lists are composed of: - atom1 is a ct-atom index - atom2 is a ct-atom index - outlined (0/1) is whether you want the arrow to have a black outline - qc is a qcolor for the bond """ canvas2d.ChemViewAnnotator.__init__(self) self.bond_info = bond_info or [] mol = canvas2d.convertChmMoltoSWIG(mol) self.draw_arrowhead = draw_arrowhead
[docs] def add_bond_arrow(self, atom1, atom2, outlined, qc): """ atom1 is a ct-atom index atom2 is a ct-atom index outlined (0/1) is whether you want the arrow to have a black outline qc is the qcolor of the bond """ self.bond_info.append([atom1, atom2, outlined, qc])
[docs] def clear_bond_arrows(self): """ Clear all bond markers in this annotator """ self.bond_info = [] return
[docs] def annotate(self, view, dm): """ This function annotates the specified arrows. """ QPointF = QtCore.QPointF QRectF = QtCore.QRectF QPolygonF = QtGui.QPolygonF # Determine bond direction indicators. This code was moved here # from the initializer to fix RGA-866. bsz = len(dm.bonds) colors = [0] * bsz bondDirectionIndicators = [0] * bsz * 2 for (atom1, atom2, outlined, qc) in self.bond_info: atom1 -= 1 #change to 0-based chmmol indices atom2 -= 1 #change to 0-based chmmol indices for i in range(bsz): b = dm.bonds[i] a1, a2 = b.a1, b.a2 bondDir = False if a1.getMolIndex() == atom1 and a2.getMolIndex() == atom2: bondDir = 1 colors[i] = qc elif a2.getMolIndex() == atom1 and a1.getMolIndex() == atom2: bondDir = 2 if bondDir: bondDirectionIndicators[i * 2] = bondDir bondDirectionIndicators[i * 2 + 1] = outlined colors[i] = qc if not bondDirectionIndicators: return qp = QtGui.QPen() qp.setJoinStyle(QtCore.Qt.MiterJoin) qp.setWidth(0) for i in range(bsz): if bondDirectionIndicators[i * 2]: if bondDirectionIndicators[i * 2 + 1] > 0: qp.setColor(QtGui.QColor(0, 0, 0)) else: qp.setColor(colors[i]) a1, a2 = dm.bonds[i].a1, dm.bonds[i].a2 if bondDirectionIndicators[i * 2] == 2: a1, a2 = dm.bonds[i].a2, dm.bonds[i].a1 p = QPolygonF() for j in [ QPointF(a1.x(), a1.y()), QPointF(a1.x() + 0.1, a1.y() + 0.1), QPointF(a2.x() + 0.1, a2.y() + 0.1), QPointF(a2.x(), a2.y()), QPointF(a1.x(), a1.y()) ]: p.append(j) br = dm.bonds[i].boundingRect() p2 = QPolygonF() for j in [ br.topLeft(), br.topRight(), br.bottomRight(), br.bottomLeft(), br.topLeft() ]: p2.append(j) p = p.intersected(p2) points = [] for point in p: points.append((point.x(), point.y())) def calc_endpoint(p_in, points): def dist_sqrd(p1, p2): return (p1[0] - p2[0]) * (p1[0] - p2[0]) + \ (p1[1] - p2[1]) * (p1[1] - p2[1]) ax, ay = points[0] dsqd = dist_sqrd(p_in, points[0]) for p in points[1:]: tmp_dsqd = dist_sqrd(p_in, p) if tmp_dsqd < dsqd: ax, ay = p dsqd = tmp_dsqd return (ax, ay) a1x, a1y = calc_endpoint((a1.x(), a1.y()), points) a2x, a2y = calc_endpoint((a2.x(), a2.y()), points) x = a1x y = a1y xChange = x - (a1x * (old_div(4., 6.)) + a2x * (old_div(2., 6.))) yChange = y - (a1y * (old_div(4., 6.)) + a2y * (old_div(2., 6.))) x2 = (a2x * (old_div(4., 6.)) + a1x * (old_div(2., 6.))) y2 = (a2y * (old_div(4., 6.)) + a1y * (old_div(2., 6.))) change = [xChange, yChange] multiplier = 9 if not self.draw_arrowhead: multiplier = 15 xChange = xChange / numpy.linalg.norm(change) * \ view.getModel().getBondLineWidth() * multiplier yChange = yChange / numpy.linalg.norm(change) * \ view.getModel().getBondLineWidth() * multiplier # Here is the arrow as we draw it # A # / \ # / \ # B-C-F-G # || # || # DE na = numpy.array p = [] if self.draw_arrowhead: p.append(QPointF(x, y)) #A p.append( QPointF(x - (xChange + yChange), y - (yChange - xChange))) #B (px, py) = self._seg_intersect( na([x - (xChange + yChange), y - (yChange - xChange)]), na([x - (xChange - yChange), y - (yChange + xChange)]), na([ x - old_div((xChange + yChange), 3), y - old_div( (yChange - xChange), 3) ]), na([ x2 - old_div((xChange + yChange), 3), y2 - old_div( (yChange - xChange), 3) ])) C = QPointF(px, py) p.append(C) #C p.append( QPointF(x2 - old_div((xChange + yChange), 3), y2 - old_div( (yChange - xChange), 3))) #D p.append( QPointF(x2 - old_div((xChange - yChange), 3), y2 - old_div( (yChange + xChange), 3))) #E (px, py) = self._seg_intersect( na([x - (xChange + yChange), y - (yChange - xChange)]), na([x - (xChange - yChange), y - (yChange + xChange)]), na([ x - old_div((xChange - yChange), 3), y - old_div( (yChange + xChange), 3) ]), na([ x2 - old_div((xChange - yChange), 3), y2 - old_div( (yChange + xChange), 3) ])) p.append(QPointF(px, py)) #F if self.draw_arrowhead: p.append( QPointF(x - (xChange - yChange), y - (yChange + xChange))) #G p.append(QPointF(x, y)) #A, to close shape else: p.append(C) qpoly = QtGui.QPolygonF(p) qgpi = QtWidgets.QGraphicsPolygonItem(qpoly) qgpi.setPen(qp) qgpi.setBrush(QtGui.QBrush(colors[i])) qgpi.setZValue(10) qgpi.setParentItem(dm.bonds[i])
def _perp(self, a): b = numpy.empty_like(a) b[0] = -a[1] b[1] = a[0] return b def _seg_intersect(self, a1, a2, b1, b2): """ line 1 given by endpoints a1, a2 line 2 given by endpoints b1, b2 """ da = a2 - a1 db = b2 - b1 dp = a1 - b1 dap = self._perp(da) denom = numpy.dot(dap, db) num = numpy.dot(dap, dp) return (old_div(num, denom)) * db + b1
[docs]class CgCoreAnnotator(ColoredArrowAnnotator): """ This is the original ColoredArrowAnnotator class name. When that class had the format of its input changed, it became incompatible with the original class. This class will remain to preserve backwards compatability. """
[docs] def __init__(self, bond_info, colors, atoms, atomColors, mol): """ bond_info is a list of size 3N, (atom1 idx, a2 idx, outline) colors is a list of size 3N (R, G, B) atoms/atomColors are deprecated mol is a chmmol """ rearranged_input = [] for i in range(old_div(len(bond_info), 3)): rearranged_input.append([ bond_info[i * 3], bond_info[i * 3 + 1], bond_info[i * 3 + 2], QtGui.QColor(colors[i * 3], colors[i * 3 + 1], colors[i * 3 + 2]) ]) ColoredArrowAnnotator.__init__(self, mol, rearranged_input)
[docs]class BaseSquareAnnotator(canvas2d.ChemViewAnnotator): """ Base class for annotators that draw a square around atoms """
[docs] def annotate(self, view, dm): """ Add this sphere to the list of things in the picture :type view: Chemview :param view: the View this item goes in :type dm: ChmDrawMol :param dm: object that contains the list of atom and bond graphics """ for atom_num, color in self._label_dict.items(): brush = QtGui.QBrush(QtCore.Qt.transparent) pen = QtGui.QPen(color) pen.setWidth(4) # Create the square point = old_div(-self.size, 2) square = QtWidgets.QGraphicsRectItem(point, point, self.size, self.size) square.setBrush(brush) square.setPen(pen) # Puts this square at the same location as the atom square.setParentItem(dm.atoms[atom_num])
[docs]class RedSquareAnnotator(BaseSquareAnnotator): """ Create a square around the specified atom. """
[docs] def __init__(self, atom=None, size=100, color=QtCore.Qt.red): """ Instantiate a RedSquareAnnotator instance. :type atom: int :param atom: the atom number to which square applies. Note that this expects the first atom to be atom 1, not 0. :type size: float :param size: the size of one side of the square in pixels. :type color: QColor :param color: the color used to draw annotator. Default is red. """ canvas2d.ChemViewAnnotator.__init__(self) self._label_dict = {} if atom: self._label_dict[atom - 1] = color self.size = size self.color = color
[docs] def setAtom(self, atom, color=None): """ Set the atom number to which the square applies :type atom: int :param atom: the atom number to which square applies. Note that this expects the first atom to be atom 1, not 0. If 0 is passed in, no atom will be annotated. :type color: QColor :param color: the color used to draw annotator. If not given, the previously set color for this annotator will be used. """ if not atom: self.clearAtom() return if color is None: color = self.color self._label_dict = {atom - 1: color}
[docs] def clearAtom(self): """ Remove the current atom so no atoms are annotated """ self._label_dict = {}
[docs] def getAtom(self): """ Return the atom index current annotated :rtype: int or None :return: The index (1-based) of the atom annotated, or None if no atoms are annotated """ try: return list(self._label_dict)[0] + 1 except IndexError: return None
[docs] def setColor(self, color): """ Set the color of the square :type color: QColor :param color: the color used to draw annotator. """ self.color = color
[docs]class MultiSquareAnnotator(BaseSquareAnnotator): """ Create a square around each specified atom. """
[docs] def __init__(self, atom_dict, size=100): """ Instantiate a MultiSquareAnnotator instance. :type atom_dict: dict :param atom_dict: A dictionary of {atom number: QColor} specifying the appropriate color for each atom :type size: float :param size: The size of one side of the square in pixels. Defaults to 100 pixels. """ canvas2d.ChemViewAnnotator.__init__(self) self.size = size self._label_dict = {key - 1: val for key, val in atom_dict.items()}
[docs]class AtomNumberAnnotator(canvas2d.ChemViewAnnotator): """ Show the atom number of each atom rather than a vertex or atomic symbol """
[docs] def annotate(self, view, dm): """ Add atom number to each atom :type view: Chemview :param view: the View this item goes in :type dm: ChmDrawMol :param dm: object that contains the list of atom and bond graphics """ for i, a in enumerate(dm.atoms): a.replaceLabel(str(i + 1), "") dm.recreateBonds()
[docs]class AtomLabelAnnotator(canvas2d.ChemViewAnnotator): """ Changes the label displayed for an atom. The label can have subscripts. """
[docs] def __init__(self, atom_labels): """ Instantiate a AtomLabelAnnotator instance. :type atom_labels: dict :param atom_labels: A dictionary of {atom number: label} specifying the label for each atom to be custom labeled. Each label may either be a string or a (str, str) tuple. In the latter case, the first string is the main label and the second string is the subscript. Note the atom numbers used here should be 1-based (first atom number = 1) """ canvas2d.ChemViewAnnotator.__init__(self) self.atom_labels = atom_labels
[docs] def annotate(self, view, drawmol): """ Add a label to each atom specified in the atom_labels property :type view: Chemview :param view: the View this item goes in :type drawmol: ChmDrawMol :param drawmol: object that contains the list of atom and bond graphics """ for index, atom in enumerate(drawmol.atoms, 1): label = self.atom_labels.get(index) if isinstance(label, str): atom.replaceLabel(label, "") elif isinstance(label, tuple): atom.replaceLabel(label[0], label[1])
[docs]class CircleAnnotator(canvas2d.ChemViewAnnotator): """ Creates a circle behind one or more atoms. """
[docs] def __init__(self, atom=None, radius=75., color=QtCore.Qt.green, gradient=False, fill=False, width=4): """ Instantiate a ColorCircleAnnotator instance. :type atom: int :param atom: the atom number to which radius applies. Note that this expects the first atom to be atom 1, not 0. :type radius: float :param radius: the radius of the sphere in pixels. Default is 75. :type color: QColor :param color: The color of the circle fill :type gradient: bool :param gradient: If True, the circle is filled with a gradient that goes from white at the center to the defined color. gradient is exclusive with fill. :type fill: bool :param fill: If True, the circle is given a constant fill. fill is exclusive with gradient. :type width: int :param width: The width of the pen if fill and gradient are both False. Default is 4. """ canvas2d.ChemViewAnnotator.__init__(self) self._label_dict = {} self.color = color self.radius = radius if atom is not None: self.addAtom(atom) self.fill = fill self.gradient = gradient if self.fill and self.gradient: raise RuntimeError('gradient and fill cannot both be True') self.width = width
[docs] def addAtom(self, atom_num, radius=None, color=None): """ Add another sphere to this annotator :type atom_num: int :param atom_num: the atom number to which radius applies. Note that this expects the first atom to be atom 1, not 0. :type radius: float :param radius: the radius of the sphere in pixels. If not given, the radius given at the time of instance creation is used. :type color: QColor :param color: The color of the circle fill. If not given, the color given at the time of instance creation is used. """ if not radius: radius = self.radius if not color: color = self.color self._label_dict[atom_num - 1] = (radius, color)
[docs] def removeAtom(self, atom_num): """ Remove an atom from the annotator :type atom_num: int :param atom_num: the atom number to which radius applies. Note that this expects the first atom to be atom 1, not 0. """ del self._label_dict[atom_num - 1]
[docs] def clearAtoms(self): """ Remove all atoms from the annotator """ self._label_dict = {}
[docs] def setColor(self, color, reset_colors=False): """ Set the default color for this annotator :type color: QColor :param color: The color of the circle fill. :type reset_colors: bool :param reset_colors: If True, all existing circles will have their color reset to this value """ self.color = color if reset_colors: for key in list(self._label_dict): radius, old_color = self._label_dict[key] self._label_dict[key] = (radius, color)
[docs] def setRadius(self, radius, reset_radii=False): """ Set the default radius for this annotator :type radius: float :param radius: The radius of the circle :type reset_radii: bool :param reset_radii: If True, all existing circles will have their radius reset to this value """ self.radius = radius if reset_radii: for key in list(self._label_dict): old_radius, color = self._label_dict[key] self._label_dict[key] = (radius, color)
[docs] def annotate(self, view, dm): """ Add this sphere to the list of things in the picture :type view: Chemview :param view: the View this item goes in :type dm: ChmDrawMol :param dm: object that contains the list of atom and bond graphics """ for atom_num, data in self._label_dict.items(): radius, color = data pen = QtGui.QPen(color) # Create a gradient that goes from white at the center to full green # halfway along the radius of the circle. if self.gradient: gradient = QtGui.QRadialGradient(0, 0, radius) gradient.setColorAt(0, QtCore.Qt.white) gradient.setColorAt(0.5, color) brush = QtGui.QBrush(gradient) elif self.fill: brush = QtGui.QBrush(color) else: brush = None pen.setWidth(self.width) # Create the circle point = old_div(-radius, 2) circle = QtWidgets.QGraphicsEllipseItem(point, point, radius, radius) if brush is not None: circle.setBrush(brush) # Set the pen to the same color as the sphere so there is no border circle.setPen(pen) # Puts this circle at the same location as the atom circle.setParentItem(dm.atoms[atom_num]) # Puts the circle behind the atom circle.setFlag(QtWidgets.QGraphicsItem.ItemStacksBehindParent) # Makes sure the atom stacks behind any bonds dm.atoms[atom_num].setZValue(-50)
[docs]class ResizableStructurePicture(StructurePicture): """ StructurePicture widget that resizes the structure image with the widget. """
[docs] def __init__(self, parent=None, layout=None, height=200, width=200, margin=10, background='white', annotators=None): """ See `StructurePicture` for all arguments, except: :param margin: the margin to use around the image :type margin: int """ super(ResizableStructurePicture, self).__init__(parent, layout, height, width, background, annotators) self._margin = margin self.setFrameShape(QtWidgets.QFrame.NoFrame) self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) self._args = [] # arguments needed for resizing/redrawing
[docs] def drawChmmol( self, chmmol, atoms=[], # noqa: M511 bonds=[], # noqa: M511 color=QtGui.QColor(255, 255, 255), # noqa: M511 gen_coord=False): super(ResizableStructurePicture, self).drawChmmol(chmmol, atoms, bonds, color, gen_coord) # when resizing the image the gen_coord should be False self._args = [chmmol, atoms, bonds, color, False]
[docs] def resizeEvent(self, event): super(ResizableStructurePicture, self).resizeEvent(event) if self._args: self.resizeImage()
[docs] def resizeImage(self): rect = canvas2d.ChmRect2D(0, 0, self.width() - self._margin, self.height() - self._margin) self.renderer.setBoundingBox(rect) self.drawChmmol(*self._args)