"""
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)