Source code for schrodinger.application.msv.gui.row_delegates

"""
Delegates used to paint an entire row at a time in the MSV tree view.
Normal Qt delegates inherit from QStyledItemDelegate and paint a single
cell at a time.  By painting a row at a time instead, we massively speed up
(~6x) painting for the alignment.
"""

import string
from enum import Enum

from schrodinger import structure
from schrodinger.application.msv.gui import color
from schrodinger.application.msv.gui.color import ResidueTypeScheme
from schrodinger.application.msv.gui.viewconstants import CustomRole
from schrodinger.application.msv.gui.viewconstants import ResSelectionBlockStart
from schrodinger.application.msv.gui.viewconstants import RowType
from schrodinger.application.msv.gui.viewmodel import NON_SELECTABLE_ANNO_TYPES
from schrodinger.protein import annotation
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt.QtCore import Qt
from schrodinger.utils.scollections import DefaultFactoryDict
from schrodinger.utils.scollections import IdDict

PROT_SEQ_ANN_TYPES = annotation.ProteinSequenceAnnotations.ANNOTATION_TYPES
PROT_ALIGN_ANN_TYPES = annotation.ProteinAlignmentAnnotations.ANNOTATION_TYPES
# Rows that get the special hover brush
SPECIAL_HOVER_ROW_TYPES = NON_SELECTABLE_ANNO_TYPES - {
    PROT_ALIGN_ANN_TYPES.indices
}
# Rows that don't get the normal hover brush (may still get special hover brush)
NO_HOVER_ROW_TYPES = SPECIAL_HOVER_ROW_TYPES | frozenset(
    {RowType.Spacer, PROT_ALIGN_ANN_TYPES.indices})
RES_FONT_TEXT = "WOI"

# A list of data roles that should be fetched per-row, but that may be defined
# as per-cell for certain row types. This data will be discarded if the role is
# listed in the row delegate's PER_CELL_PAINT_ROLES.
POSSIBLE_PER_ROW_PAINT_ROLES = frozenset(
    (Qt.DisplayRole, Qt.ForegroundRole, Qt.BackgroundRole))

# A list of all data roles that should be fetched per-row.
PER_ROW_PAINT_ROLES = POSSIBLE_PER_ROW_PAINT_ROLES | frozenset(
    (Qt.FontRole, CustomRole.RowType, CustomRole.RowHeightScale,
     CustomRole.DataRange, CustomRole.ResOutline, CustomRole.SeqMatchesRefType))

# Row heights for annotations with constant row heights (i.e. when row height
# doesn't vary with font size.)
SINGLE_ANNOTATION_HEIGHT = 16
DOUBLE_ANNOTATION_HEIGHT = 40
RULER_HEIGHT = 24
RIGHT_ALIGN_PADDING = 5


[docs]def all_delegates(): """ Return a list of all delegates in this module. :rtype: list(AbstractDelegate) """ return [ obj for obj in globals().values() if (isinstance(obj, type) and issubclass(obj, AbstractDelegate)) ]
[docs]class AbstractDelegate(object): """ Base delegate class. Non-abstract subclasses must must provide appropriate values for ANNOTATION_TYPE and PER_CELL_PAINT_ROLES and must reimplement `paintRow`. :cvar ANNOTATION_TYPE: The annotation type associated with this class :vartype ANNOTATION_TYPE: `enum.Enum` or None :cvar PER_CELL_PAINT_ROLES: Data roles that should be fetched per-cell (as opposed to per-row). Note that data for roles in `PER_ROW_PAINT_ROLES` will be fetched per-row. :vartype PER_CELL_PAINT_ROLES: frozenset :cvar ROW_HEIGHT_CONSTANT: The row height, in pixels. Subclasses for rows that should not vary in size as the font size changes should define this value. :vartype ROW_HEIGHT_CONSTANT: int or None :cvar ROW_HEIGHT_SCALE: The row height relative to the font height. This value will be ignored if `ROW_HEIGHT_CONSTANT` is defined. :vartype ROW_HEIGHT_SCALE: int """ ANNOTATION_TYPE = tuple() PER_CELL_PAINT_ROLES = frozenset() ROW_HEIGHT_CONSTANT = None ROW_HEIGHT_SCALE = 1
[docs] def __init__(self): super(AbstractDelegate, self).__init__() # selected cells are overlaid with a pale blue and partially # transparent brush self._sel_brush = QtGui.QBrush(color.PALE_BLUE)
[docs] def clearCache(self): """ Clear any cached data. Called whenever the font size changes. The base implementation of this method does nothing, but subclasses that cache data must reimplement this method to prevent cache staling. """
[docs] def paintRow(self, painter, per_cell_data, per_row_data, row_rect, left_edges, top_edge, col_width, row_height): """ Paint an entire row of data. Non-abstract subclasses must reimplement this method. :param painter: The painter to use for painting. :type painter: QtGui.QPainter :param per_cell_data: A list of data for the entire row. Each column is represented by a dictionary of {role: value}. Only roles with differing values between different columns are present in these dictionaries. Note that the keys of this dictionary are `self.PER_CELL_PAINT_ROLES`. :type per_cell_data: list(dict(int, object)) :param per_row_data: A dictionary of {role: value} for data that's the same for all columns in the row. Note that the keys of this dictionary are `PER_ROW_PAINT_ROLES - self.PER_CELL_PAINT_ROLES`. :type per_row_data: dict(int, object) :param row_rect: A rectangle that covers the entire area to be painted. :type row_rect: QtCore.QRect :param left_edges: A list of the x-coordinates for the left edges of each column. :type left_edges: list(int) :param top_edge: The y-coordinate of the top edge of the row :type top_edge: int :param col_width: The width of each column in pixels. :type col_width: int :param row_height: The height of the row in pixels. :type row_height: int """ raise NotImplementedError
def _paintBackground(self, painter, per_cell_data, left_edges, top_edge, col_width, row_height): """ Paint background colors for all cells in the row. This method assumes that each cell has a different color background. :param painter: The painter to use for painting. :type painter: QtGui.QPainter :param per_cell_data: A list of data for the entire row. Each column is represented by a dictionary of {role: value}. :type per_cell_data: list(dict(int, object)) :param left_edges: A list of the x-coordinates for the left edges of each column. :type left_edges: list(int) :param top_edge: The y-coordinate of the top edge of the row :type top_edge: int :param col_width: The width of each column in pixels. :type col_width: int :param row_height: The height of the row in pixels. :type row_height: int """ # reusing the same QRect for every drawRect call is about 2.5% faster # than creating a new one for each cell rect = QtCore.QRect(0, 0, col_width, row_height) painter.setPen(Qt.NoPen) for left, data in zip(left_edges, per_cell_data): background = data.get(Qt.BackgroundRole) if background is None: continue rect.moveTo(left, top_edge) painter.setBrush(background) painter.drawRect(rect) def _paintOverlay(self, painter, per_cell_data, left_edges, top_edge, col_width, row_height, role, brush): """ Use `brush` to paint rectangles that cover all cells with truthy data for role `role`. This can be used to, e.g., cover all selected cells with a partially transparent blue. :param painter: The painter to use for painting. :type painter: QtGui.QPainter :param per_cell_data: A list of data for the entire row. Each column is represented by a dictionary of {role: value}. :type per_cell_data: list(dict(int, object)) :param left_edges: A list of the x-coordinates for the left edges of each column. :type left_edges: list(int) :param top_edge: The y-coordinate of the top edge of the row :type top_edge: int :param col_width: The width of each column in pixels. :type col_width: int :param row_height: The height of the row in pixels. :type row_height: int :param role: The data role to check. Any columns for which this role returns a truthy value will be painted. :type role: int :param brush: The brush to use for painting. :type brush: QtGui.QBrush """ painter_init = False rect_left = None width_in_cols = None for cur_left, data in zip(left_edges, per_cell_data): if data.get(role): if rect_left is None: rect_left = cur_left width_in_cols = 1 else: width_in_cols += 1 elif rect_left is not None: if not painter_init: painter.setPen(Qt.NoPen) painter.setBrush(brush) rect = QtCore.QRect(rect_left, top_edge, width_in_cols * col_width, row_height) painter.drawRect(rect) rect_left = None if rect_left is not None: if not painter_init: painter.setPen(Qt.NoPen) painter.setBrush(brush) rect = QtCore.QRect(rect_left, top_edge, width_in_cols * col_width, row_height) painter.drawRect(rect) def _popPaddingCells(self, per_cell_data): """ Utility method that pops off padding cells from the end of `per_cell_data`. Any delegate that calls this method should have `CustomRole.ResidueIndex` in its `PER_CELL_PAINT_ROLES`. :param per_cell_data: A list of data for the entire row to remove padding cells from. :type per_cell_data: list(dict(int, object)) """ assert CustomRole.ResidueIndex in self.PER_CELL_PAINT_ROLES while per_cell_data and per_cell_data[-1].get( CustomRole.ResidueIndex) is None: per_cell_data.pop()
[docs] def rowHeight(self, text_height): """ Return the appropriate height for this row type. :param text_height: The height of the current font, in pixels. :type text_height: int :return: The desired height of this row, in pixels. :rtype: int """ if self.ROW_HEIGHT_CONSTANT is None: return int(text_height * self.ROW_HEIGHT_SCALE) else: return self.ROW_HEIGHT_CONSTANT
[docs] def setLightMode(self, enabled): """ Reimplement on subclasses which need to respond to changes in light mode :param enabled: whether to turn light mode on or off :type enabled: bool """ pass
[docs]class AbstractDelegateWithTextCache(AbstractDelegate): """ A delegate that caches text using QStaticTexts. Note that QPainter::drawStaticText is roughly 10x faster than QPainter::drawText. """
[docs] def __init__(self, *args, **kwargs): super(AbstractDelegateWithTextCache, self).__init__(*args, **kwargs) # stores tuples of (QStaticText object, x-offset needed for center # alignment) self._static_texts = DefaultFactoryDict(self._populateStaticText) # Stores the y-offset needed for center alignment self._y_offset = None # Stores a QFontMetrics object for the current font self._font_metrics = None
[docs] def clearCache(self): # See parent class for method documentation # We don't actually need to get rid of the QStaticText objects since # they'll automatically update their internal calculations when we try # to paint them with a new font, but there's not much of an advantage to # reusing them and we have to get rid of the x-offsets anyway, so we # clear the entire _static_texts dictionary. self._static_texts.clear() self._font_metrics = None self._y_offset = None
def _populateYOffsetAndFontMetrics(self, font, row_height): """ Store values for self._y_offset and self._font_metrics. Should only be called if the cache was just cleared. :param font: The current font :type font: QtGui.QFont :param row_height: The height of the row in pixels :type row_height: int """ self._font_metrics = QtGui.QFontMetrics(font) # Add a one pixel offset to help with font centering self._y_offset = (row_height - self._font_metrics.height()) // 2 + 1 def _populateStaticText(self, text, col_width): """ Create a new `QStaticText` object for the specified text. This method should only be called by `self._static_texts.__missing__`. Otherwise, static texts should be retrieved using `self._static_texts`. :param text: The text to convert to a `QStaticText` object :type text: str :param col_width: The width of a column in pixels :type col_width: int :return: A tuple of: - The newly created `QStaticText` object - The x-offset needed to center the text :rtype: tuple(QtGui.QStaticText, int) """ static_text = QtGui.QStaticText(text) x = (col_width - self._font_metrics.horizontalAdvance(text)) // 2 return static_text, x def _paintText(self, painter, per_cell_data, per_row_data, left_edges, top_edge, col_width, row_height): """ Paint text for all cells in the row. This method assumes that each cell has a different text color. :param painter: The painter to use for painting. :type painter: QtGui.QPainter :param per_cell_data: A list of data for the entire row. Each column is represented by a dictionary of {role: value}. The text and color to paint are taken from the `Qt.DisplayRole` and `Qt.ForegroundRole` values in these dictionaries. :type per_cell_data: list(dict(int, object)) :param per_row_data: A dictionary of {role: value} for data that's the same for all columns in the row. The font is taken from the `Qt.FontRole` value in this dictionary. :type per_row_data: dict(int, object) :param left_edges: A list of the x-coordinates for the left edges of each column. :type left_edges: list(int) :param top_edge: The y-coordinate of the top edge of the row :type top_edge: int :param col_width: The width of each column in pixels. :type col_width: int :param row_height: The height of the row in pixels. :type row_height: int """ font = per_row_data[Qt.FontRole] if self._y_offset is None: self._populateYOffsetAndFontMetrics(font, row_height) text_y = top_edge + self._y_offset painter.setFont(font) for left, data in zip(left_edges, per_cell_data): text = data.get(Qt.DisplayRole) pen_color = data.get(Qt.ForegroundRole) if text is None or pen_color is None: continue painter.setPen(pen_color) static_text, x = self._static_texts[text, col_width] painter.drawStaticText(left + x, text_y, static_text) def _paintSameColorText(self, painter, per_cell_data, per_row_data, left_edges, top_edge, col_width, row_height): """ Paint text for all cells in the row. This method assumes that each cell has the same text color. :param painter: The painter to use for painting. :type painter: QtGui.QPainter :param per_cell_data: A list of data for the entire row. Each column is represented by a dictionary of {role: value}. The text to paint is taken from the `Qt.DisplayRole` values in these dictionaries. :type per_cell_data: list(dict(int, object)) :param per_row_data: A dictionary of {role: value} for data that's the same for all columns in the row. The font and color are taken from the `Qt.FontRole` and `Qt.ForegroundRole` values in this dictionary. :type per_row_data: dict(int, object) :param left_edges: A list of the x-coordinates for the left edges of each column. :type left_edges: list(int) :param top_edge: The y-coordinate of the top edge of the row :type top_edge: int :param col_width: The width of each column in pixels. :type col_width: int :param row_height: The height of the row in pixels. :type row_height: int """ font = per_row_data[Qt.FontRole] pen_color = per_row_data[Qt.ForegroundRole] painter.setFont(font) painter.setPen(pen_color) if self._y_offset is None: self._populateYOffsetAndFontMetrics(font, row_height) text_y = top_edge + self._y_offset for left, data in zip(left_edges, per_cell_data): text = data.get(Qt.DisplayRole) if text is None: continue static_text, x = self._static_texts[text, col_width] painter.drawStaticText(left + x, text_y, static_text)
[docs]class SequenceLogoDelegate(AbstractDelegate): """ This delegate is used to draw multiple residue 1-letter codes at each alignment position. The letters are drawn from bottom to top, in the order of increasing frequency. """ ANNOTATION_TYPE = PROT_ALIGN_ANN_TYPES.sequence_logo PER_CELL_PAINT_ROLES = frozenset((Qt.DisplayRole, CustomRole.ResidueIndex)) # set scaling so that columns with multiple letters display appropriately ROW_HEIGHT_SCALE = 4
[docs] def __init__(self, *args, **kwargs): super(SequenceLogoDelegate, self).__init__(*args, **kwargs) self._color_dict = {} self._cached_pixmaps = {} self._font_height = None self.setLightMode(False)
[docs] def clearCache(self): self._cached_pixmaps.clear() self._font_height = None
[docs] def setLightMode(self, enabled): """ When light mode is enabled, use darker colors. :param enabled: Whether or not light mode is enabled :type enabled: bool """ for key, rgb in ResidueTypeScheme.COLOR_BY_KEY.items(): color = QtGui.QColor(*rgb) if enabled: # darken the colors used for sequence logo when in light mode # based on tweaking in MSV-2616 h, s, v, _ = color.getHsvF() color.setHsvF(h, s + .16, v - .07) self._color_dict[key] = color self.clearCache()
[docs] def paintRow(self, painter, per_cell_data, per_row_data, row_rect, left_edges, top_edge, col_width, row_height): self._popPaddingCells(per_cell_data) font = QtGui.QFont(per_row_data[Qt.FontRole]) fm = QtGui.QFontMetricsF(font) if self._font_height is None: self._font_height = -fm.tightBoundingRect( string.ascii_uppercase).top() row_to_font_ratio = row_height / self._font_height for left_edge, cur_cell_data in zip(left_edges, per_cell_data): data = cur_cell_data[Qt.DisplayRole] if data not in self._cached_pixmaps: total_bits, aa_freq_list = data # Calculate ratio between this columns bits and the maximum # number of bits. bits_ratio = total_bits / annotation.LOGO_MAX_DIVERSITY y_pos = row_height # Setup pixmap pixmap = QtGui.QPixmap(QtCore.QSize(col_width, row_height)) pixmap.fill(Qt.transparent) # Setup painter pixmap_painter = QtGui.QPainter(pixmap) pixmap_painter.setFont(font) # Paint pixmap in reversed order so that smaller letters are # on the bottom of the logo for aa, freq in reversed(aa_freq_list): vscale = int(freq * bits_ratio * row_to_font_ratio) if freq == 0 or vscale * self._font_height < 3: continue hscale = col_width / fm.horizontalAdvance(aa) pixmap_painter.scale(hscale, vscale) pixmap_painter.setPen(self._color_dict.get(aa, Qt.white)) pixmap_painter.drawText(0, y_pos // vscale, aa) # Scale is set relative to the previous value so we # reset it here. pixmap_painter.scale(1 / hscale, 1 / vscale) y_pos -= self._font_height * vscale self._cached_pixmaps[data] = pixmap else: pixmap = self._cached_pixmaps[data] painter.drawPixmap(left_edge, top_edge, pixmap)
[docs]class ConsensusSymbolsDelegate(AbstractDelegateWithTextCache): ANNOTATION_TYPE = PROT_ALIGN_ANN_TYPES.consensus_symbols PER_CELL_PAINT_ROLES = frozenset((Qt.DisplayRole, Qt.ForegroundRole)) ROW_HEIGHT_SCALE = 1.5
[docs] def paintRow(self, painter, per_cell_data, per_row_data, row_rect, left_edges, top_edge, col_width, row_height): self._paintText(painter, per_cell_data, per_row_data, left_edges, top_edge, col_width, row_height)
# we don't need to worry about painting selection since this # delegate is only used for global annotations, which can't be selected.
[docs]class ResnumDelegate(AbstractDelegateWithTextCache): """ A delegate to draw the residue number annotation. Numbers are drawn every 5. :cvar MIN_SPACING: Minimum number of spaces between painted residue numbers :vartype MIN_SPACING: int """ ANNOTATION_TYPE = PROT_SEQ_ANN_TYPES.resnum PER_CELL_PAINT_ROLES = frozenset((Qt.DisplayRole, CustomRole.ResSelected)) ROW_HEIGHT_CONSTANT = SINGLE_ANNOTATION_HEIGHT
[docs] def paintRow(self, painter, per_cell_data, per_row_data, row_rect, left_edges, top_edge, col_width, row_height): # The model only provides text for residue numbers that are divisible by # five, so we don't need to worry about doing that filtering here. self._paintSameColorText(painter, per_cell_data, per_row_data, left_edges, top_edge, col_width, row_height) # paint the selection self._paintOverlay(painter, per_cell_data, left_edges, top_edge, col_width, row_height, CustomRole.ResSelected, self._sel_brush)
[docs]class PfamDelegate(AbstractDelegateWithTextCache): """ A delegate to draw the pfam annotation. """ ANNOTATION_TYPE = PROT_SEQ_ANN_TYPES.pfam PER_CELL_PAINT_ROLES = frozenset((Qt.DisplayRole, CustomRole.ResSelected)) ROW_HEIGHT_CONSTANT = SINGLE_ANNOTATION_HEIGHT
[docs] def paintRow(self, painter, per_cell_data, per_row_data, row_rect, left_edges, top_edge, col_width, row_height): self._paintSameColorText(painter, per_cell_data, per_row_data, left_edges, top_edge, col_width, row_height) # paint the selection self._paintOverlay(painter, per_cell_data, left_edges, top_edge, col_width, row_height, CustomRole.ResSelected, self._sel_brush)
[docs]class AbstractResidueDelegate(AbstractDelegateWithTextCache): PER_CELL_PAINT_ROLES = frozenset( (Qt.DisplayRole, Qt.ForegroundRole, Qt.BackgroundRole, CustomRole.SeqresOnly, CustomRole.ResSelected, CustomRole.NonstandardRes)) ROW_HEIGHT_SCALE = 1.2 # This should be an integer so we can paint with QPoint, not QPointF RES_OUTLINE_HALF_PEN_WIDTH = 1
[docs] def __init__(self): super().__init__() self._nonstandard_pen = QtGui.QPen(QtGui.QColor(0, 0, 0, 102), self.RES_OUTLINE_HALF_PEN_WIDTH * 2) self._nonstandard_sel_pen = QtGui.QPen( QtGui.QColor(255, 255, 255, 102), self.RES_OUTLINE_HALF_PEN_WIDTH * 2)
def _paintNonstandard(self, painter, per_cell_data, left_edges, top_edge, col_width, row_height): """ For nonstandard residues, paint a line at the bottom of the rect. See `AbstractDelegate._paintBackground` for argument documentation. """ half_pen_width = self.RES_OUTLINE_HALF_PEN_WIDTH paint_bottom = top_edge + row_height - half_pen_width painter.setBrush(Qt.NoBrush) for left, data in zip(left_edges, per_cell_data): nonstandard = data.get(CustomRole.NonstandardRes) if not nonstandard: continue sel = data.get(CustomRole.ResSelected) pen = self._nonstandard_sel_pen if sel else self._nonstandard_pen painter.setPen(pen) painter.drawLine(left + half_pen_width, paint_bottom, left + col_width - half_pen_width, paint_bottom)
[docs]class ResidueDelegate(AbstractResidueDelegate): """ :cvar EDIT_MODE_ROLES: Roles for data that will only be painted in edit mode. :vartype EDIT_MODE_ROLES: frozenset """ ANNOTATION_TYPE = RowType.Sequence PER_CELL_PAINT_ROLES = ( AbstractResidueDelegate.PER_CELL_PAINT_ROLES | frozenset( (CustomRole.ResAnchored, CustomRole.ChainDivider))) CONSTRAINTS_ROLES = frozenset({CustomRole.PartialPairwiseConstraint}) OUTLINE_ROLES = frozenset({CustomRole.ResidueIndex}) CHIMERA_ROLES = frozenset({CustomRole.HMCompositeRegion}) EDIT_MODE_ROLES = frozenset({CustomRole.ResSelectionBlockStart})
[docs] def __init__(self): super(ResidueDelegate, self).__init__() self._show_constraints = False self._show_outlines = False self._show_chimera = False self._edit_mode = False self._ibar_path = None self._ibar_brush = QtGui.QBrush(Qt.white) self._constraint_outline_pen = QtGui.QPen(Qt.magenta) self._chain_divider_path = None self._chain_divider_brush = QtGui.QBrush(Qt.white) self._chimera_brush = QtGui.QBrush(color.HM_CHIMERA_PICK_COLOR) self._chimera_seqres_only_brush = QtGui.QBrush( color.HM_CHIMERA_PICK_COLOR_STRUCTURELESS, Qt.Dense5Pattern) self._sel_brush = QtGui.QBrush(color.RES_SEL_COLOR) self._nonmatching_brush = QtGui.QBrush(color.NONMATCHING_FADE) self._res_outline_pens = {} self.setLightMode(False)
[docs] def clearCache(self): super().clearCache() self._ibar_path = None self._chain_divider_path = None
[docs] def setEditMode(self, enable): """ Enable or disable edit mode. This affects whether I-bars are painted or not. :param enable: Whether to enable edit mode. :type enable: bool """ self._edit_mode = enable if enable: self.PER_CELL_PAINT_ROLES |= self.EDIT_MODE_ROLES else: self.PER_CELL_PAINT_ROLES -= self.EDIT_MODE_ROLES
[docs] def setConstraintsShown(self, enable): """ Enable or disable constraints. This affects whether constraints are painted or not. :param enable: Whether to display constraints :type enable: bool """ self._show_constraints = enable if enable: self.PER_CELL_PAINT_ROLES |= self.CONSTRAINTS_ROLES else: self.PER_CELL_PAINT_ROLES -= self.CONSTRAINTS_ROLES
[docs] def setResOutlinesShown(self, enable): self._show_outlines = enable if enable: self.PER_CELL_PAINT_ROLES |= self.OUTLINE_ROLES else: self.PER_CELL_PAINT_ROLES -= self.OUTLINE_ROLES
[docs] def setChimeraShown(self, enable): """ Enable or disable chimera mode. This affects whether chimeric regions are painted. :param enable: Whether to display chimeric regions. :type enable: bool """ self._show_chimera = enable if enable: self.PER_CELL_PAINT_ROLES |= self.CHIMERA_ROLES else: self.PER_CELL_PAINT_ROLES -= self.CHIMERA_ROLES
[docs] def paintRow(self, painter, per_cell_data, per_row_data, row_rect, left_edges, top_edge, col_width, row_height): self._paintBackground(painter, per_cell_data, left_edges, top_edge, col_width, row_height) # paint the selection self._paintOverlay(painter, per_cell_data, left_edges, top_edge, col_width, row_height, CustomRole.ResSelected, self._sel_brush) self._paintText(painter, per_cell_data, per_row_data, left_edges, top_edge, col_width, row_height) # Fade out any structureless residues self._paintOverlay(painter, per_cell_data, left_edges, top_edge, col_width, row_height, CustomRole.SeqresOnly, self._seqres_only_brush) # Fade out non-matching sequences if not per_row_data[CustomRole.SeqMatchesRefType]: painter.setPen(Qt.NoPen) painter.setBrush(self._nonmatching_brush) painter.drawRect(row_rect) # paint anchors self._paintOverlay(painter, per_cell_data, left_edges, top_edge, col_width, row_height, CustomRole.ResAnchored, self._anchored_res_brush) # paint chimeric regions if self._show_chimera: self._paintChimera(painter, per_cell_data, left_edges, top_edge, col_width, row_height) # paint residue outlines if self._show_outlines: self._paintResidueOutlines(painter, per_cell_data, per_row_data, left_edges, top_edge, col_width, row_height) if self._show_constraints: self._paintConstraintOutline(painter, per_cell_data, left_edges, top_edge, col_width, row_height) self._paintNonstandard(painter, per_cell_data, left_edges, top_edge, col_width, row_height) self._paintChainDividers(painter, per_cell_data, left_edges, top_edge, col_width, row_height) if self._edit_mode: self._paintIBars(painter, per_cell_data, left_edges, top_edge, col_width, row_height)
[docs] def setLightMode(self, enabled): """ Set light mode for the sequence alignment. This affects the overlay that is drawn over structureless residues and anchored residues """ if enabled: self._seqres_only_brush = QtGui.QBrush(color.SEQRES_ONLY_FADE_LM) self._anchored_res_brush = QtGui.QBrush(color.ANCHOR_RES_FADE_LM) else: self._seqres_only_brush = QtGui.QBrush(color.SEQRES_ONLY_FADE) self._anchored_res_brush = QtGui.QBrush(color.ANCHOR_RES_FADE)
def _getResidueOutlinePen(self, rgb): """ Get a cached pen for painting residue outlines :param rgb: Pen color :type rgb: tuple """ if self._res_outline_pens.get(rgb) is None: pen = QtGui.QPen(QtGui.QColor(*rgb), self.RES_OUTLINE_HALF_PEN_WIDTH * 2) self._res_outline_pens[rgb] = pen return self._res_outline_pens[rgb] def _paintResidueOutlines(self, painter, per_cell_data, per_row_data, left_edges, top_edge, col_width, row_height): """ Paint rectangular outlines around blocks of residues """ half_pen_width = self.RES_OUTLINE_HALF_PEN_WIDTH paint_top = top_edge + half_pen_width painter.setBrush(Qt.NoBrush) color_infos = per_row_data[CustomRole.ResOutline] if color_infos: self._popPaddingCells(per_cell_data) else: return cell_idx = 0 outline_idx = 0 while cell_idx < len(per_cell_data) and outline_idx < len(color_infos): cell_data = per_cell_data[cell_idx] res_idx = cell_data[CustomRole.ResidueIndex] # Skip over outlines that come before the current residue while color_infos[outline_idx].end_idx < res_idx: outline_idx += 1 if outline_idx == len(color_infos): # Residue comes after all the outlines, nothing to paint return outline_info = color_infos[outline_idx] pen = self._getResidueOutlinePen(outline_info.color) painter.setPen(pen) left_edge = left_edges[cell_idx] # Advance cell_idx to the right edge of the outline so we can paint # it all at once found_left = outline_info.start_idx == res_idx found_right = False if outline_info.start_idx <= res_idx < outline_info.end_idx: for next_cell_idx in range(cell_idx + 1, len(per_cell_data)): next_cell_data = per_cell_data[next_cell_idx] next_res_idx = next_cell_data[CustomRole.ResidueIndex] cell_idx += 1 if next_res_idx >= outline_info.end_idx: found_right = True break elif res_idx == outline_info.end_idx: found_right = True else: cell_idx += 1 continue right_edge = left_edges[cell_idx] + col_width width = right_edge - left_edge paint_left = left_edge + half_pen_width right = left_edge + width - half_pen_width paint_bottom = top_edge + row_height - half_pen_width if found_left: # Left edge of outline painter.drawLine(paint_left, paint_top, paint_left, paint_bottom) if found_right: # Right edge of outline painter.drawLine(right, paint_top, right, paint_bottom) # Top and bottom of outline painter.drawLine(paint_left, paint_top, right, paint_top) painter.drawLine(paint_left, paint_bottom, right, paint_bottom) cell_idx += 1 def _paintConstraintOutline(self, painter, per_cell_data, left_edges, top_edge, col_width, row_height): """ Paint rect around a partial pairwise constraint """ for left, data in zip(left_edges, per_cell_data): outline = data.get(CustomRole.PartialPairwiseConstraint) if not outline: continue painter.setBrush(Qt.NoBrush) painter.setPen(self._constraint_outline_pen) rect = QtCore.QRect(left, top_edge, col_width, row_height) painter.drawRect(rect) break def _paintChimera(self, painter, per_cell_data, left_edges, top_edge, col_width, row_height): """ Paint standard highlight on chimera regions for structured residues and custom highlight for structureless residues """ # Precompute fake roles to avoid performance penalty in `_paintOverlay` STRUCTURED_COMPOSITE_ROLE = 1 STRUCTURELESS_COMPOSITE_ROLE = 2 chimera_data = [] for data in per_cell_data: composite = data.get(CustomRole.HMCompositeRegion) seqres_only = data.get(CustomRole.SeqresOnly) chimera_data.append({ STRUCTURED_COMPOSITE_ROLE: composite and not seqres_only, STRUCTURELESS_COMPOSITE_ROLE: composite and seqres_only }) paint_args = (painter, chimera_data, left_edges, top_edge, col_width, row_height) self._paintOverlay(*paint_args, STRUCTURED_COMPOSITE_ROLE, self._chimera_brush) self._paintOverlay(*paint_args, STRUCTURELESS_COMPOSITE_ROLE, self._chimera_seqres_only_brush) def _paintIBars(self, painter, per_cell_data, left_edges, top_edge, col_width, row_height): """ Paint I-bars to the left of any selected blocks. See `AbstractDelegate._paintBackground` for argument documentation. """ if self._ibar_path is None: self._ibar_path = self._generateIbarPath(col_width, row_height) painter.setBrush(self._ibar_brush) painter.setPen(Qt.NoPen) for left, data in zip(left_edges, per_cell_data): block_start = data.get(CustomRole.ResSelectionBlockStart) if block_start == ResSelectionBlockStart.Before: path = self._ibar_path.translated(left, top_edge) painter.drawPath(path) last_block_start = per_cell_data[-1].get( CustomRole.ResSelectionBlockStart) if last_block_start == ResSelectionBlockStart.After: right = left_edges[-1] + col_width path = self._ibar_path.translated(right, top_edge) painter.drawPath(path) def _generateIbarPath(self, col_width, row_height): """ Create a QPainterPath for an I-bar for cells of the specified size. :param col_width: The width of each column in pixels. :type col_width: int :param row_height: The height of the row in pixels. :type row_height: int :return: The newly created I-bar path. :rtype: QtGui.QPainterPath """ # The thickness of the I-bar in pixels. This must be an even number. THICKNESS = 2 # the width of the joins in pixels (i.e. the slight curves where the # horizontal bars meets the vertical bar) JOIN_WIDTH = 1 # half the width of the top and bottom bars half_width = col_width // 3 half_thickness = THICKNESS // 2 half_plus_join = half_thickness + JOIN_WIDTH path = QtGui.QPainterPath() # The top bar path.addRect(-half_width, 1, half_width - half_plus_join, THICKNESS) path.addRect(half_plus_join, 1, half_width - half_plus_join, THICKNESS) # the joins between the top bar and the vertical bar path.addRect(-half_plus_join, 2, JOIN_WIDTH, THICKNESS) path.addRect(half_thickness, 2, JOIN_WIDTH, THICKNESS) # the vertical bar path.addRect(-half_thickness, 3, THICKNESS, row_height - 6) # the joins between the bottom bar and the vertical bar path.addRect(-half_plus_join, row_height - 2 - THICKNESS, JOIN_WIDTH, THICKNESS) path.addRect(half_thickness, row_height - 2 - THICKNESS, JOIN_WIDTH, THICKNESS) # the bottom bar path.addRect(-half_width, row_height - 1 - THICKNESS, half_width - half_plus_join, THICKNESS) path.addRect(half_plus_join, row_height - 1 - THICKNESS, half_width - half_plus_join, THICKNESS) return path def _paintChainDividers(self, painter, per_cell_data, left_edges, top_edge, col_width, row_height): """ Paint the chain divider indicator in between chains (only has an effect when "Split chain view" is disabled). See `AbstractDelegate._paintBackground` for argument documentation. """ painter.setPen(Qt.NoPen) for left, data in zip(left_edges, per_cell_data): cur_color = data.get(CustomRole.ChainDivider) if cur_color is None: continue if self._chain_divider_path is None: self._chain_divider_path = self._generateChainDividerPath( col_width, row_height) self._chain_divider_brush.setColor(cur_color) painter.setBrush(self._chain_divider_brush) path = self._chain_divider_path.translated(left, top_edge) painter.drawPath(path) def _generateChainDividerPath(self, col_width, row_height): """ Create a QPainterPath for the chain divider indicator for cells of the specified size. :param col_width: The width of each column in pixels. :type col_width: int :param row_height: The height of the row in pixels. :type row_height: int :return: The newly created chain divider indicator path. :rtype: QtGui.QPainterPath """ THICKNESS = 3 path = QtGui.QPainterPath() # the vertical line path.addRect(-1, -1, THICKNESS, row_height + 1) # the horizontal line path.addRect(THICKNESS - 1, -1, col_width * 3 // 4 - THICKNESS, THICKNESS) return path
[docs]class ConsensusResidueDelegate(AbstractResidueDelegate): ANNOTATION_TYPE = PROT_ALIGN_ANN_TYPES.consensus_seq ROW_HEIGHT_SCALE = 1.5
[docs] def paintRow(self, painter, per_cell_data, per_row_data, row_rect, left_edges, top_edge, col_width, row_height): self._paintBackground(painter, per_cell_data, left_edges, top_edge, col_width, row_height) self._paintText(painter, per_cell_data, per_row_data, left_edges, top_edge, col_width, row_height) self._paintNonstandard(painter, per_cell_data, left_edges, top_edge, col_width, row_height)
[docs]class SpacerDelegate(AbstractDelegate): """ A delegate for blank spacer rows. """ ANNOTATION_TYPE = RowType.Spacer
[docs] def paintRow(self, *args): # This method intentionally left blank pass
[docs]class AbstractBracketDelegate(AbstractDelegate): """ An abstract delegate for delegates that need to paint brackets (e.g. to represent a bond between two residues) :cvar CapStyle: An enum describing whether the bracket should be painted with its end caps flush `(|_____|)` or centered `(|-----|)` :vartype CapStyle: Enum """ CapStyle = Enum('CapStyle', 'FLUSH CENTERED') def _paintBracket(self, left_x, top_y, width, height, painter, cap_style=CapStyle.FLUSH): """ Paint a bracket. :param left_x: The left coordinate of the bracket where the left cap will be drawn. :type left_x: int :param top_y: The top coordinate of the bracket. The bracket will not extend beyond this level. :type top_y: int :param width: The desired width of the bracket. :type width: int :param height: The desired height of the bracket. :type height: int :param painter: The painter to paint the bracket with. :type painter: QtGui.QPainter :param cap_style: How to style the ends of the bracket. See class documentation for description of types of caps. :type cap_style: AbstractBracketDelegate.CapStyle """ bottom_y, right_x = top_y + height, left_x + width # Paint vertical line on the left side of the bracket start_point = (left_x, top_y) end_point = (left_x, bottom_y) painter.drawLine(start_point[0], start_point[1], *end_point) start_point = (right_x, top_y) end_point = (right_x, bottom_y) painter.drawLine(start_point[0], start_point[1], *end_point) # Paint the horizontal line connecting the ends of the bracket if cap_style is self.CapStyle.FLUSH: start_point = (left_x, bottom_y) end_point = (right_x, bottom_y) else: mid_y = top_y + height // 2 start_point = (left_x, mid_y) end_point = (right_x, mid_y) painter.drawLine(start_point[0], start_point[1], *end_point)
[docs]class AntibodyCDRDelegate(AbstractBracketDelegate, AbstractDelegateWithTextCache): """ A delegate to draw CDR annotations. CDR annotations are drawn using a bracket surrounding the relevant residues with the CDR label drawn at the center of the bracket. For example:: 1cmy CTCGACCG CDR |----| H1 """ ANNOTATION_TYPE = PROT_SEQ_ANN_TYPES.antibody_cdr PER_CELL_PAINT_ROLES = frozenset( (CustomRole.ResidueIndex, CustomRole.ResSelected)) ROW_HEIGHT_SCALE = 1.4 # Give enough space to draw CDR labels in OK size
[docs] def __init__(self): super().__init__() self.color_scheme = color.AntibodyCDRScheme() self._font = None
[docs] def paintRow(self, painter, per_cell_data, per_row_data, row_rect, left_edges, top_y, col_width, row_height): self._popPaddingCells(per_cell_data) if not per_cell_data: return start_col_idx = per_cell_data[0][CustomRole.ResidueIndex] end_col_idx = per_cell_data[-1][CustomRole.ResidueIndex] bracket_thickness = row_height // 5 bottom_of_bracket = top_y + bracket_thickness # Set up parameters for CDR labels # Allocate 20% of the space towards padding between the bracket & label # so that spacing is increased when larger fonts are used. remaining_height = row_height - bracket_thickness padding = remaining_height // 5 label_top = bottom_of_bracket + padding if self._font_metrics is None: # Determine how high the CDR label should be (and font size to use) # for it to fit the remaining space: font = QtGui.QFont(per_row_data[Qt.FontRole]) font_height = QtGui.QFontMetrics(font).ascent() label_height = remaining_height - padding vscale = label_height / font_height font.setPointSizeF(font.pointSizeF() * vscale) self._font = font self._font_metrics = QtGui.QFontMetrics(font) painter.setFont(self._font) for cdr in per_row_data[Qt.DisplayRole]: # If the region is out of view, move on if cdr.end < start_col_idx or cdr.start > end_col_idx: continue pen = QtGui.QPen(self.color_scheme.getBrushByKey(cdr.label), bracket_thickness) painter.setPen(pen) left_x = left_edges[0] + col_width * (cdr.start - start_col_idx) right_x = (left_edges[0] + col_width) + col_width * (cdr.end - start_col_idx) self._paintBracket( left_x=left_x + bracket_thickness // 2, top_y=top_y, width=(right_x - left_x) - bracket_thickness, height=bracket_thickness * 3, painter=painter, cap_style=self.CapStyle.CENTERED) # yapf: disable # Create the CDR label in the center of the region if the center # is in view middle_idx = (cdr.end + cdr.start) // 2 if start_col_idx <= middle_idx <= end_col_idx: label_left = left_edges[middle_idx - start_col_idx] painter.setPen(Qt.white) static_text, x_offset = self._static_texts[cdr.label.name, col_width] # x_offset is used to center the text since drawStatictext # doesn't take text option flags painter.drawStaticText(label_left + x_offset, label_top, static_text) # Paint the selection self._paintOverlay(painter, per_cell_data, left_edges, top_y, col_width, row_height, CustomRole.ResSelected, self._sel_brush)
[docs]class DisulfideBondsDelegate(AbstractBracketDelegate): """ A delegate to draw disulfide bonds. Bonds are drawn with a connecting bracket like so:: 1cmy CTCGACCG ss-bond |____| If there are multiple bonds, they are drawn at staggered heights. """ ANNOTATION_TYPE = ( PROT_SEQ_ANN_TYPES.disulfide_bonds, PROT_SEQ_ANN_TYPES.proximity_constraints, ) PER_CELL_PAINT_ROLES = frozenset( (CustomRole.ResidueIndex, CustomRole.ResSelected)) ROW_HEIGHT_CONSTANT = SINGLE_ANNOTATION_HEIGHT
[docs] def paintRow(self, painter, per_cell_data, per_row_data, row_rect, left_edges, top_y, col_width, row_height): self._popPaddingCells(per_cell_data) if not per_cell_data: return pen_color = per_row_data[Qt.BackgroundRole].color() self._setPen(pen_color, painter) self._paintBonds(painter, per_cell_data, per_row_data, row_rect, left_edges, top_y, col_width, row_height) # Paint the selection self._paintOverlay(painter, per_cell_data, left_edges, top_y, col_width, row_height, CustomRole.ResSelected, self._sel_brush)
def _setPen(self, pen_color, painter): painter.setPen(QtGui.QPen(pen_color, 1.5)) def _paintBonds(self, painter, per_cell_data, per_row_data, row_rect, left_edges, top_y, col_width, row_height): ss_bonds = per_row_data[Qt.DisplayRole] start_col_idx = per_cell_data[0][CustomRole.ResidueIndex] end_col_idx = per_cell_data[-1][CustomRole.ResidueIndex] max_height = row_height min_height = row_height // 4 height_range = max_height - min_height if len(ss_bonds) > 1: height_incr = height_range // (len(ss_bonds) - 1) else: height_incr = 0 for bond_idx, (res_idx1, res_idx2) in enumerate(ss_bonds): # If the bond is out of view, move on if res_idx2 < start_col_idx or res_idx1 > end_col_idx: continue left_x = left_edges[0] + col_width // 2 + col_width * ( res_idx1 - start_col_idx) bracket_width = col_width * (res_idx2 - res_idx1) self._paintBracket(left_x, top_y, bracket_width, min_height + (height_incr * bond_idx), painter)
[docs]class PredictedDisulfideBondsDelegate(DisulfideBondsDelegate): ANNOTATION_TYPE = (PROT_SEQ_ANN_TYPES.pred_disulfide_bonds,) def _setPen(self, pen_color, painter): pen = QtGui.QPen(pen_color, 1.5) pen.setStyle(Qt.DashLine) painter.setPen(pen)
[docs]class RulerDelegate(AbstractDelegateWithTextCache): """ A delegate to draw the ruler. Numbers are drawn above the ruler at intervals of 10 with a long tick. Medium ticks are drawn at intervals of 5. Other ticks are very short. """ ANNOTATION_TYPE = PROT_ALIGN_ANN_TYPES.indices PER_CELL_PAINT_ROLES = frozenset( (Qt.DisplayRole, CustomRole.IsAnchoredColumnRangeEnd)) ROW_HEIGHT_CONSTANT = RULER_HEIGHT
[docs] def __init__(self): super(RulerDelegate, self).__init__() self._font = None self._font_height = None self.setLightMode(False)
[docs] def setLightMode(self, enabled): """ Set light mode on the ruler delegate. The anchor icon is drawn with a darker anchor when in light mode """ if enabled: self.anchor_icon = QtGui.QIcon(":/msv/icons/anchor-light.png") else: self.anchor_icon = QtGui.QIcon(":/msv/icons/anchor.png")
[docs] def clearCache(self): super().clearCache() self._font = None self._font_height = None
[docs] def paintRow(self, painter, per_cell_data, per_row_data, row_rect, left_edges, top_edge, col_width, row_height): if self._font is None: self._font = QtGui.QFont(per_row_data[Qt.FontRole]) fm = QtGui.QFontMetrics(self._font) max_width = fm.width("9999") + 2 # adds a little extra room column_width = fm.width(RES_FONT_TEXT) self._font.setPointSizeF(self._font.pointSizeF() * column_width / max_width) self._font_metrics = QtGui.QFontMetrics(self._font) self._font_height = self._font_metrics.height() brush_color = per_row_data[Qt.BackgroundRole] painter.setBrush(brush_color) painter.setPen(Qt.NoPen) painter.drawRect(row_rect) pen_color = per_row_data[Qt.ForegroundRole] painter.setPen(pen_color) painter.setFont(self._font) half_width = col_width // 2 bottom = int(top_edge + row_height - 1) major_tick_y = int(bottom - 0.6 * self._font_height) minor_tick_y = int(bottom - 0.3 * self._font_height) for left, data in zip(left_edges, per_cell_data): num = data.get(Qt.DisplayRole) if num is None: # we're past the end of the alignment continue elif data[CustomRole.IsAnchoredColumnRangeEnd]: topleft = (left, top_edge) dimensions = (col_width, row_height) self.anchor_icon.paint(painter, *topleft, *dimensions) elif num % 10 == 0: mid = left + half_width painter.drawLine(mid, bottom, mid, major_tick_y) text = str(num) static_text, x = self._static_texts[text, col_width] painter.drawStaticText(left + x, top_edge, static_text) elif num % 5 == 0: mid = left + half_width painter.drawLine(mid, bottom, mid, minor_tick_y) else: painter.drawPoint(left + half_width, bottom)
[docs]class ColorBlockDelegate(AbstractDelegate): """ A delegate that paints only colored blocks using the `Qt.BackgroundRole` color. """ ANNOTATION_TYPE = ( PROT_SEQ_ANN_TYPES.helix_propensity, PROT_SEQ_ANN_TYPES.beta_strand_propensity, PROT_SEQ_ANN_TYPES.turn_propensity, PROT_SEQ_ANN_TYPES.helix_termination_tendency, PROT_SEQ_ANN_TYPES.exposure_tendency, PROT_SEQ_ANN_TYPES.steric_group, PROT_SEQ_ANN_TYPES.side_chain_chem, PROT_SEQ_ANN_TYPES.domains, PROT_SEQ_ANN_TYPES.kinase_features, PROT_SEQ_ANN_TYPES.kinase_conservation, ) PER_CELL_PAINT_ROLES = frozenset( (Qt.BackgroundRole, CustomRole.ResSelected))
[docs] def paintRow(self, painter, per_cell_data, per_row_data, row_rect, left_edges, top_edge, col_width, row_height): self._paintBackground(painter, per_cell_data, left_edges, top_edge, col_width, row_height) # paint the selection self._paintOverlay(painter, per_cell_data, left_edges, top_edge, col_width, row_height, CustomRole.ResSelected, self._sel_brush)
[docs]class BindingSiteDelegate(ColorBlockDelegate): """ A delegate for binding site rows. """ ANNOTATION_TYPE = (PROT_SEQ_ANN_TYPES.binding_sites,) CONSTRAINTS_ROLES = frozenset({CustomRole.BindingSiteConstraint})
[docs] def __init__(self): super().__init__() self._show_constraints = False constraint_color = QtGui.QColor(color.BINDING_SITE_PICK_HEX) self._constraint_pen = QtGui.QPen(constraint_color) self._constraint_brush = QtGui.QBrush(constraint_color, Qt.FDiagPattern)
[docs] def setConstraintsShown(self, enable): """ Enable or disable constraints. This affects whether constraints are painted or not. :param enable: Whether to display constraints :type enable: bool """ self._show_constraints = enable if enable: self.PER_CELL_PAINT_ROLES |= self.CONSTRAINTS_ROLES else: self.PER_CELL_PAINT_ROLES -= self.CONSTRAINTS_ROLES
[docs] def paintRow(self, painter, per_cell_data, per_row_data, row_rect, left_edges, top_edge, col_width, row_height): super().paintRow(painter, per_cell_data, per_row_data, row_rect, left_edges, top_edge, col_width, row_height) if self._show_constraints: self._paintConstraintOutline(painter, per_cell_data, left_edges, top_edge, col_width, row_height)
def _paintConstraintOutline(self, painter, per_cell_data, left_edges, top_edge, col_width, row_height): """ Paint rect around a partial pairwise constraint """ painter.setBrush(self._constraint_brush) painter.setPen(self._constraint_pen) for left, data in zip(left_edges, per_cell_data): outline = data.get(CustomRole.BindingSiteConstraint) if not outline: continue rect = QtCore.QRect(left, top_edge, col_width, row_height) painter.drawRect(rect)
[docs]class StripedColorBlockDelegate(ColorBlockDelegate): PER_CELL_PAINT_ROLES = frozenset( (Qt.BackgroundRole, CustomRole.ResSelected)) ANNOTATION_TYPE = (PROT_SEQ_ANN_TYPES.pred_accessibility, PROT_SEQ_ANN_TYPES.pred_disordered, PROT_SEQ_ANN_TYPES.pred_domain_arr)
[docs] def paintRow(self, painter, per_cell_data, per_row_data, row_rect, left_edges, top_edge, col_width, row_height): self._paintBackground(painter, per_cell_data, left_edges, top_edge, col_width, row_height) # paint diagonal stripes over the background color self._paintOverlay(painter, per_cell_data, left_edges, top_edge, col_width, row_height, Qt.BackgroundRole, Qt.BDiagPattern) # paint the selection self._paintOverlay(painter, per_cell_data, left_edges, top_edge, col_width, row_height, CustomRole.ResSelected, self._sel_brush)
[docs]class BarDelegate(AbstractDelegate): """ A delegate for bar charts with only positive values. """ ANNOTATION_TYPE = (PROT_ALIGN_ANN_TYPES.mean_isoelectric_point, PROT_SEQ_ANN_TYPES.window_isoelectric_point, PROT_SEQ_ANN_TYPES.b_factor, PROT_ALIGN_ANN_TYPES.consensus_freq) PER_CELL_PAINT_ROLES = frozenset((Qt.DisplayRole, CustomRole.ResSelected)) ROW_HEIGHT_CONSTANT = DOUBLE_ANNOTATION_HEIGHT
[docs] def paintRow(self, painter, per_cell_data, per_row_data, row_rect, left_edges, top_edge, col_width, row_height): min_val, max_val = per_row_data[CustomRole.DataRange] val_range = max_val - min_val if val_range < 0.00001: # There's no plotable data for this sequence return scaling = row_height / val_range values = ( scaling * (data.get(Qt.DisplayRole) or 0) for data in per_cell_data) painter.setPen(Qt.NoPen) # TODO: use ForegroundRole instead of BackgroundRole since we're # painting the foreground brush = per_row_data.get(Qt.BackgroundRole) painter.setBrush(brush) bottom = top_edge + row_height rect = QtCore.QRectF(0, top_edge, col_width - 1, row_height) for cur_value, left in zip(values, left_edges): rect.moveLeft(left) rect.setTop(bottom - cur_value) painter.drawRect(rect) # paint the selection self._paintOverlay(painter, per_cell_data, left_edges, top_edge, col_width, row_height, CustomRole.ResSelected, self._sel_brush)
[docs]class SSADelegate(AbstractDelegate): """ A delegate for painting secondary structures. Alpha helixes are painted as rectangles with ellipses on the ends and beta strands are painted as arrows. """ ANNOTATION_TYPE = (PROT_SEQ_ANN_TYPES.secondary_structure,) PER_CELL_PAINT_ROLES = frozenset( (CustomRole.ResidueIndex, Qt.BackgroundRole, CustomRole.ResSelected)) ROW_HEIGHT_CONSTANT = SINGLE_ANNOTATION_HEIGHT
[docs] def __init__(self): super(SSADelegate, self).__init__() # We use a cached painter path to paint arrows so we don't have to # reconstruct it every time. self._arrow_path = None
[docs] def clearCache(self): self._arrow_path = None
[docs] def paintRow(self, painter, per_cell_data, per_row_data, row_rect, left_edges, top_edge, col_width, row_height): self._popPaddingCells(per_cell_data) secondary_strucs, gap_idxs = per_row_data[Qt.DisplayRole] ssa_idx = 0 cell_idx = 0 while cell_idx < len(per_cell_data) and ssa_idx < len(secondary_strucs): cell_data = per_cell_data[cell_idx] res_idx = cell_data[CustomRole.ResidueIndex] brush = cell_data[Qt.BackgroundRole] if res_idx in gap_idxs or brush is None: # We don't draw anything for gaps. cell_idx += 1 continue # Skip over secondary structures that come before the current residue while secondary_strucs[ssa_idx].limits[1] < res_idx: ssa_idx += 1 if ssa_idx == len(secondary_strucs): # Residue comes after all secondary structures so we're done return ssa_limits, ssa_type = secondary_strucs[ssa_idx] left_edge = left_edges[cell_idx] # If we're in the middle of a structure, move the index to the end # so we can paint it all at once. if ssa_limits[0] < res_idx < ssa_limits[1]: for next_cell_idx in range(cell_idx + 1, len(per_cell_data)): next_cell_data = per_cell_data[next_cell_idx] next_res_idx = next_cell_data[CustomRole.ResidueIndex] if next_res_idx in gap_idxs or next_res_idx >= ssa_limits[1]: break cell_idx += 1 right_edge = left_edges[cell_idx] + col_width width = right_edge - left_edge if res_idx == ssa_limits[0]: self._paintSSAStartImage(ssa_type, left_edge, top_edge, width, row_height, painter, brush.color()) elif res_idx == ssa_limits[1]: self._paintSSAEndImage(ssa_type, left_edge, top_edge, width, row_height, painter, brush.color()) elif ssa_limits[0] < res_idx < ssa_limits[1]: self._paintSSAMiddleImage(ssa_type, left_edge, top_edge, width, row_height, painter, brush.color()) cell_idx += 1 # paint the selection self._paintOverlay(painter, per_cell_data, left_edges, top_edge, col_width, row_height, CustomRole.ResSelected, self._sel_brush)
def _paintPartiallyBorderedRect(self, left_x, top_y, width, height, painter, brush_color, brush_style=Qt.SolidPattern): """ Paints a rectangle with dark upper and lower borders. :param left_x: Left coordinate of the rectangle to paint :type left_x: int :param top_y: Top coordinate of the rectangle to paint :type top_y: int :param width: Width of the rectangle to paint :type width: int :param height: Height of the rectangle to paint :type height: int :param painter: The painter to use to paint this rectangle :type painter: QtGui.QPainter :param brush_color: The color to paint the rectangle with. :type brush_color: QtGui.QColor :param brush_style: The style of brush to paint the rectangle with. :type brush_style: BrushPattern """ brush = QtGui.QBrush(brush_color, brush_style) painter.setBrush(brush) painter.setPen(Qt.NoPen) painter.drawRect(left_x, top_y, width + 1, height) # Outline the upper and lower border with a darker color. right, bottom = left_x + width, top_y + height painter.setPen(brush_color.darker()) painter.drawLine(left_x, top_y, right, top_y) painter.drawLine(left_x, bottom, right, bottom) def _paintSSAStartImage(self, ssa_type, left_x, top_y, width, height, painter, brush_color): """ Paint an SSA start block for the specified SSA type. :param ssa_type: What type of SSA this is. Should be one of `schrodinger.structure.SS_HELIX`, `schrodinger.structure.SS_STRAND`, or `schrodinger.structure.SS_NONE`. :type ss_type: int :param left_x: Left coordinate of the image to paint :type left_x: int :param top_y: Top coordinate of the image to paint :type top_y: int :param width: Width of the image to paint :type width: int :param height: Height of the image to paint :type height: int :param painter: The painter to draw this annotation :type painter: QtGui.QPainter :param brush_color: The color to paint the rectangle with. :type brush_color: QtGui.QColor """ half_height = height // 2 quarter_height = height // 4 if ssa_type == structure.SS_HELIX: rect_left_x = left_x + quarter_height - 1 rect_top_y = top_y + quarter_height rect_width = width - quarter_height rect_height = half_height self._paintPartiallyBorderedRect(rect_left_x, rect_top_y, rect_width, rect_height, painter, brush_color) painter.setRenderHint(QtGui.QPainter.Antialiasing, True) painter.setBrush(brush_color.lighter()) painter.drawEllipse(left_x, top_y + quarter_height, int(half_height * 0.75), half_height) painter.setRenderHint(QtGui.QPainter.Antialiasing, False) elif ssa_type == structure.SS_STRAND: self._paintPartiallyBorderedRect(left_x, top_y + quarter_height, width, half_height, painter, brush_color) else: self._paintNoSSA(left_x, top_y, height, width, painter, brush_color) def _paintNoSSA(self, left_x, top_y, height, width, painter, pen_color, pen_style=Qt.SolidLine): """ Paint a line representing a residue with no secondary structure. :param left_x: Left coordinate of the image to paint :type left_x: int :param top_y: Top coordinate of the image to paint :type top_y: int :param width: Width of the image to paint :type width: int :param height: Height of the image to paint :type height: int :param painter: The painter to draw this annotation :type painter: QtGui.QPainter :param pen_color: The color to paint the line with. :type pen_color: QtGui.QColor :param pen_style: The style to paint the line with :type pen_style: Qt.PenStyle """ pen = QtGui.QPen(pen_color) pen.setStyle(pen_style) painter.setPen(pen) mid_y = top_y + (height // 2) right = left_x + width start_point = (left_x, mid_y) end_point = (right, mid_y) painter.drawLine(start_point[0], start_point[1], *end_point) def _paintSSAMiddleImage(self, ssa_type, left_x, top_y, width, height, painter, brush_color): """ Paints an SSA middle block for the specified SSA type. :param ssa_type: What type of SSA this is. Should be one of `schrodinger.structure.SS_HELIX`, `schrodinger.structure.SS_STRAND`, or `schrodinger.structure.SS_NONE`. :type ss_type: int :param left_x: Left coordinate of the image to paint :type left_x: int :param top_y: Top coordinate of the image to paint :type top_y: int :param width: Width of the image to paint :type width: int :param height: Height of the image to paint :type height: int :param painter: Painter to paint this image :type painter: QtGui.QPainter :param brush_color: The color to paint the rectangle with. :type brush_color: QtGui.QColor """ if ssa_type == structure.SS_HELIX or ssa_type == structure.SS_STRAND: # Give a quarter height margin above and below the rect quarter_height = height // 4 top_y = top_y + quarter_height height = height - 2 * quarter_height self._paintPartiallyBorderedRect(left_x, top_y, width, height, painter, brush_color) else: self._paintNoSSA(left_x, top_y, height, width, painter, brush_color) def _paintSSAEndImage(self, ssa_type, left_x, top_y, width, height, painter, brush_color): """ Paint an SSA end block for the specified SSA type. :param ssa_type: What type of SSA this is. Should be one of `schrodinger.structure.SS_HELIX`, `schrodinger.structure.SS_STRAND`, or `schrodinger.structure.SS_NONE`. :type ss_type: int :param left_x: left_x coordinate of the image to paint :type left_x: int :param top_y: top_y coordinate of the image to paint :type top_y: int :param width: Width of the image to paint :type width: int :param height: Height of the image to paint :type height: int :param painter: Painter to paint the image. :type painter: QtGui.QPainter :param brush_color: The color to paint the rectangle with. :type brush_color: QtGui.QColor """ bottom_y = top_y + height half_height = height // 2 quarter_height = height // 4 if ssa_type == structure.SS_HELIX: painter.setRenderHint(QtGui.QPainter.Antialiasing, True) # Draw border around ellipse to match partially bordered rect painter.setPen(brush_color.darker()) painter.setBrush(QtGui.QBrush(brush_color)) x = int(left_x + width - half_height) y = top_y + quarter_height painter.drawEllipse(x, y, half_height, half_height) painter.setRenderHint(QtGui.QPainter.Antialiasing, False) painter.setPen(brush_color) # Rect should stop at center of ellipse self._paintPartiallyBorderedRect(left_x, top_y + quarter_height, width - quarter_height, half_height, painter, brush_color) elif ssa_type == structure.SS_STRAND: # Draw an arrow for the end of a strand. self._paintArrow(left_x, top_y, bottom_y, width, half_height, painter, brush_color) else: self._paintNoSSA(left_x, top_y, height, width, painter, brush_color) def _paintArrow(self, left_x, top_y, bottom_y, width, height, painter, brush_color, brush_style=Qt.SolidPattern): """ :param left_x: Left coordinate of the image to paint :type left_x: int :param top_y: Top coordinate of the image to paint :type top_y: int :param width: Width of the image to paint :type width: int :param height: Height of the image to paint :type height: int :param painter: Painter to paint this image :type painter: QtGui.QPainter :param brush_color: The color to paint the arrow with. :type brush_color: QtGui.QColor :param brush_style: The style to paint the arrow with :type brush_style: Qt.BrushStyle """ if self._arrow_path is None: start_point = QtCore.QPoint(left_x, top_y) self._arrow_path = QtGui.QPainterPath(start_point) self._arrow_path.lineTo(left_x + width, bottom_y - height) self._arrow_path.lineTo(left_x, bottom_y) else: path_pos = self._arrow_path.currentPosition() dx, dy = left_x - path_pos.x(), bottom_y - path_pos.y() self._arrow_path.translate(dx, dy) brush = QtGui.QBrush(brush_color, brush_style) painter.setBrush(brush) painter.setPen(brush_color.darker()) painter.drawPath(self._arrow_path)
[docs]class PredSSADelegate(SSADelegate): """ Paint shapes with overlayed stripes and arrows with dashed lines. """ ANNOTATION_TYPE = (PROT_SEQ_ANN_TYPES.pred_secondary_structure,) def _paintPartiallyBorderedRect(self, left_x, top_y, width, height, painter, brush_color): super()._paintPartiallyBorderedRect(left_x, top_y, width, height, painter, brush_color) super()._paintPartiallyBorderedRect(left_x, top_y, width, height, painter, QtGui.QColor(0, 0, 0), brush_style=Qt.BDiagPattern) def _paintNoSSA(self, brush_color, left_x, top_y, height, width, painter): super()._paintNoSSA(brush_color, left_x, top_y, height, width, painter, pen_style=Qt.DashLine) def _paintArrow(self, left_x, top_y, bottom_y, width, half_height, painter, brush_color): super()._paintArrow(left_x, top_y, bottom_y, width, half_height, painter, brush_color) super()._paintArrow(left_x, top_y, bottom_y, width, half_height, painter, QtGui.QColor(0, 0, 0), brush_style=Qt.BDiagPattern)
[docs]class BidirectionalBarDelegate(AbstractDelegate): """ Delegate used for bar charts that represent positive and negative values. Positive values are drawn above the midpoint of the bar and negative values below. """ ANNOTATION_TYPE = (PROT_SEQ_ANN_TYPES.window_hydrophobicity, PROT_ALIGN_ANN_TYPES.mean_hydrophobicity) PER_CELL_PAINT_ROLES = frozenset((Qt.DisplayRole, CustomRole.ResSelected)) ROW_HEIGHT_CONSTANT = DOUBLE_ANNOTATION_HEIGHT
[docs] def paintRow(self, painter, per_cell_data, per_row_data, row_rect, left_edges, top_edge, col_width, row_height): min_val, max_val = per_row_data[CustomRole.DataRange] val_range = max(abs(min_val), abs(max_val)) if val_range < 0.00001: # There's no plotable data for this sequence return half_height = row_height / 2 scaling = half_height / val_range values = ( scaling * (data.get(Qt.DisplayRole) or 0) for data in per_cell_data) painter.setPen(Qt.NoPen) # TODO: use ForegroundRole instead of BackgroundRole since we're # painting the foreground brush = per_row_data.get(Qt.BackgroundRole) painter.setBrush(brush) rect = QtCore.QRectF(0, 0, col_width - 1, 0) mid = top_edge + half_height for cur_value, left in zip(values, left_edges): if cur_value < 0: rect.setTop(mid) rect.setHeight(-cur_value) elif cur_value > 0: rect.setTop(mid - cur_value) rect.setHeight(cur_value) else: # value is exactly zero, so there's nothing to paint continue rect.moveLeft(left) painter.drawRect(rect) # paint the selection self._paintOverlay(painter, per_cell_data, left_edges, top_edge, col_width, row_height, CustomRole.ResSelected, self._sel_brush)
[docs]class PairwiseConstraintDelegate(AbstractDelegate): ANNOTATION_TYPE = PROT_SEQ_ANN_TYPES.pairwise_constraints PER_CELL_PAINT_ROLES = frozenset((CustomRole.ResidueIndex,))
[docs] def paintRow(self, painter, per_cell_data, per_row_data, row_rect, left_edges, top_y, col_width, row_height): self._popPaddingCells(per_cell_data) if not per_cell_data: return constraints = per_row_data[Qt.DisplayRole] if not constraints: return painter.setRenderHint(QtGui.QPainter.Antialiasing, True) pen_color = per_row_data[Qt.BackgroundRole].color() painter.setPen(pen_color) painter.setBrush(Qt.NoBrush) start_col_idx = per_cell_data[0][CustomRole.ResidueIndex] end_col_idx = per_cell_data[-1][CustomRole.ResidueIndex] base_x = left_edges[0] + col_width * (0.5 - start_col_idx) top_y = top_y + 1 bottom_y = top_y + row_height - 1 hline_half_width = min(col_width / 2, 4) for ref_res_idx, other_res_idx in constraints: # If the constraint is out of view, move on res_idx1, res_idx2 = sorted((ref_res_idx, other_res_idx)) if res_idx2 < start_col_idx or res_idx1 > end_col_idx: continue path = QtGui.QPainterPath() ref_x = base_x + col_width * ref_res_idx other_x = base_x + col_width * other_res_idx path.moveTo(ref_x - hline_half_width, top_y) path.lineTo(ref_x + hline_half_width, top_y) path.moveTo(other_x - hline_half_width, bottom_y) path.lineTo(other_x + hline_half_width, bottom_y) path.moveTo(ref_x, top_y) path.cubicTo(ref_x, bottom_y, other_x, top_y, other_x, bottom_y) painter.drawPath(path) painter.setRenderHint(QtGui.QPainter.Antialiasing, False)
[docs]class AlignmentSetDelegate(AbstractDelegateWithTextCache): ANNOTATION_TYPE = PROT_SEQ_ANN_TYPES.alignment_set
[docs] def __init__(self): super().__init__() self._icon_path = None
[docs] def paintRow(self, painter, per_cell_data, per_row_data, row_rect, left_edges, top_edge, col_width, row_height): set_name = per_row_data.get(Qt.DisplayRole) if not set_name: return # TODO: use the same font that the left-hand fixed columns use font = per_row_data[Qt.FontRole] pen_color = per_row_data[Qt.ForegroundRole] painter.setFont(font) painter.setPen(pen_color) if self._y_offset is None: self._populateYOffsetAndFontMetrics(font, row_height) text_y = top_edge + self._y_offset static_text = self._static_texts[set_name] left = row_rect.left() text_x = left + 1.5 * col_width painter.drawStaticText(text_x, text_y, static_text) if self._icon_path is None: self._icon_path = self._generateIconPath(col_width, row_height) path = self._icon_path.translated(left, top_edge) painter.drawPath(path)
def _populateStaticText(self, text): return QtGui.QStaticText(text) def _generateIconPath(self, col_width, row_height): """ Create a QPainterPath for an Alignment Set icon for cells of the specified size. :param col_width: The width of each column in pixels. :type col_width: int :param row_height: The height of the row in pixels. :type row_height: int :return: The newly created I-bar path. :rtype: QtGui.QPainterPath """ BOX_OFFSET = 1 box_size = BOX_OFFSET * 2 fourth_height = row_height // 4 path = QtGui.QPainterPath() for y_multiplier in range(1, 4): y = fourth_height * y_multiplier # Full width horizontal line path.moveTo(0, y) path.lineTo(col_width, y) # Alternating square if y_multiplier % 2: x = BOX_OFFSET * 2 else: x = col_width - box_size - BOX_OFFSET path.addRect(x, y - BOX_OFFSET, box_size, box_size) return path
[docs]class FixedColumnsDelegate(): """ A delegate for the fixed columns on the left and right of the view. Note that this delegate does not inherit from `AbstractDelegate` and that `paintRow` takes different arguments than `AbstractDelegate.paintRow`. This is because this delegate has to paint heterogeneous columns with per-row selection rather than homogeneous columns with per-cell selection. :cvar PAINT_ROLES: Data roles that should be fetched per-cell. Note that this delegate does not fetch any roles per-row. :vartype PAINT_ROLES: frozenset """ PAINT_ROLES = frozenset( (Qt.DisplayRole, Qt.DecorationRole, Qt.FontRole, Qt.TextAlignmentRole, Qt.ForegroundRole, Qt.BackgroundRole, CustomRole.RowType, CustomRole.RowHeightScale, CustomRole.SeqSelected, CustomRole.AnnotationSelected, CustomRole.PreviousRowHidden, CustomRole.NextRowHidden))
[docs] def __init__(self): super().__init__() self._sel_brush = QtGui.QBrush(color.NONREF_SEL_COLOR) self._ref_sel_brush = QtGui.QBrush(color.REF_SEQ_SEL_COLOR) self._ann_sel_brush = QtGui.QBrush(color.ANN_SEL_COLOR) self._special_mouse_over_brush = QtGui.QBrush(color.SPECIAL_HOVER_COLOR) self._hidden_seq_pen = QtGui.QPen(color.HIDDEN_SEQ_COLOR) self._static_texts = {} self._font_metrics = {} self._font_height = {} self._text_widths = {} self.setLightMode(False)
[docs] def setLightMode(self, enabled): """ Set light mode for the delegate. This affects how the first row (header) is drawn """ if enabled: first_row_color = color.HEADER_BACKGROUND_COLOR_LM mouse_over_color = color.HOVER_COLOR_LM mouse_over_factor = color.HOVER_LIGHTER_LM else: first_row_color = color.HEADER_BACKGROUND_COLOR mouse_over_color = color.HOVER_COLOR mouse_over_factor = color.HOVER_LIGHTER self._first_row_brush = QtGui.QBrush(first_row_color) self._mouse_over_brush = QtGui.QBrush(mouse_over_color) self._hovered_sel_brushes = IdDict() for brush in (self._sel_brush, self._ref_sel_brush, self._ann_sel_brush): hovered_color = brush.color().lighter(mouse_over_factor) hovered_brush = QtGui.QBrush(hovered_color) self._hovered_sel_brushes[brush] = hovered_brush
[docs] def clearCache(self): """ Clear any cached data. Must be called whenever the font size changes. """ self._static_texts.clear() self._font_metrics.clear() self._font_height.clear() self._text_widths.clear()
[docs] def paintRow(self, painter, data, is_title_row, selection_rect, row_rect, left_edges, col_widths, top_edge, row_height, is_mouse_over_row, is_mouse_over_struct_col, paint_expansion_column): """ Paint an entire row of data. :param painter: The painter to use for painting. :type painter: QtGui.QPainter :param data: A list of data for the entire row. Each column is represented by a dictionary of {role: value}. Note that the keys of this dictionary are `self.PAINT_ROLES`. :type data: list(dict(int, object)) :param is_title_row: Whether this row is a title row that should get a special background color. :type is_title_row: bool :param selection_rect: A QRect to use for painting the selection highlighting for selected rows. The left and right edges of this rectangle are set correctly, but the top and bottom need to be updated before painting. Note that the left and right edges of this rectangle must not be changed. :type selection_rect: QtCore.QRect :param row_rect: A rectangle that covers the entire area to be painted. :type row_rect: QtCore.QRect :param left_edges: A list of the x-coordinates for the left edges of each column. :type left_edges: list(int) :param col_widths: A list of the widths of each column in pixels. :type col_widths: list(int) :param top_edge: The y-coordinate of the top edge of the row :type top_edge: int :param row_height: The height of the row in pixels. :type row_height: int :param is_mouse_over_row: Whether the mouse is over this row :type is_mouse_over_row: bool :param is_mouse_over_struct_col: Whether the mouse is over the struct column :type is_mouse_over_struct_col: bool :param paint_expansion_column: Whether to paint the expansion column (i.e. whether left_edges[0] represents the leftmost column) :type paint_expansion_column: bool """ first_col_data = data[0] if is_title_row: painter.fillRect(row_rect, self._first_row_brush) else: background_brush = first_col_data.get(Qt.BackgroundRole) if background_brush is not None: painter.fillRect(row_rect, background_brush) row_type = first_col_data.get(CustomRole.RowType) hidden_paint_args = [] if row_type is RowType.Sequence: if paint_expansion_column and first_col_data.get( CustomRole.PreviousRowHidden): x1 = left_edges[0] x2 = x1 + col_widths[0] y = top_edge hidden_paint_args.append([x1, y, x2, y]) sel_brush = None if row_type is RowType.Sequence and first_col_data.get( CustomRole.SeqSelected): if first_col_data.get(CustomRole.ReferenceSequence) is True: sel_brush = self._ref_sel_brush else: sel_brush = self._sel_brush elif first_col_data.get(CustomRole.AnnotationSelected): sel_brush = self._ann_sel_brush if is_mouse_over_row and row_type not in NO_HOVER_ROW_TYPES: if sel_brush is None: sel_brush = self._mouse_over_brush else: # Selected and hovered sel_brush = self._hovered_sel_brushes[sel_brush] if sel_brush is not None: # Paint selection before text selection_rect.setTop(top_edge) selection_rect.setHeight(row_height) painter.fillRect(selection_rect, sel_brush) if (paint_expansion_column and first_col_data.get(CustomRole.NextRowHidden)): x1 = left_edges[0] x2 = x1 + col_widths[0] y = top_edge + row_height - 1 hidden_paint_args.append([x1, y, x2, y]) if hidden_paint_args: painter.setPen(self._hidden_seq_pen) for args in hidden_paint_args: painter.drawLine(*args) if is_mouse_over_row and row_type in SPECIAL_HOVER_ROW_TYPES: painter.fillRect(row_rect, self._special_mouse_over_brush) font_set = False for cur_data, cur_left_edge, cur_width in zip(data, left_edges, col_widths): # display text display_data = cur_data.get(Qt.DisplayRole) if display_data: if not font_set: # wait to set the font until we know we're actually going to # be painting something font = first_col_data.get(Qt.FontRole) painter.setFont(font) text_color = cur_data.get(Qt.ForegroundRole, Qt.white) painter.setPen(text_color) font_set = True text_aln = cur_data.get(Qt.TextAlignmentRole, Qt.AlignCenter) if text_aln == Qt.AlignRight: cur_width = cur_width - RIGHT_ALIGN_PADDING if "\n" in display_data: lines = display_data.splitlines() height_per_line = row_height / len(lines) for i, line in enumerate(lines): line_top = int(top_edge + i * height_per_line) self._drawText(painter, line, text_aln, font, cur_left_edge, line_top, cur_width, height_per_line) else: self._drawText(painter, display_data, text_aln, font, cur_left_edge, top_edge, cur_width, row_height) decoration_data = cur_data.get(Qt.DecorationRole) if decoration_data is not None: rect = QtCore.QRectF(cur_left_edge, top_edge, cur_width, row_height) painter.drawImage(rect, decoration_data) if is_mouse_over_row and is_mouse_over_struct_col: painter.fillRect(rect, self._special_mouse_over_brush)
def _drawText(self, painter, text, text_aln, font, left_edge, top_edge, cell_width, row_height): """ Paint text for a single cell. :param painter: The painter to use for painting. :type painter: QtGui.QPainter :param text: The text to paint :type text: str :param text_aln: The alignment for the text. Note that only horizontal alignment is obeyed; all text is painted with a centered vertical alignment. :type text_aln: QtCore.Qt.AlignmentFlag :param font: The font to use for painting. Note that this font has already been set on the painter. It should only be used to retrieve per-font cached values. :type font: QtGui.QFont :param left_edge: The x-coordinate of the left edge of the column. :type left_edge: int :param top_edge: The y-coordinate of the top edge of the row :type top_edge: int :param cell_width: The width of the column in pixels. :type cell_width: int :param row_height: The height of the row in pixels. :type row_height: int """ font_id = id(font) if font_id in self._font_height: font_height = self._font_height[font_id] else: font_metrics = QtGui.QFontMetrics(font) font_height = font_metrics.height() self._font_metrics[font_id] = font_metrics self._font_height[font_id] = font_height static_text_key = (font_id, text) if static_text_key in self._static_texts: static_text, text_width = self._static_texts[static_text_key] else: fm = self._font_metrics[font_id] text = fm.elidedText(text, Qt.ElideRight, cell_width) static_text = QtGui.QStaticText(text) text_width = fm.width(text) self._static_texts[static_text_key] = static_text, text_width text_y = top_edge + (row_height - font_height) // 2 if text_aln & Qt.AlignLeft: text_x = left_edge elif text_aln & Qt.AlignRight: text_x = left_edge + cell_width - text_width else: # center aligned text_x = left_edge + (cell_width - text_width) // 2 if text_width > cell_width: painter.setClipRect(left_edge, top_edge, cell_width, row_height) painter.drawStaticText(text_x, text_y, static_text) if text_width > cell_width: painter.setClipping(False)