Source code for schrodinger.ui.qt.delegates

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
from schrodinger.ui.qt.standard.colors import LightModeColors

# 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 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)
[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 PushButtonDelegate(MouseTrackingDelegateMixin, QtWidgets.QStyledItemDelegate): """ A delegate containing a clickable push button. The button name will be taken from the Qt.DisplayRole data. Then 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. """ clicked = QtCore.pyqtSignal(object)
[docs] def __init__(self, view, 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 """ super().__init__(view) self._btn_down = None # index of the button that is down self._mouse_over = None # index of button that the mouse is over # We only need the palette from the button, but we keep a reference to # the button itself to prevent garbage collection of the button from # triggering destruction of the palette. self._button = QtWidgets.QPushButton() self.setRole(role)
[docs] def setRole(self, role): """Specify the role to emit data for when a button is clicked. If None, the index that was clicked will be emitted instead. :param role: The role :type role: int or NoneType """ self._role = role
[docs] def paint(self, painter, option, index): # See Qt documentation for method documentation btn_txt = index.data() if btn_txt is None: super(PushButtonDelegate, self).paint(painter, option, index) return blank_index = index.model().index(-1, -1) super(PushButtonDelegate, self).paint(painter, option, blank_index) btn_style = self._getButtonStyle(option, index, btn_txt) app_style = QtWidgets.QApplication.style() app_style.drawControl(app_style.CE_PushButton, btn_style, painter)
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() btn_style.palette = self._button.palette() btn_style.state = option.state & ~QtWidgets.QStyle.State_MouseOver if self._mouse_over == self._getRc(index): btn_style.state |= QtWidgets.QStyle.State_MouseOver if self._btn_down == (index.row(), index.column()): btn_style.state |= QtWidgets.QStyle.State_Sunken else: btn_style.state |= QtWidgets.QStyle.State_Raised # Mac needs State_Active off to ensure painting of the button text btn_style.state &= ~QtWidgets.QStyle.State_Active btn_style.text = btn_txt btn_style.rect = self._getButtonRect(btn_txt, btn_style, option.rect, index) return btn_style def _getButtonRect(self, btn_txt, btn_style, option_rect, index): """ Determine the rectangle for painting the button :param btn_txt: The text to be displayed in the button. It not given, will be retrieved from index. :param btn_txt: str :param btn_style: The button style :type btn_style: `QtWidgets.QStyleOptionButton` :param option_rect: The index rectangle specified in the `QtWidgets.QStyleOptionViewItem` :type option_rect: `QtCore.QRect` :param index: The index that the button will be painted on :type index: `QtCore.QModelIndex` :return: The rectangle to paint the button into :rtype: `QtCore.QRect` """ app_style = QtWidgets.QApplication.style() btn_size = self._getButtonSize(btn_txt, btn_style, app_style) if btn_size.width() > option_rect.width(): btn_size.setWidth(option_rect.width()) if btn_size.height() > option_rect.height(): btn_size.setHeight(option_rect.height()) alignment = index.data(Qt.TextAlignmentRole) if alignment is None: alignment = Qt.AlignCenter else: # PyQt converts AlignmentFlag objects to plain ints when retrieved # via the data method, so we have to convert them back to # Alignments alignment = Qt.Alignment(alignment) return app_style.alignedRect(Qt.LayoutDirectionAuto, alignment, btn_size, option_rect) def _getButtonSize(self, btn_txt, btn_style, app_style=None): """ Determine the size of the button :param btn_txt: The text to be displayed in the button :param btn_txt: str :param btn_style: The button style :type btn_style: `QtWidgets.QStyleOptionButton` :param app_style: The QApplication's style :type app_style: `QtWidgets.QStyle` :return: The size of the button :rtype: `QtCore.QSize` """ if app_style is None: app_style = QtWidgets.QApplication.style() font = self.parent().font() font_metrics = QtGui.QFontMetrics(font) txt_size = font_metrics.size(Qt.TextShowMnemonic, btn_txt) btn_size = app_style.sizeFromContents(QtWidgets.QStyle.CT_PushButton, btn_style, txt_size) return btn_size
[docs] def editorEvent(self, event, model, option, index): """ Handle mouse clicks and key presses. Left clicking on the button or hitting the space bar will emit the clicked signal. Left clicks outside of the button will be handled as normal (i.e. selecting the cell). See Qt documentation for documentation on arguments and return value. """ handled = False if not index.flags() & Qt.ItemIsEnabled: # Don't handle any events if the button is disabled. pass elif isinstance(event, QtGui.QMouseEvent): handled = self._mouseEvent(event, model, option, index) elif (event.type() == event.KeyPress and event.key() in (Qt.Key_Space, Qt.Key_Select)): self._buttonClicked(index) handled = True if not handled: return super(PushButtonDelegate, self).editorEvent(event, model, option, index) else: return True
def _mouseEvent(self, event, model, option, index): """ Handle mouse events :param event: The event that occurred :type event: `PyQt5.QtCore.QEvent` :param model: The data model :type model: `QtCore.QAbstractTableModel` :param option: The style options for the cell :type option: `QtWidgets.QStyleOptionViewItem` :param index: The index being edited :type index: `PyQt5.QtCore.QModelIndex` :return: True if the event was handled. False otherwise. :rtype: bool """ redraw = False handled = False cur_index = self._getRc(index) if (event.type() == event.MouseButtonPress and event.button() == Qt.LeftButton and self._mouseOnButton(event, option, index)): self._btn_down = cur_index redraw = True handled = True elif (event.type() == event.MouseButtonRelease and event.button() == Qt.LeftButton and self._btn_down == cur_index): self._btn_down = None redraw = True self._buttonClicked(index) handled = True elif event.type() == event.MouseMove: mouse_over_btn = self._mouseOnButton(event, option, index) if not mouse_over_btn and self._mouse_over is not None: self._mouse_over = None redraw = True elif mouse_over_btn and self._mouse_over is None: self._mouse_over = cur_index redraw = True if not mouse_over_btn and self._btn_down is not None: self._btn_down = None redraw = True if redraw: self._redrawIndex(model, index) return handled def _mouseOnButton(self, event, option, index): """ Determine if the given mouse event occurred over the button. :param event: The event that occurred :type event: `PyQt5.QtCore.QEvent` :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 over the button. False otherwise. :rtype: bool """ btn_txt = index.data() btn_style = self._getButtonStyle(option, index, btn_txt) btn_rect = btn_style.rect return btn_rect.contains(event.pos()) 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 index.data() is None: # If the index returns `None`, the button should be disabled return if self._role is None: self.clicked.emit(index) else: self.clicked.emit(index.data(self._role))
[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. Without these events, we may not know when the mouse is no longer over a button, so we won't know when to remove mouse-over highlighting on the button. Since this class has tokeep track of `_btn_down` and `_mouse_over` the parent class' eventFilter can not be used. :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 self._btn_down is None and self._mouse_over is None: return result elif event.type() == event.Leave: model = self.parent().model() if self._btn_down is not None: row, col = self._btn_down self._btn_down = None self._redrawRc(model, row, col) if self._mouse_over is not None: row, col = self._mouse_over self._mouse_over = None self._redrawRc(model, row, col) elif event.type() == event.MouseMove: view = self.parent() model = view.model() if self._btn_down is not None: index = model.index(*self._btn_down) rect = view.visualRect(index) if not rect.contains(event.pos()): self._btn_down = None self._redrawIndex(model, index) elif self._mouse_over is not None: index = model.index(*self._mouse_over) rect = view.visualRect(index) if not rect.contains(event.pos()): self._mouse_over = None self._redrawIndex(model, index) return result
[docs] def sizeHint(self, option, index): # See Qt documentation btn_style = self._getButtonStyle(option, index) return btn_style.rect.size()
[docs] def sizeHintForText(self, txt): """ Generate a size hint using the specified text. :param txt: The text to use for determining the button's size :type txt: str :return: The calculated size hint :rtype: `QtCore.QSize` """ btn_style = QtWidgets.QStyleOptionButton() return self._getButtonSize(txt, btn_style)
[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 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 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 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: QtCore.Qt.ItemFlag :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 RadioButtonDelegate(CheckboxDelegate): """ Subclass of `CheckboxDelegate` giving the widget a radio button appearance. Note that the model must define check state behavior, e.g. to uncheck all other rows when a new row is checked. """ DRAW_STYLE = QtWidgets.QStyle.CE_RadioButton
[docs] def paint(self, painter, option, index): # This fixes PANEL-10092 on Windows 7: option.styleObject.setProperty("_q_no_animation", True) return super(RadioButtonDelegate, self).paint(painter, option, index)
[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 ToolTipIconDelegate(AbstractCustomDelegate): """ A delegate for drawing an icon which shows a tooltip when clicked when user hovers the mouse over the cell. Tool tip text must be returned by the model's data() method for the ToolTipRole. """
[docs] def __init__(self, parent, icon): """ Initialize the delegate. :param parent: Parent view widget :param icon: QIcon to draw in the cell. """ super(ToolTipIconDelegate, self).__init__(parent) self._icon = icon
[docs] def paint(self, painter, option, index): """ Paint the appropriate icon, but only for cells that have a ToolTipRole. :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) tooltip = index.data(Qt.ToolTipRole) if tooltip: self._icon.paint(painter, option.rect, Qt.AlignLeft)
[docs] def editorEvent(self, event, model, option, index): """ Handle mouse clicks: show tool tip if the cell is clicked. """ if (event.type() == event.MouseButtonRelease and event.button() == Qt.LeftButton): tooltip = index.data(Qt.ToolTipRole) if tooltip: QtWidgets.QToolTip.showText(QtGui.QCursor.pos(), tooltip) return True # to avoid table scrolling return False
[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 disabled link color :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 = f'QPushButton {{color: {LightModeColors.LINK_DISABLED}}}' ENABLED_STYLE = f'QPushButton {{color: {LightModeColors.LINK}}}'
[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. (Note: may need to use a role different from Qt.DisplayRole if the link should use the disabled stylesheet) :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 """ if self._link_role is None: return True data = index.data(self._link_role) return not any((data is False, data is None))
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(True) 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