"""
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.width(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)