import io
import os
import sys
from functools import lru_cache
from past.utils import old_div
from typing import List
from typing import Optional
from typing import Tuple
import numpy as np
from rdkit import Chem
from rdkit.Chem.rdchem import Mol
from schrodinger import structure
from schrodinger.infra import canvas2d
from schrodinger.livedesign import substructure
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.Qt.QtGui import QColor
from schrodinger.rdkit.alignment import generate_min_height_coords
from schrodinger.structutils import analyze
from schrodinger.ui import sketcher
from schrodinger.ui.qt import icons
from schrodinger.ui.qt import structure2d
from schrodinger.ui.qt import swidgets
# Flags for including/removing an entry from the workspace
WS_REMOVE, WS_INCLUDE, WS_INCLUDE_ONLY = list(range(3))
# Custom Roles
NUM_INCLUDED_ENTRIES_ROLE = Qt.UserRole + 657390
_MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
# A dictionary of {icon path: QImage}.  We can't create the QImage now since it
# requires QApplication instance, which may not yet exist.  Instead, QImages are
# instantiated in AbstractDelegateWithEditIcon.__init__.
_EDIT_ICONS = {}
# Data role for sending text information to PictureDelegate
PICTURE_TEXT_ROLE = Qt.UserRole + 9000
[docs]class CenteredIconDelegate(QtWidgets.QStyledItemDelegate):
    """
    Displays a center-aligned icon based on a `QPixmap` received from
    `Qt.DecorationRole`. Does nothing if `Qt.DecorationRole` returns `None`.
    """
[docs]    def paint(self, painter, option, index):
        """
        Paint the icon supplied by `QDecorationRole` into each cell.
        :param painter: The painter being used to render the delegate
        :type painter: QPainter
        :param option: The style options to use when painting
        :type option: QStyleOptionViewItem
        :param index: The index being represented
        :type index: QtCore.QModelIndex
        """
        pixmap = index.data(Qt.DecorationRole)
        icon = QtGui.QIcon(pixmap)
        if icon is not None:
            align = Qt.AlignCenter
            icon.paint(painter, option.rect, align)  
[docs]class AbstractDelegateWithEditIcon(QtWidgets.QStyledItemDelegate):
    """
    A base class for delegates that paint an edit icon.
    :cvar EDIT_ICON: The file name for the icon to paint on editable cells.
        This file must be in the same directory as this module.
    :vartype EDIT_ICON: str
    """
    EDIT_ICON = icons.PENCIL_ICON
[docs]    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.EDIT_ICON not in _EDIT_ICONS:
            _EDIT_ICONS[self.EDIT_ICON] = QtGui.QImage(self.EDIT_ICON)
        self._edit_icon = _EDIT_ICONS[self.EDIT_ICON] 
    def _drawEditIcon(self, painter, option, index):
        """
        For use in the paint() method of delegates that allow editing of values.
        Draws `EDIT_ICON` in the right-side of the cell if that cell has
        ItemIsEnabled and ItemIsEditable flags set.
        """
        flags = index.flags()
        if not flags & Qt.ItemIsEnabled or not flags & Qt.ItemIsEditable:
            return
        render_size = QtCore.QSize(12, 12)
        icon_rect = QtWidgets.QStyle.alignedRect(
            Qt.LeftToRight, Qt.AlignRight | Qt.AlignVCenter, render_size,
            option.rect)
        # Add a buffer on the right:
        icon_rect.translate(-4, 0)
        painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform)
        painter.drawImage(icon_rect, self._edit_icon) 
[docs]class LineEditDelegate(AbstractDelegateWithEditIcon):
    """
    Delegate recommended for use with all Schrodinger table model. It's just
    like the standard Qt delegate, except that if a table cell has the
    ItemIsEditable flag set, it will draw a "pencil" icon on the right side
    of the cell.
    QLineEdit widget will be shown for cells that return str values for data,
    and QSpinBox widget will be shown for cells that return int or float values.
    To limit allowed min/max values, use SpinBoxDelegate.
    """
    # TODO consider using QLineEdit widget even for numeric values, or
    # renaming this class.
[docs]    def paint(self, painter, option, index):
        # See Qt documentation for method documentation
        super(LineEditDelegate, self).paint(painter, option, index)
        view = self.parent()
        # Don't draw the edit icon when editor is up
        if (isinstance(view, QtWidgets.QTableView) and
                view.state() == view.EditingState):
            return
        self._drawEditIcon(painter, option, index)  
[docs]class MouseTrackingDelegateMixin:
    """
    A mixin for a `QtWidgets.QStyledItemDelegate` that tracks which cell the
    mouse is in.
    """
[docs]    def __init__(self, view):
        """
        :param view: The view that this delegate will be added to.  Note that
            mouse tracking will be enabled in the view
        :type view: QtWidgets.QTableView
        """
        super().__init__(view)
        self._mouse_rc = None  # row and column value, or None
        view.setMouseTracking(True)
        view.viewport().installEventFilter(self) 
    @property
    def mouse_data(self):
        """
        Returns the data for the cell that the mouse is in.
        :return: the data for the cell that the mouse is on or None
        :rtype: object
        """
        if self._mouse_rc is None:
            return None
        view = self.parent()
        model = view.model()
        index = model.index(*self._mouse_rc)
        return index.data()
[docs]    def createEditor(self, parent, option, index):
        """
        This delegate does not have a separate editor widget, so we return None.
        All arguments are ignored, but are present for Qt compatibility.
        """
        return None 
[docs]    def eventFilter(self, viewport, event):
        """
        The delegate does not receive Leave events or MouseMove events that
        don't occur over an index (e.g. events that occur over a header or over
        blank space in the view), so we have to act as an event filter for the
        view's viewport to observe these events.
        :param viewport: The view's viewport.  Not used, but present for
            compatibility with Qt's event filtering.
        :type viewport: QtWidgets.QWidget
        :param event: The event to filter
        :type event: QtCore.QEvent
        :return: True if the event was handled and does not need to be passed to
            the viewport.  False otherwise.  We want all events to be passed to
            the viewport, so we always return False.
        :rtype: bool
        """
        if event.type() not in (event.MouseMove, event.Leave):
            # This filter is only concerned with specific cursor-related events
            return False
        model = self.parent().model()
        if event.type() == event.Leave:
            if self._mouse_rc is not None:
                row, col = self._mouse_rc
                self._mouse_rc = None
                self._redrawRc(model, row, col)
        elif event.type() == event.MouseMove:
            view = self.parent()
            pos = event.pos()
            index = view.indexAt(pos)
            prev_rc = self._mouse_rc
            self._mouse_rc = self._getRc(index)
            if self._mouse_rc == (-1, -1):
                self._mouse_rc = None
            if self._mouse_rc != prev_rc:
                self._redrawIndex(model, index)
        return False 
    def _redrawIndex(self, model, index):
        """
        Alert the view that the specified index needs to be redrawn.
        We use the model's dataChanged signal to accomplish this.
        :param model: The data model.
        :type model: QtCore.QAbstractTableModel
        :param index: The index to redraw.
        :type index: QtCore.QModelIndex
        """
        model.dataChanged.emit(index, index)
    def _getRc(self, index):
        """
        Get the row and column of the specified index.
        :param index: The table index to get (row, column) of
        :type index: QtCore.QModelIndex or NoneType
        :return: If the index is not None, return (row, column).
            Otherwise, return None.
        :rtype: tuple or NoneType
        """
        if index is not None:
            return (index.row(), index.column())
    def _redrawRc(self, model, row, col):
        """
        Alert the view that the cell at the specified row and column needs to be
        redrawn.
        We use the model's dataChanged signal to accomplish this.
        :param model: The data model.
        :type model: QtCore.QAbstractTableModel
        :param row: The row of the index to redraw
        :type row: int
        :param col: The column of the index to redraw
        :type col: int
        """
        index = model.index(row, col)
        self._redrawIndex(model, index) 
[docs]class SpinBoxDelegate(LineEditDelegate):
    """
    A delegate that provides a `QtWidgets.QSpinBox` as an editor.  The minimum and
    maximum allowed values for the spin box are set using data retrieved
    via the SPINBOX_LIMITS_ROLE.
    Editing is initiated when the cell is clicked. Note that the mouse down and
    up must both occur in the same cell; otherwise, editing will not be started.
    This allows the user to highlight multiple cells by clicking and dragging.
    NOTE: Use STableView in order to enter the edit mode after a click.
    """
    SPINBOX_LIMITS_ROLE = Qt.UserRole + 101
    # Tell STableView to enter the edit mode when the user clicks in the cell:
    MOUSE_CLICK_STARTS_EDIT = True
[docs]    def createEditor(self, parent, option, index):
        # See Qt documentation for method documentation
        spin = QtWidgets.QSpinBox(parent)
        spin_min, spin_max = index.data(self.SPINBOX_LIMITS_ROLE)
        spin.setRange(spin_min, spin_max)
        return spin 
[docs]    def setEditorData(self, editor, index):
        # See Qt documentation for method documentation
        data = index.data(Qt.EditRole)
        editor.setValue(data) 
[docs]    def setModelData(self, editor, model, index):
        # See Qt documentation for method documentation
        data = editor.value()
        model.setData(index, data) 
[docs]    def updateEditorGeometry(self, editor, option, index):
        # See Qt documentation for method documentation
        editor.setGeometry(option.rect)  
class _TableComboBox(QtWidgets.QComboBox):
    """
    A ComboBox with some modifications:
    1. It is never actually drawn - ComboBoxDelegate.paint() handles drawing
       the contents of the cells.
    2. Releasing the mouse when the menu is up does not automatically close it
       if the release is within "drag timeout" of the initial press. This allows
       the user to click the cell to bring up the menu, move the mouse, and
       click on the new item to select it.
    3. Emits popUpClosed when the menu is closed.
    """
    popUpClosed = QtCore.pyqtSignal()
    def __init__(self, parent=None):
        """
        :param parent: The parent widget.
        :type parent: `QWidget`
        """
        super(_TableComboBox, self).__init__(parent)
        self.ignore_next_hide_popup = True
        drag_time = QtWidgets.QApplication.instance().startDragTime()
        QtCore.QTimer.singleShot(drag_time, self._dragTimedOut)
    def _dragTimedOut(self):
        self.ignore_next_hide_popup = False
    def paintEvent(self, event):
        # Do not actually draw a combo box - use it only for the menu.
        # The contents of cells will be rendered by ComboBoxDelegate.paint().
        return
    def hidePopup(self):
        # See Qt documentation for method documentation
        if self.ignore_next_hide_popup:
            self.ignore_next_hide_popup = False
            return
        super(_TableComboBox, self).hidePopup()
        QtCore.QTimer.singleShot(0, self.popUpClosed.emit)
[docs]class ComboBoxDelegate(AbstractDelegateWithEditIcon):
    """
    A delegate that provides a `_TableComboBox` as an editor.  The combo box is
    populated using a list or OrderedDict retrieved via the COMBOBOX_ROLE.
    NOTE: Use STableView in order to open the combo menu with a single click.
    """
    COMBOBOX_ROLE = Qt.UserRole + 102
    # Tell STableView to enter the edit mode when the user presses the mouse in
    # the cell. This properly handles both a click (press & release) and a drag.
    MOUSE_PRESS_STARTS_EDIT = True
    EDIT_ICON = icons.COMBOBOX_ICON
[docs]    def paint(self, painter, option, index):
        # See Qt documentation for method documentation
        # Draw the label (selected item text):
        super(ComboBoxDelegate, self).paint(painter, option, index)
        # Draw the down-pointing triangle:
        self._drawEditIcon(painter, option, index) 
[docs]    def createEditor(self, parent, option, index):
        # See Qt documentation for method documentation
        combo = _TableComboBox(parent)
        combo.popUpClosed.connect(lambda: self._popUpClosed(combo))
        items = index.data(self.COMBOBOX_ROLE)
        if isinstance(items, list):
            # Use each value for both representation and data
            for value in items:
                combo.addItem(value, value)
        else:
            # Assume a dictionary
            for key, val in items.items():
                combo.addItem(key, val)
        # Show the pop up only after the current item is selected based on the
        # contents of the model:
        QtCore.QTimer.singleShot(0, combo.showPopup)
        return combo 
[docs]    def setEditorData(self, editor, index):
        # See Qt documentation for method documentation
        data = index.data(Qt.EditRole)
        for combo_index in range(editor.count()):
            if data == editor.itemData(combo_index):
                editor.setCurrentIndex(combo_index)
                return
        raise ValueError("Data not in combo box: %s" % data) 
[docs]    def setModelData(self, editor, model, index):
        # See Qt documentation for method documentation
        combo_index = editor.currentIndex()
        data = editor.itemData(combo_index)
        model.setData(index, data) 
    def _popUpClosed(self, editor):
        """
        Respond to the combo box pop up closing by closing the editor.
        :param editor: The combo box that was closed.
        :type editor: `_TableComboBox`
        """
        self.commitData.emit(editor)
        self.closeEditor.emit(editor, self.NoHint) 
[docs]class MatplotlibDelegate(QtWidgets.QStyledItemDelegate):
    """
    A delegate that displays a matplotlib plot for all data of type
    PLOT_DATA_TYPE. Data of other types will be displayed normally (i.e. as a
    string). A small plot (generated in `_genCellPlot`) will be shown inside of
    the cell and a larger plot (generated in `_genToolTipPlot`) will be shown
    in a tool-tip-style pop up if the user hovers over a cell.  Data for the
    small plot is retrieved using Qt.UserRole.  Data for the tool tip plot is
    retrieved using Qt.ToolTipRole.
    This class should not be instantiated directly and must be subclassed.
    Subclasses must redefine `_genCellPlot` and may also redefine
    `_genToolTipPlot` and the class variables.
    :cvar TOOLTIP_PLOT: Whether the matplotlib tool tip pop up should be used.
        If this is False, all of the other TOOLTIP_PLOT_* constants will be ignored,
        as will `_genToolTipPlot`.
    :vartype TOOLTIP_PLOT: bool
    :cvar TOOLTIP_PLOT_WIDTH: The width of the plots shown in the tool tip
        popup
    :vartype TOOLTIP_PLOT_WIDTH: int
    :cvar TOOLTIP_PLOT_HEIGHT: The height of the plots shown in the tool tip
        popup
    :vartype TOOLTIP_PLOT_HEIGHT: int
    :cvar TOOLTIP_FRAME_WIDTH: The thickness of the border around the tool tip
        plot popup
    :vartype TOOLTIP_FRAME_WIDTH: int
    :cvar PLOT_DATA_TYPE: Data of this type (or tuple of types) will result in a
        matplotlib plot.  Other data types will result in standard table behavior.
    :vartype PLOT_DATA_TYPE: type or tuple
    """
    TOOLTIP_PLOT = True
    TOOLTIP_PLOT_WIDTH = 640
    TOOLTIP_PLOT_HEIGHT = 480
    TOOLTIP_FRAME_WIDTH = 1
    PLOT_DATA_TYPE = (list, tuple)
[docs]    def __init__(self, parent=None):
        super(MatplotlibDelegate, self).__init__(parent)
        self._popup = _ToolTipPlotHolder(parent, self.TOOLTIP_PLOT_WIDTH,
                                         self.TOOLTIP_PLOT_HEIGHT,
                                         self.TOOLTIP_FRAME_WIDTH)
        self._popup_index = None 
[docs]    def createEditor(self, parent, option, index):
        """
        This delegate does not have a separate editor widget, so we return None.
        All arguments are ignored, but are present for Qt compatibility.
        """
        return None 
[docs]    def paint(self, painter, option, index):
        """
        Paint the cell contents
        :param painter: The painter being used to render the delegate
        :type painter: QPainter
        :param option: The style options to use when painting
        :type option: QStyleOptionViewItem
        :param index: The index being represented
        :type index: `PyQt5.QtCore.QModelIndex`
        """
        data = index.data()
        if isinstance(data, self.PLOT_DATA_TYPE):
            self._paintBlank(painter, option)
            self._paintPlot(painter, option, data)
        else:
            super(MatplotlibDelegate, self).paint(painter, option, index) 
    def _paintBlank(self, painter, option):
        """
        Paint a blank cell so that the selection background and the current
        selected cell outline (i.e. dotted line) get painted.
        :param painter: The painter to use for painting
        :type painter: `PyQt5.QtGui.QPainter`
        :param option: The options to use for painting
        :type option: `PyQt5.QtWidgets.QStyleOptionViewItem`
        """
        blank_index = QtCore.QModelIndex()
        super(MatplotlibDelegate, self).paint(painter, option, blank_index)
    def _paintPlot(self, painter, option, data):
        """
        Paint the matplotlib plot into a cell
        :param painter: The painter to use for painting
        :type painter: `PyQt5.QtGui.QPainter`
        :param option: The options to use for painting
        :type option: `PyQt5.QtWidgets.QStyleOptionViewItem`
        :param data: The data to plot
        :type data: PLOT_DATA_TYPE
        """
        canvas = self._genCellPlot(data)
        # resizing the canvas won't work unless the canvas is shown, so we
        # resize the figure instead
        size = option.rect.size()
        self._setFigureSize(canvas, size)
        pixmap = QtGui.QPixmap(size)
        pixmap.fill(QtGui.QColor(0, 0, 0, 0))
        # Save figure in buffer, then read it in pixmap
        buff = io.BytesIO()
        canvas.print_figure(buff)
        pixmap.loadFromData(buff.getbuffer())
        painter.drawPixmap(option.rect, pixmap)
    def _setFigureSize(self, canvas, size):
        """
        Set the figure size to fill the rect
        :param canvas: the canvas
        :type canvas: `schrodinger.mpl_backend_agg.FigureCanvasQTAgg`
        :param size: the required size of the figure
        :type size: QtCore.QSize
        """
        figure = canvas.figure
        width, height = size.width(), size.height()
        dpi = figure.dpi
        figure.set_size_inches(width / dpi, height / dpi, True)
    def _genCellPlot(self, data):
        """
        Generate the matplotlib plot to paint inside of the cell.  This function
        must be defined in subclasses.
        :param data: The data to plot
        :type data: PLOT_DATA_TYPE
        :return: The canvas to paint
        :rtype: `schrodinger.mpl_backend_agg.FigureCanvasQTAgg`
        """
        raise NotImplementedError
    def _genToolTipPlot(self, data):
        """
        Generate the matplotlib plot to paint inside of the tool tip pop up.  If
        this function is not defined in a subclass, `genCellPlot` will be used.
        :param data: The data to plot
        :type data: PLOT_DATA_TYPE
        :return: The canvas to paint
        :rtype: `schrodinger.mpl_backend_agg.FigureCanvasQTAgg`
        """
        return self._genCellPlot(data)
[docs]    def helpEvent(self, event, view, option, index):
        """
        Handle help events.  If the index has data of type self.PLOT_DATA_TYPE
        for Qt.ToolTipRole, a pop up plot will be generated.  All other events
        will be handled as usual.  Before generating a pop up, make sure that a
        pop up for the given index isn't already displayed.  If it is, don't do
        anything.
        :param event: The event to be handled
        :type event: `QtGui.QHelpEvent`
        :param view: The view that this delegate is used in
        :type view: `QtWidgets.QTableView`
        :param option: Ignored, but present for Qt compatibility.
        :param index: The index of the cell where the event occurred
        :type index: `QtCore.QModelIndex`
        :return: True if the event was handled.  False otherwise.
        :rtype: bool
        """
        data = index.data(Qt.ToolTipRole)
        if self.TOOLTIP_PLOT and isinstance(data, self.PLOT_DATA_TYPE):
            if (not self._popup.isVisible() or
                (index.row(), index.column()) != self._popup_index):
                self._popup_index = (index.row(), index.column())
                # If do self._popup_index = index here, then occasionally
                # self._popup_index will be modified before the next time this
                # function is called.  Presumably, something is happening C++
                # that modifies the index in place after this function returns.
                # We get around this by storing (row, column) instead of a
                # reference to the passed QModelIndex object.
                canvas = self._genToolTipPlot(data)
                pos = event.globalPos()
                cell_rect = view.visualRect(index)
                top_left = cell_rect.topLeft()
                top_left = view.mapToGlobal(top_left)
                cell_rect.moveTopLeft(top_left)
                self._popup.showPlot(canvas, pos, cell_rect)
            return True
        return super(MatplotlibDelegate,
                     self).helpEvent(event, view, option, index)  
class _ToolTipPlotHolder(QtWidgets.QFrame):
    """
    A widget to hold the matplotlib tool tip plot generated in
    `MatplotlibDelegate`.  The implementation of this class is based loosely on
    Qt's QTipLabel implementation.
    """
    def __init__(self, parent, plot_width, plot_height, frame_width=1):
        """
        Create a plot holder, but do not display it
        :param parent: The Qt parent widget
        :type parent: `QtWidgets.QWidget`
        :param plot_width: The width to display the plot, in pixels
        :type plot_width: int
        :param plot_height: The height to display the plot, in pixels
        :type plot_height: int
        :param frame_width: The width of the frame around the plot, in pixels
        :type frame_width: int
        """
        super(_ToolTipPlotHolder, self).__init__(parent, Qt.ToolTip)
        self._plot_width = plot_width
        self._plot_height = plot_height
        self._frame_width = frame_width
        self._widget_width = plot_width + 2 * frame_width
        self._widget_height = plot_height + 2 * frame_width
        self.setFocusPolicy(Qt.NoFocus)
        self.setAttribute(Qt.WA_TransparentForMouseEvents, True)
        self.setLineWidth(self._frame_width)
        self.setFrameShape(self.Box)
        self.setFrameShadow(self.Plain)
        self._canvas = None
        self._rect = None
        self._hide_timer = QtCore.QTimer(self)
        self._hide_timer.setInterval(300)
        self._hide_timer.setSingleShot(True)
        self._hide_timer.timeout.connect(self.hide)
        self.setGeometry(0, 0, self._widget_width, self._widget_height)
    def _clearPlot(self):
        """
        If there is a plot loaded into this widget, delete it
        """
        if self._canvas is not None:
            self._canvas.setParent(None)
            self._canvas = None
    def eventFilter(self, qapp, event):
        """
        Check all QApplication events to figure out when to hide the pop up
        :param qapp: The QApplication instance being filtered.  Ignored, but
            present for Qt compatibility.
        :type qapp: `QtWidgets.QApplication`
        :param event: The event to check
        :type event: `QtCore.QEvent`
        :return: Whether the event have been handled and should no longer be
            propagated.  Since we don't actually want to filter out any events,
            False is always returned.
        :rtype: bool
        """
        if event.type() == event.MouseMove:
            if not self._rect.contains(event.globalPos()):
                self.delayedHide()
        elif event.type() in (event.WindowActivate, event.WindowDeactivate,
                              event.MouseButtonPress, event.MouseButtonRelease,
                              event.MouseButtonDblClick, event.FocusIn,
                              event.FocusOut, event.Wheel):
            self.hide()
        return False
    def showPlot(self, canvas, pos, rect):
        """
        Show the given plot in a pop up at the specified point
        :param canvas: The plot to display
        :type canvas: `schrodinger.mpl_backend_agg.FigureCanvasQTAgg`
        :param pos: The current mouse cursor location.  Must use global
            coordinates.
        :type pos: `QtCore.QPoint`
        :param rect: The rectangle containing the table cell that the pop up is
            being displayed for.  The pop up will be hidden when the mouse cursor
            leaves this rectangle.  Must use global coordinates.
        :type rect: `QtCore.QRect`
        """
        QtWidgets.QToolTip.hideText()  # Clear any existing tool tip
        self._hide_timer.stop()
        self._clearPlot()
        canvas.setParent(self)
        canvas.show()
        self._canvas = canvas
        self._rect = rect
        canvas.setGeometry(self._frame_width, self._frame_width,
                           self._plot_width, self._plot_height)
        plot_pos = self._calcPlotLocation(pos)
        self.move(plot_pos)
        self.show()
        qapp = QtWidgets.QApplication.instance()
        qapp.installEventFilter(self)
    def delayedHide(self):
        """
        Hide the pop up after a short delay.  This mimics the standard Qt tool
        tip delay.
        """
        if not self._hide_timer.isActive():
            self._hide_timer.start()
    def hideEvent(self, event):
        """
        Clean up when hiding the pop up.
        :param event: The event that triggered the hide
        :type event: `QtCore.QEvent`
        """
        self._hide_timer.stop()
        self._clearPlot()
        qapp = QtWidgets.QApplication.instance()
        qapp.removeEventFilter(self)
        self._rect = None
        super(_ToolTipPlotHolder, self).hideEvent(event)
    def _calcPlotLocation(self, pos):
        """
        Determine where to place the tool tip plot.  Make sure that it is fully
        visible and that it's located on a single monitor.
        :param pos: The mouse location
        :type pos: `QtCore.QPoint`
        :return: The location for the upper-left corner of the tool tip plot
        :rtype: `QtCore.QPoint`
        :note: This code is based on Qt's tool tip placement
            (src/gui/kernel/qtooltip.cpp QTipLabel::placeTip), and the constants
            used below are taken from that code.  Unlike placeTip, this function
            does not take the OS X dock into account.
        """
        desktop = QtWidgets.QApplication.desktop()
        tip_screen = desktop.screenNumber(pos)
        screen_geom = desktop.screenGeometry(tip_screen)
        if os.name == "nt":
            offset = QtCore.QPoint(2, 21)
        else:
            offset = QtCore.QPoint(2, 16)
        new_pos = pos + offset
        screen_left = screen_geom.x()
        screen_top = screen_geom.y()
        # QRect.right() and QRect.bottom() are off by one for
        # historical reasons, so we calculate the real values here
        screen_right = screen_geom.x() + screen_geom.width()
        screen_bottom = screen_geom.y() + screen_geom.height()
        if new_pos.x() + self._widget_width > screen_right:
            new_x = new_pos.x() - 4 - self._widget_width
            new_pos.setX(new_x)
        if new_pos.x() < screen_left:
            new_pos.setX(screen_left)
        if new_pos.y() + self._widget_height > screen_bottom:
            new_y = new_pos.y() - 24 - self._widget_height
            new_pos.setY(new_y)
        if new_pos.y() < screen_top:
            new_pos.setY(screen_top)
        return new_pos
[docs]class ModelIndexFilter(QtCore.QModelIndex):
    """
    An index that can override the data provided by a model.
    :note: Qt's QModelIndex functions are all inlined.  As a result, any
        functions implemented in a QModelIndex subclass are bypassed when called
        from C++.  Because of this, we have to implement this class using a real
        index to a dummy model (`_DummyModel`) rather than using a dummy index with
        its own data() method.
    """
    def __new__(cls, data, index):
        """
        :param data: The data to provide, as a dictionary of {role: data}
        :type data: dict
        :param index: An index to query for data roles not present in `data`
        :type index: `QtCore.QModelIndex`
        """
        model = _DummyModel(data, index)
        index = model.index(0)
        index._model = model
        return index 
class _DummyModel(QtCore.QAbstractListModel):
    """
    A dummy model used in `ModelIndexFilter`
    """
    def __init__(self, data, index):
        """
        See documentation for `ModelIndexFilter.__new__` above.
        """
        super(_DummyModel, self).__init__()
        self._data = data
        self._index = index
    def rowCount(self, parent=None):
        return 1
    def data(self, index, role=Qt.DisplayRole):
        """
        If we have data for the requested role in `self._data`, return that.
        Otherwise, return data from `self._index`.
        See Qt documentation for an explanation of arguments and return value.
        """
        if role in self._data:
            return self._data[role]
        else:
            return self._index.data(role)
[docs]class DefaultMessageDelegate(QtWidgets.QStyledItemDelegate):
    """
    A delegate that displays a default "Double-click to edit" message when there
    is no data to display.  This way, the model and the editor don't have to
    treat no-data as a special case.  Note that the model background color will
    still be used.
    """
    FONT = QtGui.QFont()
    FONT.setItalic(True)
    FOREGROUND = QtGui.QBrush(Qt.lightGray)
[docs]    def __init__(self, parent=None, message="Double-click to edit..."):
        super(DefaultMessageDelegate, self).__init__(parent=parent)
        self.data = {
            Qt.DisplayRole: message,
            Qt.FontRole: self.FONT,
            Qt.ForegroundRole: self.FOREGROUND
        } 
[docs]    def paint(self, painter, option, index):
        """
        If the DisplayRole data for index is empty, paint the default message
        instead.
        See Qt documentation for an explanation of arguments.
        """
        display_data = index.data()
        if not display_data:
            index = ModelIndexFilter(self.data, index)
        QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)  
[docs]class AbstractCustomDelegate(QtWidgets.QStyledItemDelegate):
[docs]    def paintItemBackground(self, painter, option, index):
        """
        Paints the item's background.
        :param painter: The painter to use
        :type painter: `QtGui.QPainter`
        :param option: The options for the cell
        :type option: `QtWidgets.QStyleOptionViewItem`
        :param index: The index to paint
        :type index: `QtCore.QModelIndex`
        :return: Instance of `QtWidgets.QStyleOptionViewItem` initialized using
                 original option and an instance of `QStyle`
        :rtype: tuple of `QtWidgets.QStyleOptionViewItem`, `QStyle`
        """
        opt = QtWidgets.QStyleOptionViewItem(option)
        self.initStyleOption(opt, index)
        widget = opt.widget
        if widget is not None:
            style = widget.style()
        else:
            style = QtWidgets.QApplication.style()
        style.drawPrimitive(style.PE_PanelItemViewItem, opt, painter, widget)
        return opt, style  
[docs]class WorkspaceInclusionDelegate(AbstractCustomDelegate):
    """
    A delegate for representing workspace inclusion as it is shown in the
    project table.  This delegate allows an entry to be set as the only
    workspace entry by clicking, or allows workspace inclusion to be toggled by
    holding down Ctrl.
    NOTE: If using schrodinger.ui.qt.delegates.WorkspaceInclusionDelegate, this
    delegate expects the model to provide the number of included entries for
    NUM_INCLUDED_ENTRIES_ROLE. If this data is not provided, a black inclusion
    icon will be used for any number of inclusions, including 1.
    """
[docs]    def __init__(self, parent):
        super(WorkspaceInclusionDelegate, self).__init__(parent)
        self._black_icon = QtGui.QIcon()
        self._black_icon.addFile(":/icons/include_checked_maestrox.png",
                                 state=QtGui.QIcon.On)
        self._black_icon.addFile(":/icons/include_unchecked_maestrox.png",
                                 state=QtGui.QIcon.Off)
        self._red_icon = QtGui.QIcon()
        self._red_icon.addFile(":/icons/include_red_checked_maestrox.png",
                               state=QtGui.QIcon.On)
        self._red_icon.addFile(":/icons/include_unchecked_maestrox.png",
                               state=QtGui.QIcon.Off) 
[docs]    def createEditor(self, parent, option, index):
        """
        This delegate does not have a separate editor widget, so we return None.
        All arguments are ignored, but are present for Qt compatibility.
        """
        return None 
[docs]    def paint(self, painter, option, index):
        """
        Paint the appropriate icon
        :param painter: The painter being used to render the delegate
        :type painter: QPainter
        :param option: The style options to use when painting
        :type option: QStyleOptionViewItem
        :param index: The index being represented
        :type index: `PyQt5.QtCore.QModelIndex`
        """
        self.paintItemBackground(painter, option, index)
        if index.flags() & Qt.ItemIsEnabled:
            mode = QtGui.QIcon.Active
        else:
            mode = QtGui.QIcon.Disabled
        enabled = index.data()
        state = QtGui.QIcon.On if enabled else QtGui.QIcon.Off
        num_included = index.data(NUM_INCLUDED_ENTRIES_ROLE)
        if num_included is None or num_included > 1:
            icon = self._black_icon
        else:
            icon = self._red_icon
        align = Qt.AlignCenter
        icon.paint(painter, option.rect, align, mode, state) 
[docs]    def editorEvent(self, event, model, option, index):
        """
        Handle mouse clicks and key presses
        :param event: The event that occurred
        :type event: `PyQt5.QtCore.QEvent`
        :param model: The data model
        :type model: QAbstractTableModel
        :param option: Ignored, but present for Qt compatibility
        :param index: The index being edited
        :type index: `PyQt5.QtCore.QModelIndex`
        :return: True if the event was handled.  False otherwise.
        :rtype: bool
        """
        # We could limit the mouse click by making sure that it's inside the
        # check box, but the project table accepts clicks anywhere in the cell,
        # so we don't bother to limit things that way.
        mouse_click = (event.type() == event.MouseButtonPress and
                       event.button() == Qt.LeftButton)
        key_press = (event.type() == event.KeyPress and
                     event.key() in (Qt.Key_Space, Qt.Key_Select))
        ctrl = event.modifiers() & Qt.ControlModifier
        if mouse_click or key_press:
            if not ctrl:
                enabled = WS_INCLUDE_ONLY
            elif index.data():
                enabled = WS_REMOVE
            else:
                enabled = WS_INCLUDE
            model.setData(index, enabled)
            return True
        else:
            return False  
[docs]class PictureDelegate(AbstractCustomDelegate):
    """
    An abstract delegate class that can render images along with text.
    This delegate queries the table's `Qt.DisplayRole` for data that informs
    how it should render an image. This data must be an instance of
    `DISPLAY_DATA_CLASS`. If not, this delegate will revert to default delegate
    behavior.
    This delegate also queries the custom `PICTURE_TEXT_ROLE`
    role for text to display along with the generated image. The text color can
    be set if the table returns the appropriate `QtGui.QBrush` when
    `Qt.ForegroundRole` is queried.
    Images should be cached for faster rendering after the first render. They
    are stored in a cache that can be cleared via the `clearImageCache` method
    when the model's data changes, if necessary.
    Subclasses should define the following:
        -DISPLAY_DATA_CLASS: class variable that indicates the expected type of
            the display data to be received from the table.
        -_getPicture(): method that actually generates the picture to be
            rendered. Should only be accessed via the `getPicture()` method
    :ivar padding_factor: The portion of a cell's width and height to be used as
            padding on each side of the cell when drawing the image. Default value
            0.04.
    """
    TEXT_MARGIN = 2
    DISPLAY_DATA_CLASS = None
[docs]    def __init__(self, parent=None, padding_factor=0.04, alignment=None):
        """
        Initialize a new delegate instance, optionally specifying the size of
        the padding in the cell around the image.
        :param padding_factor: The portion of the cell's width and height to be
                used as padding on each side of the cell when drawing the image.
        :type padding_factor: `float`
        :param alignment: The alignment of the text, if any text is provided by
                the cell data method. Vertical alignment specifies whether the text
                is displayed above or below the structure image.
        :type alignment: `QtCore.QAlignment`
        """
        super(PictureDelegate, self).__init__(parent)
        # Using lru_cache() prevents _getCachedPicture() from being called if
        # its return value for the provided arguments is already stored in the
        # cache. It also adds the following functions to this method:
        # cache_info() and cache_clear(). Note that we avoid using lru_cache()
        # as a decorator, as that will store the cache data on the class.
        # Instead, by applying it during __init__(), the cache data is stored on
        # the instance.
        self._getCachedPicture = lru_cache(maxsize=1024)(self._getCachedPicture)
        self.setPaddingFactor(padding_factor)
        if alignment is None:
            alignment = Qt.AlignLeft | Qt.AlignTop
        self.setAlignment(alignment)
        self.setTextElide(Qt.ElideRight)
        self.adaptor = canvas2d.ChmMmctAdaptor()
        model2d = canvas2d.ChmRender2DModel()
        self.renderer = canvas2d.Chm2DRenderer(model2d) 
[docs]    def setAlignment(self, alignment):
        """
        Set the text alignment for the cell. The vertical component of the
        alignment indicates whether the text will be displayed above or below
        the structure image.
        The vertical component of the alignment must either be `Qt.AlignTop`
        or `Qt.AlignBottom`; if it is neither of these, the vertical alignment
        will default to `Qt.AlignTop`.
        If this delegate is not provided with both a structure and text to
        display, this alignment setting is ignored.
        :param alignment: Text alignment in table cell.
        :type alignment: `QtCore.Alignment`
        :raise ValueError: if the vertical component of the alignment is
                specified to be Qt.AlignVCenter
        """
        if alignment & Qt.AlignVCenter:
            raise ValueError('Vertical alignment must either be Qt.AlignTop or'
                             ' Qt.AlignBottom, not Qt.AlignVCenter.')
        # If no vertical alignment is specified, default to Qt.AlignTop
        if not alignment & Qt.AlignTop and not alignment & Qt.AlignBottom:
            alignment |= Qt.AlignTop
        self._alignment = alignment 
[docs]    def alignment(self):
        return self._alignment 
[docs]    def setTextElide(self, elide):
        """
        Set the text elide status of the text displayed by the delegate. The
        elide mode describes where the ellipsis should appear when displaying
        text that doesn't fit into the available space.
        :param elide: text elide mode
        :type elide: `Qt.TextElideMode`
        """
        self._elide = elide 
[docs]    def textElide(self):
        return self._elide 
[docs]    def setPaddingFactor(self, padding_factor):
        """
        Set the relative size of the cell width and height used as padding for
        the image.
        If the table view has already been painted, `view.update` should be
        called after `setPaddingFactor` to make sure that the visible cells are
        properly updated.
        :raise TypeError: when the supplied `padding_factor` is not a float
        :raise ValueError: when the supplied `padding_factor` is less than 0.0
                or greater than 0.5.
        :param padding_factor: The portion of the cell's width and height to be
                used as padding on each side of the cell when drawing the image.
        :type padding_factor: `float`
        """
        error_msg = 'Image padding factor must be a float between 0 and 0.5.'
        if not isinstance(padding_factor, float) and padding_factor != 0:
            raise TypeError(error_msg)
        if padding_factor < 0.0 or padding_factor > 0.5:
            raise ValueError(error_msg)
        self.padding_factor = padding_factor 
[docs]    def paddingFactor(self):
        return self.padding_factor 
[docs]    def paint(self, painter, option, index):
        """
        Arrange and paint image, and optionally text, into the table cell.
        See `QtWidgets.QAbstractItemDelegate.paint` for argument documentation.
        """
        # Parse data from the table model
        display_data = index.data(Qt.DisplayRole)
        if not isinstance(display_data, self.DISPLAY_DATA_CLASS):
            # Unexpected input; revert to default delegate behavior
            super(PictureDelegate, self).paint(painter, option, index)
            return
        # Paint background (so that if this cell is selected it is highlighted)
        self.paintItemBackground(painter, option, index)
        # Obtain the rectangle in which the painting will take place
        rect = self.getContentRect(option.rect)
        # Draw text into cell, if any has been provided
        text_rect = self.paintText(painter, option, index, rect)
        # Prepare sub-rectangle to paint image into; depending on vertical text
        # alignment, place image above or below text
        image_rect = QtCore.QRect(rect)
        if text_rect:
            if self._alignment & Qt.AlignTop:
                image_rect.setTop(text_rect.bottom() + self.TEXT_MARGIN)
            elif self._alignment & Qt.AlignBottom:
                image_rect.setBottom(text_rect.top() - self.TEXT_MARGIN)
        # Paint image into table cell
        self.paintPicture(display_data, painter, image_rect) 
[docs]    def paintText(self, painter, option, index, rect):
        """
        Paint supplied text into a subsection of the supplied rectangle.
        :param painter: The painter that is drawing in the table
        :type painter: `QtGui.QPainter`
        :param option: Class containing parameters used to draw the cell
        :type option: `QtGui.QStyleOptionViewItem`
        :param index: Index for the cell to be painted
        :type index: `QtCore.QModelIndex`
        :param rect: The rectangle in which the text should be painted
        :type rect: `QtCore.QRect`
        :return: If text is drawn, a rectangle contained entirely within the
                larger painting area that contains the painted text, or `None` if
                no text was provided.
        :rtype: `QtCore.QRect` or `None`
        """
        text = index.data(role=PICTURE_TEXT_ROLE)
        if not text:
            return
        # Make a copy of the option object (so that the original is not changed
        # when it is used elsewhere in the delegate logic)
        option = QtWidgets.QStyleOptionViewItem(option)
        self.initStyleOption(option, index)
        # Apply formatting to painter before painting text
        painter.setFont(option.font)
        palette = option.palette
        if option.state & QtWidgets.QStyle.State_Selected:
            painter.setPen(
                palette.color(palette.Normal, palette.HighlightedText))
        else:
            painter.setPen(palette.color(palette.Normal, palette.Text))
        brush = index.data(role=Qt.ForegroundRole)
        if brush:
            painter.setBrush(brush)
        # Obtain the sub-rectangle within rect that will contain the text
        qdata = option.fontMetrics.elidedText(text, self._elide, rect.width())
        text_rect = painter.boundingRect(rect, self._alignment, qdata)
        # Intersect text_rect with rect: text_rect must be contained within rect
        text_rect &= rect
        painter.drawText(text_rect, self._alignment, qdata)
        return text_rect 
[docs]    def paintPicture(self, display_data, painter, rect):
        """
        Paint image into a subsection of the supplied rectangle.
        :param display_data: object used to generate the image
        :type display_data: `DISPLAY_DATA_CLASS`
        :param painter: The painter that is drawing in the table
        :type painter: `QtGui.QPainter`
        :param rect: The rectangle in which the image should be painted
        :type rect: `QtCore.QRect`
        """
        # Skip painting image if there is no room
        if rect.height() <= 1:
            return
        pic = self.getPicture(display_data)
        swidgets.draw_picture_into_rect(painter, pic, rect) 
    def _getCacheableArgs(self, *args, **kwargs):
        """
        Given the display data returned by the table, produce appropriate
        arguments for `getPicture` and return them. By default, simply returns
        a tuple containing only `display_data`, but can be overridden for
        subclasses if necessary.
        :return: arguments necessary for calling `getPicture()`
        """
        raise NotImplementedError
[docs]    def getPicture(self, *args, **kwargs) -> QtGui.QPicture:
        """
        Outward-facing API for accessing a picture.
        This method should not be overridden in subclasses; instead,
        `_getCacheableArgs()` and `_getPicture()` should be.
        """
        cacheable_args = self._getCacheableArgs(*args, **kwargs)
        return self._getCachedPicture(*cacheable_args) 
    def _getCachedPicture(self, *cacheable_args):
        """
        Return the `QtGui.QPicture` instance to be displayed by the delegate.
        A simple wrapper for the `_getPicture` method that uses the
        `functools.lru_cache` to improve performance. As such, any arguments
        for this method (and therefore also `_getPicture`) should be hashable
        and should uniquely identify the desired picture.
        Note that this method has an `lru_cache` decorator applied to it during
        `__init__()` so that the cache data is stored on the instance rather
        than the class.
        :return: the picture to be displayed by this delegate
        :rtype: `QtGui.QPicture`
        """
        return self._getPicture(*cacheable_args)
    def _getPicture(self, *cacheable_args):
        """
        Return the `QtGui.QPicture` instance to be displayed by the delegate.
        Must be implemented in a subclass. Should only be accessed via the
        `getPicture` method, which is decorated with `functools.lru_cache`.
        See `getPicture` documentation for restrictions on the arguments that
        this method can accept.
        :return: the picture to be displayed by this delegate
        :rtype: `QtGui.QPicture`
        """
        raise NotImplementedError
[docs]    def getContentRect(self, rect):
        """
        Given the rectangle representing the total area in the table cell,
        return a (potentially) smaller rectangle that remains after the portion
        of the rectangle dedicated to padding is removed.
        :param rect: the rectangle corresponding to the full table cell area
        :type rect: `QtCore.QRect`
        :return: the rectangle corresponding to the subset of the table area
                devoted to the cell contents (full cell minus padding area, if any)
        :rtype: `QtCore.QRect`
        """
        pad_width = rect.width() * self.padding_factor
        pad_height = rect.height() * self.padding_factor
        return rect.adjusted(pad_width, pad_height, -pad_width, -pad_height) 
[docs]    def clearImageCache(self):
        """
        Clear the image cache. This function can be called from a subclass if
        the `functools.lru_cache` decorator is applied to `getPicture`.
        """
        self._getCachedPicture.cache_clear()  
[docs]class StructureDelegate(PictureDelegate):
    """
    Delegate used to display a 2D image of a small molecule.
    """
    DISPLAY_DATA_CLASS = structure.Structure
    def _getCacheableArgs(self, st):
        """
        Given a structure, produce arguments necessary to pass to `getPicture`
        to produce a cached picture. This must be done because the
        `structure.Structure` object is not hashable, which is a necessary
        requirement to use the `functools.lru_cache` decorator.
        :param st: the structure used to generate a picture
        :type st: `structure.Structure`
        :return: a tuple containing
                1. the structure handle
                2. a second tuple with the structure's Cartesian coordinate
                information
        :rtype: 2-`tuple` of (`int`, `tuple`)
        """
        st_tuple = structure_tuple(st)
        smiles = analyze.generate_smiles(st)
        return st.handle, st_tuple, smiles
    def _getPicture(self, st_handle, st_tuple, smiles):
        """
        Returns a 2D picture of the chemical structure with handle `st_handle`.
        :param st_handle: handle of the structure to be rendered.
        :type st_handle: `int`
        :param st_tuple: a `tuple` of two tuples describing a structure: The
                         first contains the atom types. The second contains the
                         Cartesian coordinates of the atoms. This argument is
                         ignored within the body of the function, and is used
                         to uniquely identify different structures when using
                         the `functools.lru_cache`
        :type st_tuple: A 2-`tuple` containing (`N`-tuple of `str`, `3*N`-tuple
                        of `float`)
        :param smiles: SMILES string generate from the structure. It is
                       need to distinguish two structures that have
                       different stereochemistry.
        :type smiles: str
        :return: A 2D image of the supplied chemical structure
        :rtype: `QtGui.QPicture`
        """
        stereo = canvas2d.ChmMmctAdaptor.StereoFromAnnotationAndGeometry_Safe
        chmmol = self.adaptor.create(st_handle, stereo)
        # On failure, may return a QPicture containing the text "Failed to
        # render". May also raise an exception.
        return structure2d.get_qpicture_protected(self.renderer, chmmol) 
[docs]class HashableRdMol:
    """
    A `rdchem.Mol` wrapper that extracts data from the `rdchem.Mol` object and
    uses it to form a useful hash value. This is necessary to ensure that a
    `rdchem.Mol` instance is not accidentally matched with a totally different
    `rdchem.Mol` because they happen to share the same memory location (at
    different times).
    """
[docs]    def __init__(self,
                 rdmol: Mol,
                 atom_idcs: Optional[Tuple[int]] = None,
                 color: Optional[Tuple[int]] = None,
                 atom_labels: Optional[List[str]] = None):
        """
        :note: as of this writing, sketcher does not appear to support rendering
                both custom atom coloring and atom labels.
        :param rdmol: the molecule for which the 2D image will be rendered
        :param atom_idcs: optionally, atom indices for `rdmol` which should be
                colored. Will be ignored if `color` is not specified.
        :param color: a (red, green, blue) tuple describing the color of the
                atoms (optionally) specified by `atom_idcs`. Will be ignored if
                `atom_idcs` is not specified.
        :param atom_labels: optionally, text labels to annotate each atom. The
                labeled atom is specified by the index of the label within the
                list, so we must have `len(atom_labels) == rdmol.GetNumAtoms()`
        """
        self.rdmol = rdmol
        self.atom_idcs = None
        self.color = None
        if atom_idcs and color:
            self.atom_idcs = tuple(atom_idcs)
            self.color = color
        self.atom_labels = atom_labels
        self._smiles = Chem.MolToSmiles(rdmol) 
    def __hash__(self):
        """
        Using SMILES to distinguish between different molecules.
        Note: But SMILES doesn't distinguish between different conformers of the
        same molecule.
        """
        return hash(
            (self._smiles, self.atom_idcs, self.color, self.atom_labels))
    def __eq__(self, other):
        if not isinstance(other, HashableRdMol):
            return False
        return hash(self) == hash(other) 
[docs]class RdMolDelegate(PictureDelegate):
    """
    Delegate that uses RDKit and sketcher to display a 2D image of a small
    molecule.
    Input `Mol` objects can use implicit or explicit hydrogens.
    The delegate will attempt to align the input structures against a template
    (arbitrarily chosen as the first `Mol` instance encountered by the
    delegate after reset). In order for this to work, the input structures must
    have some parts in common. Consequently, if the delegate is first used on
    a group of similar molecules "A", it must be cleared prior to being used on
    a second group of molecules "B" that may not be similar to the first.
    Otherwise, it will attempt to align all group B molecules against a template
    chosen from group A, which may not produce good results. The `clear()`
    method can be used to accomplish this.
    """
    DISPLAY_DATA_CLASS = Mol
[docs]    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.template_rdmol = None 
[docs]    def clear(self):
        """
        Clear cached data, including the image cache and template `Mol`.
        """
        self.template_rdmol = None
        self.clearImageCache() 
    def _getCacheableArgs(
            self,
            rdmol: Mol,
            atom_idcs: Optional[Tuple[int]] = None,
            color: Optional[Tuple[int]] = None,
            atom_labels: Optional[List[str]] = None) -> HashableRdMol:
        """
        Transform a RDKit molecule object into a more hash-friendly object.
        While the `rdmol` alone is necessary for generating the image for this
        delegate, the `HashableRdMol` object ensures that changes to the `rdmol`
        are reflected in its hash value, preventing the `getPicture()` cache
        from returning outdated images if the `rdmol` has changed.
        :note: as of this writing, sketcher does not appear to support rendering
                both custom atom coloring and atom labels.
        :param rdmol: the molecule for which the 2D image will be rendered
        :param atom_idcs: optionally, atom indices for `rdmol` which should be
                colored. Will be ignored if `color` is not specified.
        :param color: a (red, green, blue) tuple describing the color of the
                atoms (optionally) specified by `atom_idcs`. Will be ignored if
                `atom_idcs` is not specified.
        :param atom_labels: optionally, text labels to annotate each atom. The
                labeled atom is specified by the index of the label within the
                list, so we must have `len(atom_labels) == rdmol.GetNumAtoms()`
        :return: a hashable object that stores all of the above data
        """
        return HashableRdMol(rdmol,
                             atom_idcs=atom_idcs,
                             color=color,
                             atom_labels=atom_labels),
    def _getPicture(self, hashable_rdmol):
        """
        Generate a 2D rendering for `hashable_rdmol` using the sketcher
        library. Molecules can have implicit or explicit hydrogens - hydrogen
        coordinates will be ignored. 2D coordinates will be generated with
        no explicit hydrogens, and hydrogens will be only rendered as “H”
        labels on heavy atoms.
        :param hashable_rdmol: hash-friendly RDKit molecule object
        :type hashable_rdmol: HashableRdMol
        :return: picture of the provided `HashableRdMol` object
        :rtype: QtGui.QPicture
        """
        # Sketcher requires explicit hydrogens (PYAPP-8490) but we need to
        # generate coordinates without hydrogens, otherwise they can
        # "get in the way" and cause suboptimal 2D coordinates (PYAPP-8522).
        # So we generate coordinates for heavy atoms only, then add hydrogens:
        mol_to_render = Chem.RemoveHs(hashable_rdmol.rdmol)
        Chem.rdCoordGen.AddCoords(mol_to_render)
        mol_to_render = Chem.AddHs(mol_to_render)
        # TODO: If maestrohub.get2DRenderSettings().drawAllHs is True,
        # generate coordinates *after* adding hydrogens.
        if self.template_rdmol is None:
            self.template_rdmol = Mol(mol_to_render)
            # Align the template horizontally and the rest will follow
            generate_min_height_coords(self.template_rdmol)
        # Align the current and the template mol.
        options = substructure.QueryOptions(tautomer_insensitive=True)
        substructure.apply_substructure_coordinates(
            mol=mol_to_render,
            template_mol=self.template_rdmol,
            options=options,
        )
        renderer = sketcher.Renderer()
        # Skip clean up to preserve alignment
        settings = sketcher.RendererSettings()
        settings.skipCleanUp = True
        renderer.loadSettings(settings)
        renderer.loadStructure(mol_to_render)
        color = QColor(*hashable_rdmol.color) if hashable_rdmol.color else None
        atom_idcs = list(
            hashable_rdmol.atom_idcs) if hashable_rdmol.atom_idcs else None
        if atom_idcs and color:
            renderer.colorAtoms(atom_idcs, color)
        if hashable_rdmol.atom_labels:
            renderer.labelAtoms(list(hashable_rdmol.atom_labels))
        return renderer.getPicture() 
[docs]class ChmMolDelegate(PictureDelegate):
    DISPLAY_DATA_CLASS = canvas2d.ChmMol
    def _getCacheableArgs(self, chmmol):
        """
        Transform a `canvas2d.ChmMol` object into a more hash-friendly object.
        While the `ChmMol` alone is necessary for generating the image for this
        delegate, the `HashableChmMol` object ensures that changes to the
        `ChmMol` are reflected in its hash value, preventing the `getPicture()`
        cache from returning outdated images if the `ChmMol` has changed
        (PANEL-13897).
        :param chmmol: a ChmMol object
        :type chmmol: canvas2d.ChmMol
        :return: a tuple containing a single hashable chmmol object
        :rtype: tuple(HashableChmMol)
        """
        return HashableChmMol(chmmol),
    def _getPicture(self, hashable_chmmol):
        """
        Returns a rendering of the `ChmMol` contained in the supplied
        `HashableChmMol` object. While only the `chmmol` attribute of the object
        is used to generate the image, the object's hash value reflects
        important changes to the `ChmMol` object to ensure that the cache does
        not make an incorrect match.
        :param hashable_chmmol: a hashable ChmMol
        :type hashable_chmmol: HashableChmMol
        :return: a rendering of the supplied chmmol instance
        :rtype: QtGui.QPicture
        """
        # On failure, may return a QPicture containing the text "Failed to
        # render". May also raise an exception.
        chmmol = hashable_chmmol.chmmol
        return structure2d.get_qpicture_protected(self.renderer, chmmol, False) 
[docs]class CheckboxDelegate(AbstractCustomDelegate):
    """
    This delegate contains a clickable checkbox.  The checkbox is only displayed
    if the index contains data for the Qt.CheckStateRole role.  If the
    Qt.CheckStateRole role is None, then the standard delegate behavior will be
    used.  The index must flagged as enabled and editable for clicks to toggle
    the model value, and clicks must occur on the checkbox itself.  Clicks on
    other areas of the cell will be handled as normal (i.e. selecting the cell).
    Model.setData() will be called with the default role (i.e. Qt.EditRole) when
    toggling the checkbox value.  Note that this delegate uses a bi-state rather
    than tri-state checkbox, so partially checked states are not allowed.
    """
    DRAW_STYLE = QtWidgets.QStyle.CE_CheckBox
[docs]    def __init__(self, parent=None):
        super().__init__(parent)
        self._cb = QtWidgets.QCheckBox() 
[docs]    def createEditor(self, parent, option, index):
        """
        This delegate does not have a separate editor widget, so we return None.
        All arguments are ignored, but are present for Qt compatibility.
        """
        return None 
[docs]    def paint(self, painter, option, index):
        """
        Paint the appropriate icon
        :param painter: The painter being used to render the delegate
        :type painter: `PyQt5.QtGui.QPainter`
        :param option: The style options to use when painting
        :type option: `PyQt5.QtWidgets.QStyleOptionViewItem`
        :param index: The index being represented
        :type index: `PyQt5.QtCore.QModelIndex`
        """
        check_data = self._getCheckData(index)
        if check_data is None:
            super(CheckboxDelegate, self).paint(painter, option, index)
            return
        self.paintItemBackground(painter, option, index)
        cb_style = self._getCbStyle(option, check_data, index.flags())
        app_style = QtWidgets.QApplication.style()
        app_style.drawControl(self.DRAW_STYLE, cb_style, painter) 
    def _getCheckData(self, index):
        """
        Get the Qt.CheckStateRole data from the specified index
        :param index: The index to get data from
        :type index: `PyQt5.QtCore.QModelIndex`
        :return: The checked data from `index`
        :rtype: bool or NoneType
        """
        check_data = index.data(Qt.CheckStateRole)
        return check_data
    def _getCbStyle(self, option, check_data, flags):
        """
        Create a `PyQt5.QtWidgets.QStyleOptionButton` with the appropriate
        options for the desired checkbox.
        :param option: The style options to use when painting
        :type option: `PyQt5.QtWidgets.QStyleOptionViewItem`
        :param check_data: The data to be displayed in the checkbox.
        :type check_data: bool
        :param flags: The flags indicating the editable and enabled state of the
            checkbox
        :type flags: `PyQt5.QtCore.Qt.ItemFlags`
        :return: The styled `PyQt5.QtWidgets.QStyleOptionButton`
        :rtype: `PyQt5.QtWidgets.QStyleOptionButton`
        """
        cb_style = QtWidgets.QStyleOptionButton()
        cb_style.initFrom(self._cb)
        cb_style.state |= QtWidgets.QStyle.State_Active
        if (flags & Qt.ItemIsEnabled):
            cb_style.state |= QtWidgets.QStyle.State_Enabled
        if not (flags & Qt.ItemIsEditable):
            cb_style.state |= QtWidgets.QStyle.State_ReadOnly
        if check_data:
            cb_style.state |= QtWidgets.QStyle.State_On
        else:
            cb_style.state |= QtWidgets.QStyle.State_Off
        cb_style.rect = self._getCbRect(option)
        return cb_style
    def _getCbRect(self, option):
        """
        Determine the rectangle for painting the checkbox
        :param option: The style options to use when painting
        :type option: `PyQt5.QtWidgets.QStyleOptionViewItem`
        :return: The rectangle to paint the checkbox into
        :rtype: `PyQt5.QtCore.QRect`
        """
        cell_rect = option.rect
        app_style = QtWidgets.QApplication.style()
        cb_rect = app_style.subElementRect(app_style.SE_CheckBoxIndicator,
                                           QtWidgets.QStyleOptionButton())
        cb_x = cell_rect.x() + old_div(cell_rect.width(), 2) - old_div(
            cb_rect.width(), 2)
        cb_y = cell_rect.y() + old_div(cell_rect.height(), 2) - old_div(
            cb_rect.height(), 2)
        cb_topleft = QtCore.QPoint(cb_x, cb_y)
        centered_cb_rect = QtCore.QRect(cb_topleft, cb_rect.size())
        return centered_cb_rect
[docs]    def editorEvent(self, event, model, option, index):
        """
        Handle mouse clicks and key presses.  Left clicking on the check box or
        hitting the space bar will toggle the check box value.  Left clicks
        outside of the checkbox will be handled as normal (i.e. selecting the
        cell).
        :param event: The event that occurred
        :type event: `PyQt5.QtCore.QEvent`
        :param model: The data model
        :type model: QAbstractTableModel
        :param option: The style options for the cell
        :type option: QStyleOptionViewItem
        :param index: The index being edited
        :type index: `PyQt5.QtCore.QModelIndex`
        :return: True if the event was handled.  False otherwise.
        :rtype: bool
        """
        check_data = self._getCheckData(index)
        enabled = index.flags() & Qt.ItemIsEnabled
        editable = index.flags() & Qt.ItemIsEditable
        if check_data is None or not (enabled and editable):
            return super(CheckboxDelegate,
                         self).editorEvent(event, model, option, index)
        mouse_click = (event.type() == event.MouseButtonPress and
                       event.button() == Qt.LeftButton)
        if mouse_click:
            cb_rect = self._getCbRect(option)
            click_loc = event.pos()
            mouse_click = cb_rect.contains(click_loc)
        key_press = (event.type() == event.KeyPress and
                     event.key() in (Qt.Key_Space, Qt.Key_Select))
        if mouse_click or key_press:
            model.setData(index, not check_data)
            return True
        else:
            return super(CheckboxDelegate,
                         self).editorEvent(event, model, option, index) 
[docs]    def sizeHint(self, option=None, index=None):
        """
        Provide a reasonable default size for the table cell.  If no index is
        provided, then option is not required and it is assumed that the cell
        contains check data.
        See Qt documentation for an explanation of arguments and return type
        """
        if index is not None and self._getCheckData(index) is None:
            return super(CheckboxDelegate, self).sizeHint(option, index)
        app_style = QtWidgets.QApplication.style()
        cb_rect = app_style.subElementRect(app_style.SE_CheckBoxIndicator,
                                           QtWidgets.QStyleOptionButton())
        size = cb_rect.size()
        # Make sure there is a slight margin so the sides of the check box don't
        # overlap with the table grid lines
        size *= 1.5
        return size  
[docs]class ProgressBarDelegate(AbstractCustomDelegate):
    """
    This delegate displays if the Qt.DisplayRows datat is an int or tuple;
    standard delegate behavior is invoked otherwise.
    Integer values are displayed as percentages on the progress bar. These must
    be between -100 and 100.
    The tuple should consist of (value, maximum), in which case the progress
    text is shown as abs(value)/maximum.
    A negative value flags the existence of an error and changes the color to
    orange.
    """
    DRAW_STYLE = QtWidgets.QStyle.CE_ProgressBar
[docs]    def createEditor(self, parent, option, index):
        """
        This delegate does not have a separate editor widget, so we return None.
        All arguments are ignored, but are present for Qt compatibility.
        """
        return None 
[docs]    def paint(self, painter, option, index):
        """
        Paint the progress bar or use the default delegate
        :param painter: The painter being used to render the delegate
        :type painter: `QtGui.QPainter`
        :param option: The style options to use when painting
        :type option: `QtWidgets.QStyleOptionViewItem`
        :param index: The index being represented
        :type index: `QtCore.QModelIndex`
        """
        data = index.data(Qt.DisplayRole)
        if isinstance(data, int):
            pb_style = self._getProgressBarStyle(option, data)
        elif isinstance(data, tuple):
            value, maximum = data
            pb_style = self._getProgressBarStyle(option,
                                                 value,
                                                 maximum=maximum,
                                                 percent=False)
        else:
            super(ProgressBarDelegate, self).paint(painter, option, index)
            return
        self.paintItemBackground(painter, option, index)
        app_style = QtWidgets.QApplication.style()
        painter.save()
        if sys.platform == "darwin":
            # Paint the progress bar in the correct cell (PANEL-18688)
            # May be fixed in Qt 6.0 https://bugreports.qt.io/browse/PYSIDE-1464
            painter.translate(pb_style.rect.x(), pb_style.rect.y())
        app_style.drawControl(self.DRAW_STYLE, pb_style, painter)
        painter.restore() 
    def _getProgressBarStyle(self, option, progress, maximum=100, percent=True):
        """
        Create a `QtWidgets.QStyleOptionButton` with the appropriate
        options for the desired progress bar.
        :param option: The style options to use when painting
        :type option: `QtWidgets.QStyleOptionViewItem`
        :param progress: The value -100..100 for the progress bar, negative value
                     flags error state
        :type progress: int
        :return: The styled `QtWidgets.QStyleOptionButton`
        :rtype: `QtWidgets.QStyleOptionButton`
        """
        style = QtWidgets.QStyleOptionProgressBar()
        style.initFrom(option.widget)
        style.minimum = 0
        style.maximum = maximum
        color = QtGui.QColor("#C78A3B") if progress < 0 else QtCore.Qt.green
        for group in (QtGui.QPalette.Active, QtGui.QPalette.Inactive):
            for role in (QtGui.QPalette.Window, QtGui.QPalette.Highlight):
                style.palette.setColor(group, role, color)
        style.progress = abs(progress)
        if percent:
            style.text = "{}%".format(style.progress)
        else:
            style.text = "{}/{}".format(style.progress, maximum)
        style.textVisible = True
        style.rect = option.rect
        return style
[docs]    def sizeHint(self, option=None, index=None):
        """
        Provide a reasonable default size for the table cell.  If no index is
        provided, then option is not required and it is assumed that the cell
        contains progress bar data.
        See Qt documentation for an explanation of arguments and return type
        """
        if index is not None and self._getProgressBarData(index) is None:
            return super(ProgressBarDelegate, self).sizeHint(option, index)
        return option.rect  
[docs]class LinkDelegate(PushButtonDelegate):
    """
    A delegate containing a clickable text. Text will be
    taken from the Qt.DisplayRole data. When clicked, this delegate
    will emit a `clicked` signal with either:
    - If `role` is None, a `QtCore.QModelIndex` for the cell that was clicked.
    - Otherwise, the `role` data for the cell that was clicked.
    When optional link role is provided data with that is checked to
    determine whether link should be enabled. When link role is None
    link is always enabled. When link is disabled clicked signal is not
    emitted.
    :cvar DISABLED_STYLE: style sheet that sets button text to standard black
    :vartype DISABLED_STYLE: str
    :cvar ENABLED_STYLE: style sheet that sets button text to "Maestro blue" to
        match standard link appearance
    :vartype ENABLED_STYLE: str
    :ivar _cursor_over_delegate: whether the cursor is over the delgate
    :vartype _cursor_over_delegate: bool
    :ivar _cursor_override_active: whether the "pointing hand" cursor override
        is currently in effect
    :vartype _cursor_override_active: bool
    """
    DISABLED_STYLE = 'QPushButton {color: black}'
    ENABLED_STYLE = 'QPushButton {color: #34b0f8;}'
[docs]    def __init__(self, view, role=None, link_role=None):
        """
        :param view: The view that this delegate will be added to.  Note that
            mouse tracking will be enabled in the view.
        :type view: QtWidgets.QTableView
        :param role: The role to emit data for when a button is clicked.  If not
            given, the index that was clicked will be emitted instead.  This value
            may be specified after instantiation using `setRole`.
        :type role: int or NoneType
        :param link_role: The role to check data that determines whether link
            should be enabled.
        :type link_role: int or NoneType
        """
        super().__init__(view, role)
        self._link_role = link_role
        self._cursor_over_delegate = False
        self._cursor_override_active = False 
[docs]    def indexIsEnabled(self, index):
        """
        :return: whether the specified index should have an enabled link
        :rtype: bool
        """
        return not self._link_role or index.data(self._link_role) 
    def _getButtonStyle(self, option, index, btn_txt=None):
        """
        Create a QtWidgets.QStyleOptionButton with the appropriate options.
        :param option: The style options to use when painting
        :type option: QtWidgets.QStyleOptionViewItem
        :param index: The index that the button will be painted on
        :type index: QtCore.QModelIndex
        :param btn_txt: The text to be displayed in the button.  It not given,
            will be retrieved from index.
        :param btn_txt: str
        :return: The styled QtWidgets.QStyleOptionButton
        :rtype: QtWidgets.QStyleOptionButton
        """
        if btn_txt is None:
            btn_txt = index.data()
        btn_style = QtWidgets.QStyleOptionButton()
        if self.indexIsEnabled(index):
            self._button.setStyleSheet(self.ENABLED_STYLE)
        else:
            self._button.setStyleSheet(self.DISABLED_STYLE)
        btn_style.palette = self._button.palette()
        btn_style.features = QtWidgets.QStyleOptionButton.ButtonFeature.Flat
        btn_style.text = btn_txt
        btn_style.rect = self._getButtonRect(btn_txt, btn_style, option.rect,
                                             index)
        return btn_style
    def _buttonClicked(self, index):
        """
        Emit the button clicked signal with the appropriate data.
        :param index: The index to emit the signal for.
        :type index: QtCore.QModelIndex
        """
        if self.indexIsEnabled(index):
            super()._buttonClicked(index)
[docs]    def paint(self, painter, option, index):
        """
        Temporarily updates the painter font to be bold to match standard link
        appearance.
        """
        link_enabled = self.indexIsEnabled(index)
        link_font = painter.font()
        link_font.setBold(link_enabled)
        painter.save()
        painter.setFont(link_font)
        super().paint(painter, option, index)
        painter.restore() 
[docs]    def eventFilter(self, viewport, event):
        """
        Catch all events in order to apply the correct appearance to the cursor
        based on its position.
        :param viewport: The view's viewport.  Not used, but present for
            compatibility with Qt's event filtering.
        :type viewport: QtWidgets.QWidget
        :param event: The event to filter
        :type event: QtCore.QEvent
        :return: True if the event was handled and does not need to be passed to
            the viewport.  False otherwise.  We want all events to be passed to
            the viewport, so we always return False.
        :rtype: bool
        """
        result = super().eventFilter(viewport, event)
        if event.type() == event.Leave or self._mouse_rc is None:
            # The cursor has left the table area or is over an area that does
            # not contain a cell
            self._setCursorOverrideActive(False)
            return result
        if event.type() != event.MouseMove:
            return result
        view = self.parent()
        model = view.model()
        index = model.index(*self._mouse_rc)
        delegate = view.itemDelegate(index)
        if isinstance(delegate, LinkDelegate) and self.indexIsEnabled(index):
            self._setCursorOverrideActive(True)
        else:
            self._setCursorOverrideActive(False)
        return result 
    def _setCursorOverrideActive(self, active):
        """
        Activate or deactivate the "pointing hand" cursor override if possible.
        :param active: whether to attempt to activate or deactivate the cursor
            override
        :type active: bool
        """
        if self._cursor_over_delegate == active:
            return
        self._cursor_over_delegate = active
        app = QtWidgets.QApplication.instance()
        cursor_is_overridden = app.overrideCursor() is not None
        if active and not cursor_is_overridden:
            # Only override cursor if it is not already overridden (e.g. with
            # a "waiting" cursor)
            app.setOverrideCursor(QtGui.QCursor(Qt.PointingHandCursor))
            self._cursor_override_active = True
        elif not active and self._cursor_override_active:
            # Only restore the cursor if it was overridden by this delegate in
            # the first place
            app.restoreOverrideCursor()
            self._cursor_override_active = False 
[docs]def structure_tuple(st):
    """
    Produce a hashable `tuple` to identify a structure by its Cartesian
    coordinates and atom types.
    :param st: a structure instance
    :type st: `structure.Structure`
    :return: two tuples containing a subset of the data from the structure
             instance. The first contains the atom types; The second contains
             the Cartesian coordinates of the atoms.
    :rtype: tuple(tuple(str), tuple(float))
    """
    type_names = tuple(a.atom_type_name for a in st.atom)
    xyz = st.getXYZ()
    xyz_values = tuple(float(xyz_val) for xyz_val in np.nditer(xyz))
    return type_names, xyz_values 
[docs]class HashableChmMol:
    """
    A `ChmMol` wrapper that extracts data from the `ChmMol` object and uses it
    to present a useful hash value. This is necessary to ensure that a `ChmMol`
    instance is not accidentally matched with a totally different `ChmMol`
    because they happen to share the same memory location (at different times).
    All hashable data is extracted at the moment this object is initialized, so
    this class is only meant as a transient wrapper for internal use within
    `ChmMolDelegate`.
    """
[docs]    def __init__(self, chmmol):
        """
        Parse and store significant data from a `ChmMol` object.
        :param chmmol: a `ChmMol` instance
        :type chmmol: canvas2d.ChmMol
        :ivar _coords: the Cartesian coordinates of the stored `ChmMol`
            instance, saved at the moment this object was initialized
        :vartype _coords: tuple(tuple(float))
        :ivar _atoms: the atomic symbols of the stored `ChmMol` instance, saved
            at the moment this object was initialized
        :vartype _atoms: tuple(str)
        :ivar _smarts: the SMARTS string of the stored `ChmMol` instance, saved
            at the moment this object was initialized
        :vartype _smarts: str
        """
        self._chmmol = chmmol
        atoms = [chmmol.getAtom(idx) for idx in range(chmmol.getAtomCount())]
        self._coords = tuple((a.getX(), a.getY(), a.getZ()) for a in atoms)
        self._atoms = tuple(a.getSymbol() for a in atoms)
        self._smarts = chmmol.getSMARTS() 
    @property
    def chmmol(self):
        """
        :return: the wrapped `ChmMol` instance
        :rtype: canvas2d.ChmMol
        """
        return self._chmmol
    def __hash__(self):
        return hash((self.chmmol, self._coords, self._atoms, self._smarts))
    def __eq__(self, other):
        if not isinstance(other, HashableChmMol):
            return False
        return hash(self) == hash(other)