import bisect
import collections
import copy
import itertools
import math
import types
import typing
from functools import partial
import inflect
from schrodinger.application.msv.gui import color
from schrodinger.application.msv.gui import gui_models
from schrodinger.application.msv.gui import msv_rc
from schrodinger.application.msv.gui import picking
from schrodinger.application.msv.gui.gui_alignment import \
    GlobalAnnotationRowInfo
from schrodinger.application.msv.gui.gui_alignment import \
    SequenceAnnotationRowInfo
from schrodinger.application.msv.gui.viewconstants import DEFAULT_GAP
from schrodinger.application.msv.gui.viewconstants import TERMINAL_GAP
from schrodinger.application.msv.gui.viewconstants import TOP_LEVEL
from schrodinger.application.msv.gui.viewconstants import Adjacent
from schrodinger.application.msv.gui.viewconstants import AnnotationType
from schrodinger.application.msv.gui.viewconstants import ColorByAln
from schrodinger.application.msv.gui.viewconstants import ColumnMode
from schrodinger.application.msv.gui.viewconstants import CustomRole
from schrodinger.application.msv.gui.viewconstants import GroupBy
from schrodinger.application.msv.gui.homology_modeling.constants import HomologyStatus
from schrodinger.application.msv.gui.viewconstants import IdentityDisplayMode
from schrodinger.application.msv.gui.picking import PickMode
from schrodinger.application.msv.gui.viewconstants import ResidueFormat
from schrodinger.application.msv.gui.viewconstants import ResSelectionBlockStart
from schrodinger.application.msv.gui.viewconstants import RoleBase
from schrodinger.application.msv.gui.viewconstants import RowType
from schrodinger.application.msv.gui.viewconstants import SeqInfo
from schrodinger.application.msv.gui.viewconstants import included_map
from schrodinger.protein import annotation
from schrodinger.protein import nonstandard_residues
from schrodinger.protein import residue
from schrodinger.protein import sequence
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt.QtCore import Qt
from schrodinger.ui import sketcher
from schrodinger.ui.qt import structure2d
from schrodinger.ui.qt import table_helper
from schrodinger.ui.qt import table_speed_up
from schrodinger.ui.qt import utils as qt_utils
from schrodinger.ui.qt.standard.colors import get_contrasting_color
from schrodinger.utils.scollections import IdDict
msv_rc = msv_rc  # noqa
MAX_ANNOTATIONS = 50
MAX_DOMAINS = 50
MAX_LIGANDS = 500
MAX_SEQ_PROPS = 500
# Constants to help make the proxy row calculations easier to read:
# When determining the number of rows in a grouping, we need to account for the
# blank spacer row after the group.
SPACER = 1
# When calculating the number of annotation types, we need to account for the
# sequence itself
SEQUENCE = 1
# Adding or subtracting one to account for the fact that rows and columns are
# zero-indexed
ZERO_INDEXED = 1
ALN_ANNO_TYPES = annotation.ProteinAlignmentAnnotations.ANNOTATION_TYPES
SEQ_ANNO_TYPES = annotation.ProteinSequenceAnnotations.ANNOTATION_TYPES
PRED_ANNO_TYPES = annotation.ProteinSequenceAnnotations.PRED_ANNOTATION_TYPES
RES_PROP_ANNO_TYPES = annotation.ProteinSequenceAnnotations.RES_PROPENSITY_ANNOTATIONS
NON_SELECTABLE_ANNO_TYPES = frozenset({
    ALN_ANNO_TYPES.indices,
    SEQ_ANNO_TYPES.pairwise_constraints,
    SEQ_ANNO_TYPES.alignment_set,
    SEQ_ANNO_TYPES.proximity_constraints,
})
SELECTABLE_ANNO_TYPES = frozenset(ALN_ANNO_TYPES).union(
    SEQ_ANNO_TYPES) - NON_SELECTABLE_ANNO_TYPES
# Maximum number of residues to fit in one column of the sequence logo
MAX_AA_IN_LOGO = 5
# Only display residue numbers divisible by this number
RESNUM_INCR = 5
# Whether custom MSV fonts have been loaded to the QFontDatabase
_font_added = False
[docs]class SeqSliceReplacement(typing.NamedTuple):
    """
    Data about replacing a portion of a sequence.  Used to pass information from
    view.EditorDelegate.setModelData to SequenceAlignmentModel._setData.
    :ivar new_residues: The residues to replace the sequence with.
    :ivar num_to_replace: The number of residues to replace.  Note that this is
        not necessarily equal to `len(new_residues)` since the replacement
        doesn't have to be the same length as what its replacing.
    """
    new_residues: str
    num_to_replace: int = 1 
[docs]class CacheNamespace(types.SimpleNamespace):
    """
    A SimpleNamespace for storing all the caches in SequenceAlignmentModel.
    This is so they can be easily cleared all at once.
    """
[docs]    def __init__(self, **kwargs):
        self._cache_names = kwargs.keys()
        super().__init__(**kwargs) 
[docs]    def clear(self):
        for cache in self._cache_names:
            setattr(self, cache, None)  
[docs]class SlotsInPythonMixin:
    """
    If a model connects a signal to a non-slot C++ method, then PyQt won't be
    able to destroy the model until the main event loop runs.  (`ProcessEvents`
    calls aren't sufficient since they don't process DeferredDelete events.)
    This will prevent models from being cleaned up during unit tests, which will
    prevent the panels and alignments from being cleaned up as well.  This can
    lead to massive memory usage, especially for the performance tests and the
    Hypothesis stateful tests.  To avoid this, we override all of the C++
    methods commonly used as slots with Python methods.
    """
[docs]    @QtCore.pyqtSlot(QtCore.QModelIndex, int, int)
    def beginInsertColumns(self, parent, first, last):
        super().beginInsertColumns(parent, first, last) 
[docs]    @QtCore.pyqtSlot(QtCore.QModelIndex, int, int)
    def beginInsertRows(self, parent, first, last):
        super().beginInsertRows(parent, first, last) 
[docs]    @QtCore.pyqtSlot()
    def endInsertColumns(self):
        super().endInsertColumns() 
[docs]    @QtCore.pyqtSlot()
    def endInsertRows(self):
        super().endInsertRows() 
[docs]    @QtCore.pyqtSlot(QtCore.QModelIndex, int, int)
    def beginRemoveColumns(self, parent, first, last):
        super().beginRemoveColumns(parent, first, last) 
[docs]    @QtCore.pyqtSlot(QtCore.QModelIndex, int, int)
    def beginRemoveRows(self, parent, first, last):
        super().beginRemoveRows(parent, first, last) 
[docs]    @QtCore.pyqtSlot()
    def endRemoveColumns(self):
        super().endRemoveColumns() 
[docs]    @QtCore.pyqtSlot()
    def endRemoveRows(self):
        super().endRemoveRows() 
[docs]    @QtCore.pyqtSlot()
    def beginResetModel(self):
        super().beginResetModel() 
[docs]    @QtCore.pyqtSlot()
    def endResetModel(self):
        super().endResetModel()  
[docs]class ModelMixin(SlotsInPythonMixin):
    """
    A mixin for methods shared by both the base model and all proxy models.
    """
    def _invalidateAllPersistentIndices(self):
        """
        Invalidate all persistent indices.  This method should be called before
        emitting a layoutChanged signal.  (Alternatively, the persistent indices
        can be updated, but that would require quite a bit more logic.  Since
        we're storing selection information in the model, persistent indices
        shouldn't be keeping track of anything important, so simply clearing
        them should be sufficient.)
        """
        persistent_indices = self.persistentIndexList()
        all_invalid = [QtCore.QModelIndex()] * len(persistent_indices)
        self.changePersistentIndexList(persistent_indices, all_invalid) 
[docs]class SequenceAlignmentModel(table_speed_up.MultipleRolesRoleModelMixin,
                             ModelMixin, table_helper.RowBasedTableModel):
    """
    A QTable model where each row corresponds to a sequence and each column
    corresponds to a residue position
    :note: If the alignment contains only zero-length sequences, then this model
        creates a dummy column.  Otherwise, we wouldn't be able to generate any
        valid QModelIndex objects, which means we couldn't create any parent
        indices for row insertion signals.
    :ivar fixedColumnDataChanged: Signal emitted when the data in a fixed column
        is changed. Passes a tuple of the role and row index that are changing.
    :vartype fixedColumnDataChanged: `QtCore.pyqtSignal` emitting a tuple of
        (`enum.Enum`, int)
    :ivar rowHeightChanged: Signal emitted when the height of a row is changed
        is changed.
    :vartype rowHeightChanged: `QtCore.pyqtSignal`
    :cvar seqExpansionChanged: A signal emitted when sequence expansion is
        changed.  Emitted with:
            - A list of all indices to be expanded or collapsed.
            - True if the indices should be expanded. False if they should be
              collapsed.
    :vartype seqExpansionChanged: QtCore.pyqtSignal
    """
    domainsChanged = QtCore.pyqtSignal()
    predictionsChanged = QtCore.pyqtSignal()
    secondaryStructureChanged = QtCore.pyqtSignal()
    residueFormatChanged = QtCore.pyqtSignal()
    residueSelectionChanged = QtCore.pyqtSignal()
    fixedColumnDataChanged = QtCore.pyqtSignal(int, int)
    rowHeightChanged = QtCore.pyqtSignal()
    sequencesReordered = QtCore.pyqtSignal(list)
    textSizeChanged = QtCore.pyqtSignal()
    alnSetChanged = QtCore.pyqtSignal()
    kinaseFeaturesChanged = QtCore.pyqtSignal()
    kinaseConservationChanged = QtCore.pyqtSignal()
    hiddenSeqsChanged = QtCore.pyqtSignal(bool)
    seqExpansionChanged = QtCore.pyqtSignal(list, bool)
    sequenceStructureChanged = QtCore.pyqtSignal()
    MIN_ALN_QUALITY_WEIGHT = 0.2
[docs]    def __init__(self, parent=None):
        """
        :param parent: Parent of the row based table model.
        :type parent: `QtCore.Object`
        """
        super().__init__(parent)
        self._page_model = None
        self._options_model = None
        self.consensus_seq = None
        self.aln = None
        self._split_chain_mode = True
        self._split_aln = None
        self._has_anchors = False
        self._column_count = 0
        self._using_dummy_column = False
        self._ignoring_col_change = False
        self._structure_model = None
        self.initializeCustomFonts()
        self.setLightMode(False)
        self._color_scheme = {
            row_type: scheme()
            for row_type, scheme in color.DEFAULT_ROW_COLOR_SCHEMES.items()
        }
        # Fetching a color scheme out of the _color_scheme dictionary is
        # surprisingly slow due to the time it takes to hash an enum, so we
        # cache the sequence row color scheme separately to make painting
        # faster (and remove it from the dict to prevent accidental slow access)
        self._seq_color_scheme = self._color_scheme.pop(RowType.Sequence)
        self._nucleic_color_scheme = color.NucleicAcidColorScheme()
        self._brush_cache = table_speed_up.DataCache(15000)
        self._residue_highlights = dict()
        self._cache = CacheNamespace(average_color=None,
                                     res_matches_ref=None,
                                     resnum=None,
                                     quality_alpha_by_col=None,
                                     anchor_range_ends=None,
                                     anchored_ref_res_col_idxs=None,
                                     res_matches_cons=None)
        # make sure that the caches are cleared whenever the model changes
        self.dataChanged.connect(self._cache.clear)
        self.rowsInserted.connect(self._cache.clear)
        self.rowsRemoved.connect(self._cache.clear)
        self.rowsMoved.connect(self._cache.clear)
        self.columnsInserted.connect(self._cache.clear)
        self.columnsRemoved.connect(self._cache.clear)
        self.columnsMoved.connect(self._cache.clear)
        self.modelReset.connect(self._cache.clear)
        self.layoutChanged.connect(self._cache.clear) 
[docs]    def setStructureModel(self, smodel):
        self._structure_model = smodel 
[docs]    def setPageModel(self, page_model):
        """
        Set the page model, which contains the options model and is also
        responsible for switching between split-chain and combined-chain
        alignments.
        :param page_model: The page model to set
        :type page_model: gui_models.PageModel
        """
        if self._page_model:
            self._page_model.split_chain_viewChanged.disconnect(
                self._setSplitChainMode)
        self._page_model = page_model
        page_model.split_chain_viewChanged.connect(self._setSplitChainMode)
        self._split_chain_mode = page_model.split_chain_view
        self._setOptionsModel(page_model.options) 
    def _setOptionsModel(self, options_model):
        """
        Set the options model for the model, which reports on various display
        options that the user can set through the GUI.
        Accessing `OptionsModel` attributes is slow enough that it affects
        painting speed, so we instead use an `OptionsModelCache` in this class.
        Note that the `OptionsModelCache` instance is read-only and must not be
        used to change options.
        :param options_model: The widget options.
        :type options_model: schrodinger.application.msv.gui.gui_models.
                             OptionsModel
        """
        self._options_model_cache = OptionsModelCache()
        attrs = ('antibody_cdr', 'antibody_cdr_scheme', 'color_by_aln',
                 'average_in_cols', 'weight_by_quality', 'colors_enabled',
                 'include_gaps', 'font_size', 'identity_display', 'res_format',
                 'binding_site_distance', 'compute_for_columns', 'pick_mode')
        if self._options_model is not None:
            # Disconnect old options model
            for attr_name in attrs:
                signal = getattr(self._options_model, f"{attr_name}Changed")
                signal.disconnect()
        self._options_model = options_model
        for attr_name in attrs:
            cur_val = getattr(self._options_model, attr_name)
            setattr(self._options_model_cache, attr_name, cur_val)
            signal = getattr(self._options_model, f"{attr_name}Changed")
            signal.connect(
                partial(setattr, self._options_model_cache, attr_name))
            signal.connect(self._refreshAllData)
        opts = self._options_model
        ss = [
            (opts.font_sizeChanged, self._updateFonts),
            (opts.identity_displayChanged, self.residueFormatChanged),
            (opts.res_formatChanged, self.residueFormatChanged),
            (opts.pick_modeChanged, self._onPickModeChanged),
        ]  # yapf: disable
        for signal, slot in ss:
            signal.connect(slot)
        self._updateFonts()
[docs]    def setLightMode(self, enabled):
        self._no_background_text_color = (color.NO_BACKGROUND_TEXT_COLOR_LM
                                          if enabled else
                                          color.NO_BACKGROUND_TEXT_COLOR) 
    def _genDataArgs(self, index):
        # See table_speed_up documentation for method documentation.
        # Note that the argument list in rowData must be updated to reflect any'
        # changes made here.
        row = index.row()
        seq = self._rows[row]
        col = index.column()
        res = seq[col] if col < len(seq) else None
        return [res, seq, row, col]
[docs]    def rowData(self, row, cols, roles):
        """
        Fetch data for multiple roles for multiple indices in the same row. Note
        that this method does minimal sanity checking of its input for
        performance reasons, as it is called during painting.  The arguments are
        assumed to refer to valid indices.  Use `data` instead if more sanity
        checking is required.
        :param row: The row number to fetch data for.
        :type row: int
        :param cols: A list of columns to fetch data for.
        :type cols: list(int)
        :param roles: A list of roles to fetch data for.
        :type roles: list(int)
        :return: {role: data} dictionaries for each requested column.
        :rtype: list(dict(int, object))
        """
        seq = self._rows[row]
        all_data = []
        for cur_col in cols:
            res = seq[cur_col] if cur_col < len(seq) else None
            cur_data = {}
            data_args = (res, seq, row, cur_col)
            self._fetchMultipleRoles(cur_data, roles, *data_args)
            all_data.append(cur_data)
        return all_data 
    def _refreshAllData(self):
        """
        We emit dataChanged so that the view will update the contents of the
        residue cells
        """
        invalid_index = QtCore.QModelIndex()
        self.dataChanged.emit(invalid_index, invalid_index)
[docs]    def getResidueDisplayMode(self):
        """
        :rtype: ResidueFormat
        :return: The residue display mode in current use
        """
        return self._options_model_cache.res_format 
[docs]    @table_helper.model_reset_method
    def setAlignment(self, aln):
        """
        Set the alignment model to display data from
        :param aln: The alignment model
        :type aln: gui_alignment.GuiProteinAlignment
        """
        # As a performance optimization, we save a flag for whether or
        # not there are any residue anchors at all.
        self._has_anchors = bool(aln.getAnchoredResidues())
        self._split_aln = aln
        self._updateSplitChainMode(aln)
        self._resetColumnCount() 
    def _updateSplitChainMode(self, aln=None):
        """
        Update self._rows and self.aln based on the current split chain view
        setting.  Note that self._split_aln always refers to the split chain
        alignment, while self.aln and self._rows always refer to the alignment
        for the current split-chain setting.
        :param aln: The split-chain or combined-chain alignment to set.  If not
            given, the current alignment will be fetched from the page model.
        :type aln: gui_alignment.GuiProteinAlignment or
            gui_alignment.GuiCombinedChainProteinAlignment or None
        """
        if aln is None:
            aln = self._page_model.aln
        for signal, slot in self._getAlnSignalsAndSlots(self.aln):
            signal.disconnect(slot)
        # We can't call self.loadData() because that would emit the signals for
        # a model reset before we change self._column_count.  Instead we
        # manually assign aln to self._rows.  We also allow self.aln as an
        # alias.
        self.aln = self._rows = aln
        for signal, slot in self._getAlnSignalsAndSlots(self.aln):
            signal.connect(slot)
        # Sync initial state
        self.onResHighlightChanged()
    def _resetColumnCount(self):
        self._column_count = self.aln.num_columns
        if self._column_count == 0 and len(self.aln) > 0:
            self._using_dummy_column = True
            self._column_count = 1
    def _setSplitChainMode(self, enable):
        """
        Change the split chain view setting.  This method should not be called
        directly.  Instead, OptionsModel.split_chain_view should be toggled,
        which will trigger a call to this method.
        :param enable: Whether to enable or disable split chain view.
        :type enable: bool
        """
        if enable == self._split_chain_mode:
            return
        self._split_chain_mode = enable
        if self._split_aln is None:
            return
        with self.modelResetContext():
            self._updateSplitChainMode()
            self._resetColumnCount()
    def _getAlnSignalsAndSlots(self, aln):
        """
        :return: pairs of (aln signal, slot)
        :rtype: tuple(tuple)
        """
        if aln is None:
            return ()
        signals = aln.signals
        ss = (
            (signals.sequencesAboutToBeInserted, self._sequencesAboutToBeInserted),
            (signals.sequencesInserted, self.endInsertRows),
            (signals.sequencesAboutToBeRemoved, self._sequencesAboutToBeRemoved),
            (signals.sequencesRemoved, self._sequencesRemoved),
            (signals.sequencesAboutToBeReordered, self.beginLayoutChange),
            (signals.sequencesReordered, self.endLayoutChange),
            (signals.sequencesReordered, self.sequencesReordered),
            (signals.alignmentAboutToBeCleared, self.beginResetModel),
            (signals.alignmentCleared, self.endResetModel),
            (signals.alignmentNumColumnsChanged, self._alignmentNumColumnsChanged),
            (signals.alignmentNumColumnsAboutToChange, self._alignmentNumColumnsAboutToChange),
            (signals.sequenceResiduesChanged, self._sequenceResiduesChanged),
            (signals.anchoredResiduesChanged, self._onAnchoredResiduesChanged),
            (signals.sequenceNameChanged, self._sequenceNameChanged),
            (signals.annotationTitleChanged, self._annotationTitleChanged),
            (signals.sequenceVisibilityChanged, self._seqVisChanged),
            (signals.sequenceStructureChanged, self._sequenceStructureChanged),
            (signals.alnSetChanged, self._onAlnSetChanged),
            (signals.predictionsChanged, self._onPredictionsChanged),
            (signals.secondaryStructureChanged, self._onSecondaryStructuresChanged),
            (signals.domainsChanged, self._onDomainsChanged),
            (signals.invalidatedDomains, self._onDomainsChanged),
            (signals.pfamChanged, self._onPfamChanged),
            (signals.kinaseFeaturesChanged, self._onKinaseFeaturesChanged),
            (signals.kinaseConservationChanged, self._onKinaseConservationChanged),
            (aln.res_selection_model.selectionChanged, self._onResidueSelectionChanged),
            (aln.seq_selection_model.selectionChanged, self._refreshAllData),
            (aln.ann_selection_model.selectionChanged, self._refreshAllData),
            (signals.resHighlightStatusChanged, self.onResHighlightChanged),
            (signals.resOutlineStatusChanged, self._refreshAllData),
            (signals.homologyStatusChanged, self.onHomologyStatusChanged),
            (signals.homologyCompositeResiduesChanged, self._refreshAllData),
            (signals.homologyLigandConstraintsChanged, self._refreshAllData),
            (signals.homologyProximityConstraintsChanged, self._refreshAllData),
            (signals.pairwiseConstraintsChanged, self.onPairwiseConstraintsChanged),
            (signals.hiddenSeqsChanged, self.hiddenSeqsChanged),
            (signals.seqExpansionChanged, self._onSeqExpansionChanged),
        )  # yapf: disable
        return ss
    def _onResidueSelectionChanged(self, added, removed):
        self.residueSelectionChanged.emit()
        # The fixed columns only need to update if we're computing selected
        # columns
        if self._options_model_cache.compute_for_columns is ColumnMode.AllColumns:
            return
        changed_residues = added | removed
        changed_seq_indices = set()
        for res in changed_residues:
            changed_seq_indices.add(self.aln.index(res.sequence))
        roles = (CustomRole.AlignmentIdentity, CustomRole.AlignmentSimilarity,
                 CustomRole.AlignmentConservation, CustomRole.AlignmentScore)
        for seq_index, role in itertools.product(changed_seq_indices, roles):
            self.fixedColumnDataChanged.emit(role, seq_index)
    def _onAnchoredResiduesChanged(self):
        self._has_anchors = bool(self.aln.getAnchoredResidues())
        self._refreshAllData()
    def _onAlnSetChanged(self):
        self.alnSetChanged.emit()
        self.rowHeightChanged.emit()
        self._refreshAllData()
    def _onPfamChanged(self):
        self.rowHeightChanged.emit()
        self._refreshAllData()
    def _onKinaseFeaturesChanged(self):
        self.rowHeightChanged.emit()
        self.kinaseFeaturesChanged.emit()
        self._refreshAllData()
    def _onKinaseConservationChanged(self):
        self.rowHeightChanged.emit()
        self.kinaseConservationChanged.emit()
        self._refreshAllData()
    def _onSecondaryStructuresChanged(self):
        self.rowHeightChanged.emit()
        self.secondaryStructureChanged.emit()
        self._refreshAllData()
    def _onPredictionsChanged(self):
        self.rowHeightChanged.emit()
        self.predictionsChanged.emit()
        self._refreshAllData()
    def _onDomainsChanged(self):
        self.rowHeightChanged.emit()
        self.domainsChanged.emit()
        self._refreshAllData()
[docs]    def onPairwiseConstraintsChanged(self):
        self._refreshAllData() 
    def _onSeqExpansionChanged(self, seq, expanded):
        seq_index = self.aln.index(seq)
        model_index = self.index(seq_index, 0)
        self.seqExpansionChanged.emit([model_index], expanded)
        if self._nextRowHidden(None, seq):
            # If the next row is hidden, then we need to notify the view that it
            # may need to change where the hidden sequence marker is drawn
            self.fixedColumnDataChanged.emit(CustomRole.NextRowHidden,
                                             seq_index)
[docs]    def getAlignment(self):
        """
        Return the underlying alignment object
        :return: The alignment
        :rtype: schrodinger.protein.alignment.BaseAlignment
        """
        return self.aln 
[docs]    def sequenceCount(self):
        """
        :rtype: int
        :return: The number of sequences in the alignment
        """
        return self.rowCount() 
    def _seqVisChanged(self, seq, seq_index):
        """
        Notify listeners that sequence visibility has changed.
        :param seq: The sequence that changed visibility
        :type seq: `sequence.Sequence`
        :param seq_index: Index of the sequence that changed visibility.
        :type seq_index: int
        """
        self.fixedColumnDataChanged.emit(CustomRole.Included, seq_index)
[docs]    def beginLayoutChange(self):
        """
        Emit a layoutAboutToBeChanged signal.
        This helper makes disconnecting from the signal easier.
        """
        self.layoutAboutToBeChanged.emit() 
[docs]    def endLayoutChange(self):
        """
        Finish a layout change operation by clearing all persistent indices and
        emitting layoutChanged.
        """
        self._invalidateAllPersistentIndices()
        self.layoutChanged.emit() 
[docs]    def columnCount(self, parent=None):
        # See Qt documentation for method documentation
        return self._column_count 
[docs]    def rowCount(self, parent=None):
        # See Qt documentation for method documentation
        if parent is None or not parent.isValid():
            return len(self._rows)
        else:
            return 0 
    @table_helper.data_method(CustomRole.Residue)
    def _resRole(self, res):
        """
        Return the residue object at the location or None
        See table_helper for argument documentation.
        """
        return res
    @table_helper.data_method(CustomRole.ReferenceResidue)
    def _refResRole(self, res, seq, row, col):
        """
        Return the reference residue at the location or None
        See table_helper for argument documentation.
        """
        ref_seq = self._rows[0]
        return ref_seq[col] if col < len(ref_seq) else None
    @table_helper.data_method(CustomRole.Seq)
    def _seqData(self, res, seq):
        """
        Return the sequence for the specified row.
        See table_helper for argument documentation.
        """
        return seq
    @table_helper.data_method(CustomRole.ResSelected)
    def _resSelectedData(self, res, seq, row, col):
        """
        Return whether the specified residue is selected.
        See table_helper for argument documentation.
        """
        if col >= len(seq):
            return False
        return self.aln.res_selection_model.isSelected(res)
    @table_helper.data_method(CustomRole.NonstandardRes)
    def _nonstandardResData(self, res, seq, row, col):
        """
        Return whether the specified residue is nonstandard.
        See table_helper for argument documentation.
        """
        if col >= len(seq):
            return False
        if not res.is_res:
            return False
        return res.type.nonstandard
    @table_helper.data_method(CustomRole.ResSelectionBlockStart)
    def _resSelectionBlockStartData(self, res, seq, row, col):
        """
        Whether a residue selection block starts immediately before or after
        this index.  Used to paint I-bars when in edit mode.
        """
        sel_model = self.aln.res_selection_model
        if sel_model.isSelected(res):
            if col == 0 or not sel_model.isSelected(seq[col - 1]):
                return ResSelectionBlockStart.Before
            else:
                return None
        else:
            if col >= len(seq) - 1 or not sel_model.isSelected(seq[col + 1]):
                return None
            else:
                return ResSelectionBlockStart.After
    @table_helper.data_method(CustomRole.ResAnchored)
    def _resAnchoredData(self, res, _seq, _row, col):
        if not self._has_anchors:
            return False
        refseq = self.aln.getReferenceSeq()
        anchor_residues = self.aln.getAnchoredResidues()
        if res not in refseq:
            return res in anchor_residues
        else:
            ref_res = res
            return any(ref_res.idx_in_seq == anchor_res.idx_in_seq
                       for anchor_res in anchor_residues)
    @property
    def _anchored_ref_res_col_idxs(self):
        if self._cache.anchored_ref_res_col_idxs is None:
            anchored_res = self.aln.getAnchoredResidues()
            ref_seq = self.aln.getReferenceSeq()
            anchored_ref_res = (res for res in anchored_res if res in ref_seq)
            anchored_ref_res_col_idxs = {
                res.idx_in_seq for res in anchored_ref_res
            }
            self._cache.anchored_ref_res_col_idxs = anchored_ref_res_col_idxs
        return self._cache.anchored_ref_res_col_idxs
    @table_helper.data_method(CustomRole.IsAnchoredColumnRangeEnd)
    def _isAnchoredColumnRangeEnd(self, _res, _seq, _row, col):
        return col in self._anchor_range_ends
    @property
    def _anchor_range_ends(self):
        if self._cache.anchor_range_ends is None:
            anchored_residues = self.aln.getAnchoredResidues()
            column_is_anchored = list(range(self.aln.num_columns))
            for idx, col in enumerate(self.aln.columns()):
                if not any(res in anchored_residues for res in col):
                    column_is_anchored[idx] = -1
            anchored_range_ends = set()
            for is_anchored, col_indices in itertools.groupby(
                    column_is_anchored, key=lambda k: k != -1):
                col_indices = list(col_indices)
                if not is_anchored:
                    continue
                anchored_range_ends.add(col_indices[0])
                anchored_range_ends.add(col_indices[-1])
            self._cache.anchor_range_ends = anchored_range_ends
        return self._cache.anchor_range_ends
    @table_helper.data_method(CustomRole.SeqSelected)
    def _seqSelectedData(self, res, seq, row, col):
        """
        Return whether the specified sequence is selected.
        See table_helper for argument documentation.
        """
        return self.aln.seq_selection_model.isSelected(seq)
    @table_helper.data_method(CustomRole.HomologyStatus)
    def _homologyStatusData(self, res, seq):
        """
        Return the homology status of the specified sequence.
        See table_helper for argument documentation.
        """
        return self.aln.getHomologyStatus(seq)
    @table_helper.data_method(CustomRole.AlignmentIdentity,
                              CustomRole.AlignmentSimilarity,
                              CustomRole.AlignmentConservation,
                              CustomRole.AlignmentScore)
    def _alignmentMetrics(self, res, seq, row, col, role):
        """
        Returns alignment metric value depending on the role given.
        See table_helper for argument documentation.
        """
        metrics_map = {
            CustomRole.AlignmentIdentity: 'getIdentity',
            CustomRole.AlignmentSimilarity: 'getSimilarity',
            CustomRole.AlignmentConservation: 'getConservation',
            CustomRole.AlignmentScore: 'getSimilarityScore'
        }
        reference_seq = self.aln.getReferenceSeq()
        if reference_seq is None:
            return
        meth_name = metrics_map[role]
        meth = getattr(reference_seq, meth_name)
        only_consider = None
        if self._options_model_cache.compute_for_columns == \
                
ColumnMode.SelectedColumns:
            selected_residues = self.aln.res_selection_model.getSelection()
            only_consider = set(reference_seq) & selected_residues
        return meth(seq, self._options_model_cache.include_gaps, only_consider)
    @table_helper.data_method(CustomRole.ReferenceSequence)
    def _referenceSequence(self, res, seq):
        """
        Returns True if the given sequence is set as the reference sequence in
        the alignment, False otherwise.
        See table_helper for argument documentation.
        """
        return self.aln.isReferenceSeq(seq)
    @table_helper.data_method(Qt.DisplayRole, Qt.EditRole)
    def _displayData(self, res, seq, row, col, role):
        """
        Return the residue object at the location or an empty string
        See table_helper for argument documentation.
        """
        if col >= len(seq):
            return TERMINAL_GAP
        elif res.is_gap:
            return DEFAULT_GAP
        display_mode = self._options_model_cache.res_format
        dot_mode = (self._options_model_cache.identity_display is
                    IdentityDisplayMode.MidDot and not role == Qt.EditRole)
        # If we're in the process of deleting the last sequence, we'll have a
        # temporary dummy column after the alignment has deleted columns but
        # before it's deleted the sequence.  In that case, the
        # columnHasAllSameResidues() call will fail if we don't first check
        # self._using_dummy_column.
        draw_dot = (dot_mode and not self._using_dummy_column and
                    self.aln.columnHasAllSameResidues(col))
        if display_mode is ResidueFormat.OneLetter or role == Qt.EditRole:
            if draw_dot:
                return u'\u00B7'
            return str(res)
        if display_mode is ResidueFormat.HideLetters:
            return ""
        if draw_dot:
            return u' \u00B7 '
        return res.long_code
    @table_helper.data_method(
        *{
            RoleBase.SeqAnnotationIndexes + i.value
            for i in SEQ_ANNO_TYPES
            if i is not SEQ_ANNO_TYPES.alignment_set
        })
    def _seqAnnotationIndexesData(self, res, seq, row, col, role):
        ann_num = role - RoleBase.SeqAnnotationIndexes
        ann = self.aln.seq_annotations(ann_num)
        return self._page_model.getShownAnnIndexes(seq, ann)
    @table_helper.data_method(
        *{
            RoleBase.SeqAnnotation + i.value
            for i in SEQ_ANNO_TYPES
            if i not in (SEQ_ANNO_TYPES.alignment_set,
                         SEQ_ANNO_TYPES.kinase_features,
                         SEQ_ANNO_TYPES.kinase_conservation)
        })
    def _seqAnnotationData(self, res, seq, row, col, role):
        """
        Return the value for the specified sequence annotation role
        See table_helper for argument documentation.
        """
        ann_num = role - RoleBase.SeqAnnotation
        ann = self.aln.seq_annotations(ann_num)
        if ann in {
                SEQ_ANNO_TYPES.disulfide_bonds,
                SEQ_ANNO_TYPES.pred_disulfide_bonds
        }:
            if ann is SEQ_ANNO_TYPES.disulfide_bonds:
                bonds = seq.disulfide_bonds
            elif ann is SEQ_ANNO_TYPES.pred_disulfide_bonds:
                bonds = seq.pred_disulfide_bonds
            ss_bonds = (bond for bond in bonds if bond.is_intra_sequence)
            ss_bond_idxs = [
                (seq.index(r1), seq.index(r2)) for r1, r2 in ss_bonds
            ]
            return ss_bond_idxs
        if ann is SEQ_ANNO_TYPES.antibody_cdr:
            return seq.annotations.getAntibodyCDRs(
                scheme=self._options_model_cache.antibody_cdr_scheme)
        if ann is SEQ_ANNO_TYPES.resnum:
            if col >= len(seq):
                return None
            return self.getResnumForColumn(seq, col)
        if ann is SEQ_ANNO_TYPES.secondary_structure:
            gap_indices = {i for (i, res) in enumerate(seq) if res.is_gap}
            return (seq.secondary_structures, gap_indices)
        if ann is SEQ_ANNO_TYPES.pred_secondary_structure:
            gap_indices = {i for (i, res) in enumerate(seq) if res.is_gap}
            return (seq.pred_secondary_structures, gap_indices)
        if ann is SEQ_ANNO_TYPES.pairwise_constraints:
            if seq is not self.aln.getReferenceSeq():
                return []
            return self.aln.pairwise_constraints.indices
        if ann is SEQ_ANNO_TYPES.proximity_constraints:
            if seq is not self.aln.getReferenceSeq():
                return []
            return self.aln.proximity_constraints.indexes
        return seq.getAnnotation(col, ann)
    @table_helper.data_method(RoleBase.SeqAnnotation +
                              SEQ_ANNO_TYPES.alignment_set.value)
    def _alignmentSetData(self, res, seq):
        alignment_set = self.aln.alnSetForSeq(seq)
        if alignment_set is None:
            return None
        return alignment_set.name
    @table_helper.data_method(*list(
        range(RoleBase.GlobalAnnotation,
              RoleBase.GlobalAnnotation + MAX_ANNOTATIONS)))
    def _globalAnnotationData(self, res, seq, row, col, role):
        """
        Return the Qt.DisplayData value for the specified global annotation
        role.
        See table_helper for argument documentation.
        """
        if self._using_dummy_column:
            return None
        ann_num = role - RoleBase.GlobalAnnotation
        ann = self.aln.global_annotations(ann_num)
        data = self.aln.getGlobalAnnotationData(col, ann)
        if data is None:
            # We may be in the process of lengthening the alignment and this
            # column doesn't have any data in it yet.
            return None
        elif ann is ALN_ANNO_TYPES.consensus_symbols:
            return data.value
        elif ann is ALN_ANNO_TYPES.consensus_seq and data is not None:
            # If there is only one member in data, return it
            if len(data) == 1:
                cons_res = data[0]
                return cons_res if cons_res.is_gap else cons_res.short_code
            # Disagreement (i.e. non-consensus) is indicated by "+"
            else:
                return '+'
        elif ann is ALN_ANNO_TYPES.sequence_logo:
            # Process the sequence logo in three ways:
            # - Round the frequencies to reduce cache misses
            # - Truncate the list to MAX_AA_IN_LOGO amino acids to reduce crowding
            # - Convert residue objects to string
            bits, freqs = data
            rounded_freqs = tuple([(aa.short_code, round(freq, 6))
                                   for aa, freq in freqs[:MAX_AA_IN_LOGO]])
            return (bits, rounded_freqs)
        else:
            return data
    @table_helper.data_method(*list(
        range(RoleBase.SequenceProperty,
              RoleBase.SequenceProperty + MAX_SEQ_PROPS)))
    def _seqPropData(self, res, seq, row, col, role):
        """
        Return the Qt.DisplayData value for sequence property
        See table_helper for argument documentation.
        """
        prop_num = role - RoleBase.SequenceProperty
        seq_prop = self._options_model.sequence_properties[prop_num]
        return seq.getProperty(seq_prop)
    @table_helper.data_method(CustomRole.ResidueIndex)
    def _residueIndexData(self, res, seq, row, col):
        if self._using_dummy_column:
            return None
        return col
    @table_helper.data_method(CustomRole.ConsensusSeq)
    def _consensusSeqData(self, res, seq, row, col):
        """
        Return the raw consensus sequence value for the given column.
        See table_helper for argument documentation.
        """
        if self._using_dummy_column:
            return []
        return self.aln.getGlobalAnnotationData(col,
                                                ALN_ANNO_TYPES.consensus_seq)
    @table_helper.data_method(*[
        RoleBase.SeqAnnotationRange + ann.value
        for ann in (SEQ_ANNO_TYPES.window_hydrophobicity, SEQ_ANNO_TYPES.sasa,
                    SEQ_ANNO_TYPES.window_isoelectric_point)
    ])
    def _seqAnnotationRangeData(self, res, seq, row, col, role):
        """
        Return the range for the specified sequence annotation role
        See table_helper for argument documentation.
        """
        ann_num = role - RoleBase.SeqAnnotationRange
        ann = self.aln.seq_annotations(ann_num)
        values = getattr(seq.annotations, ann.name)
        return values.range
    @table_helper.data_method(RoleBase.SeqAnnotationRange +
                              SEQ_ANNO_TYPES.b_factor.value)
    def _bFactorRangeData(self, res, seq, row, col, role):
        min_bf = (seq.annotations.min_b_factor
                  if seq.annotations.min_b_factor < 0 else 0)
        max_bf = (seq.annotations.max_b_factor
                  if seq.annotations.max_b_factor > 0 else 0)
        return (min_bf, max_bf)
    @table_helper.data_method(*[
        RoleBase.GlobalAnnotationRange + ann.value
        for ann in (ALN_ANNO_TYPES.mean_hydrophobicity,
                    ALN_ANNO_TYPES.mean_isoelectric_point,
                    ALN_ANNO_TYPES.consensus_freq)
    ])
    def _globalAnnotationRangeData(self, res, seq, row, col, role):
        """
        Return the range for the specified global annotation role
        See table_helper for argument documentation.
        """
        ann_num = role - RoleBase.GlobalAnnotationRange
        ann = self.aln.global_annotations(ann_num)
        values = getattr(self.aln.annotations, ann.name)
        return values.range
    @table_helper.data_method(CustomRole.Included)
    def _sequenceIncluded(self, res, seq):
        """
        :return: Whether the sequence is included and/or visible in the
            workspace.
        :rtype: `viewconstants.Inclusion`
        """
        return seq.visibility
    @table_helper.data_method(CustomRole.EntryID)
    def _entryID(self, res, seq):
        """
        Sequence entry ID information
        """
        return seq.entry_id
[docs]    def getRowVisibilities(self):
        """
        Get whether the sequences are visible in the MSV sequence list.
        The actual filtering is done by SequenceFilterProxyModel.
        """
        if self.aln is None:
            return []
        return self.aln.getSeqShownStates() 
    @table_helper.data_method(CustomRole.PreviousRowHidden)
    def _previousRowHidden(self, res, seq):
        """
        Will the previous row get filtered out by the SequenceFilterProxyModel?
        Controls whether or not the AlignmentInfoView draws a hidden sequence
        marker between this sequence and the previous one.  This data is further
        transformed in AnnotationProxyModel to take annotation rows into
        account.
        """
        row_visibilities = self.getRowVisibilities()
        seq_index = self.aln.index(seq)
        prev_index = seq_index - 1
        return prev_index >= 0 and not row_visibilities[prev_index]
    @table_helper.data_method(CustomRole.NextRowHidden)
    def _nextRowHidden(self, res, seq):
        """
        Will the next row get filtered out by the SequenceFilterProxyModel?
        Controls whether or not the AlignmentInfoView draws a hidden sequence
        marker between this sequence and the next one.  This data is further
        transformed in AnnotationProxyModel to take annotation rows into
        account.
        """
        row_visibilities = self.getRowVisibilities()
        seq_index = self.aln.index(seq)
        next_index = seq_index + 1
        return next_index < len(self.aln) and not row_visibilities[next_index]
    @table_helper.data_method(CustomRole.SeqExpanded)
    def _seqExpandedData(self, res, seq):
        return self.aln.isSeqExpanded(seq)
    @table_helper.data_method(SeqInfo.Name)
    def _seqNameData(self, res, seq):
        """
        Get the (short) name of a sequence.
        See table_helper for argument documentation.
        """
        return seq.name
    @table_helper.data_method(SeqInfo.EntryName)
    def _seqEntryNameData(self, res, seq):
        return seq.entry_name
    @table_helper.data_method(SeqInfo.Title)
    def _seqLongNameData(self, res, seq):
        """
        Get the name of a sequence.
        See table_helper for argument documentation.
        """
        return seq.long_name
    @table_helper.data_method(SeqInfo.GaplessLength)
    def _seqGaplessLengthData(self, res, seq):
        """
        Get the gapless length of a sequence.
        """
        return seq.getGaplessLength()
    @table_helper.data_method(SeqInfo.GapCount)
    def _seqGapCountData(self, res, seq):
        """
        Get the gap count of a sequence.
        """
        return seq.getGapCount()
    @table_helper.data_method(SeqInfo.Chain)
    def _seqChainData(self, res, seq):
        """
        Get the chain of a sequence.
        See table_helper for argument documentation.
        """
        return seq.chain
    @table_helper.data_method(CustomRole.HasStructure)
    def _hasStructureData(self, res, seq):
        """
        Whether the sequence has a structure associated with it.
        """
        return seq.hasStructure()
    @table_helper.data_method(CustomRole.SeqresOnly)
    def _seqresOnlyData(self, res, seq, row, col):
        """
        Whether the residue is structureless.
        Returns True if all of the following conditions are met:
            - the sequence has an associated structure
            - the residue is not a gap
            - the residue only appears in the SEQRES record of the structure
        :rtype: bool
        """
        if not seq.hasStructure():
            return False
        return col < len(seq) and not res.hasStructure() and res.is_res
    @table_helper.data_method(CustomRole.PartialPairwiseConstraint)
    def _partialPairwiseConstraintData(self, res, seq, row, col):
        """
        Whether the residue is a partial pairwise constraint
        :rtype: bool
        """
        if col >= len(seq):
            return False
        pick_mode = self._options_model_cache.pick_mode
        if pick_mode is PickMode.Pairwise:
            constraints = self.aln.pairwise_constraints
            return (res == constraints.other_residue or
                    res == constraints.ref_residue)
        elif pick_mode is PickMode.HMProximity:
            constraints = self.aln.proximity_constraints
            return res == constraints.picked_residue
        return False
    @table_helper.data_method(CustomRole.HMCompositeRegion)
    def _compositeRegionData(self, res):
        """
        :return: Whether the residue is in a homology modeling composite region
        :rtype: bool
        """
        return self.aln.isHomologyCompositeResidue(res)
    @table_helper.data_method(CustomRole.ResOutline)
    def _resOutlineData(self, res, seq, row, col):
        return self.aln.getResOutlinesForSeq(seq)
    @table_helper.data_method(CustomRole.SeqMatchesRefType)
    def _seqMatchesRefTypeData(self, res, seq, row, col):
        return self.aln.seqMatchesRefType(seq)
    @table_helper.data_method(CustomRole.PfamName)
    def _pfamNameData(self, res, seq):
        return seq.pfam_name
    @table_helper.data_method(Qt.BackgroundRole)
    def _sequenceBackgroundData(self, res, seq, row, col):
        """
        Return the appropriate background brush for the specified residue
        in a sequence row.
        :return: A brush containing the background color or None to paint no
            background color
        :rtype: QtGui.QBrush or NoneType
        """
        if not self._hasBackgroundColor(res, seq, row, col):
            return None
        alpha = 255
        if res in self._residue_highlights:
            rgb = self._residue_highlights[res]
        elif isinstance(seq, sequence.NucleicAcidSequence):
            rgb = self._nucleic_color_scheme.getColorByRes(res)
        else:
            if self._options_model_cache.weight_by_quality:
                # "Weight Colors By Alignment Quality" sets alpha transparency
                # per-column
                alpha = self._getQualityAlphaByCol(col)
                if alpha == 0:
                    # The background would be fully transparent, so there's no
                    # point in painting anything
                    return None
            rgb = self._seqBaseColor(res, seq, row, col)
        r, g, b = rgb
        rgba = (r, g, b, alpha)
        if rgba in self._brush_cache:
            return self._brush_cache[rgba]
        else:
            brush = QtGui.QBrush(QtGui.QColor(*rgba))
            self._brush_cache[rgba] = brush
            return brush
    def _seqBaseColor(self, res, seq, row, col):
        """
        Determine the color to use for the background the specified sequence row
        cell.  This method does not take the weight-by-quality option into
        account.
        :param res: The residue for the specified cell.
        :type res: residue.Residue
        :param seq: The sequence for the specified row
        :type seq: sequence.Sequence
        :param row: The row number.
        :type row: int
        :param col: The column number.
        :type col: int
        :return: An rgb tuple representing the background color.
        :rtype: tuple(int, int, int)
        """
        if self._options_model_cache.average_in_cols:
            # If "Average Color In Columns" is set, then all residues in a
            # column get the same color
            if not self._cache.average_color:
                self._updateAverageColorCache()
            return self._cache.average_color[col]
        # If none of the above options apply, then ask the color scheme for the
        # appropriate color.  This will take into account custom colors
        # (including workspace residue coloring).
        if isinstance(self._seq_color_scheme, color.AlignmentRowColorScheme):
            rgb = self._seq_color_scheme.getColorByResAndAln(
                res, self.getAlignment())
        else:
            rgb = self._seq_color_scheme.getColorByRes(res)
        return rgb
    def _hasBackgroundColor(self, res, seq, row, col):
        """
        :return: Whether the given residue should not have background color
        """
        if col >= len(seq) or res.is_gap or self._using_dummy_column:
            return False
        if not self._options_model_cache.colors_enabled:
            return False
        if res in self._residue_highlights:
            return True
        by_aln_mode = self._options_model_cache.color_by_aln
        if by_aln_mode is ColorByAln.Unset:
            return False
        if by_aln_mode in (ColorByAln.DifferentCons, ColorByAln.MatchingCons):
            # If the "Color By Sequence Alignment" option is set and this
            # residue doesn't match/differ from the consensus sequence, then
            # don't paint the background
            if not self._cache.res_matches_cons:
                self._updateResMatchesConsensus()
            matches = self._cache.res_matches_cons[row][col]
            return matches is (by_aln_mode is not ColorByAln.DifferentCons)
        if row != 0 and by_aln_mode in (ColorByAln.Different,
                                        ColorByAln.Matching):
            # If the "Color By Sequence Alignment" option is set and this
            # non-ref residue doesn't match/differ from the reference sequence,
            # then don't paint the background
            if not self._cache.res_matches_ref:
                self._updateResMatchesRef()
            matches = self._cache.res_matches_ref[row][col]
            return matches is (by_aln_mode is not ColorByAln.Different)
        return True
    def _updateResMatchesRef(self):
        """
        Update self._cache.res_matches_ref, which stores whether a given residue
        matches the reference residue for that column.
        """
        num_rows = len(self.aln)
        num_cols = self._column_count
        res_matches_ref = ([[True] * num_cols] +
                           [[False] * num_cols for _ in range(num_rows - 1)])
        ref_seq = self.aln[0]
        for seq_idx, seq in enumerate(self.aln[1:], start=1):
            if not self.aln.seqMatchesRefType(seq):
                continue
            for res_idx, (res, ref_res) in enumerate(zip(seq, ref_seq)):
                if (ref_res.is_res and res.is_res and
                        ref_res.type.long_code != 'UNK' and
                        ref_res.type.short_code == res.type.short_code):
                    res_matches_ref[seq_idx][res_idx] = True
        self._cache.res_matches_ref = res_matches_ref
    def _updateResMatchesConsensus(self):
        """
        Update self._cache.res_matches_cons, which stores whether a given
        residue matches the consensus residue for that column. If there is no
        consensus (e.g. multiple most common residues), all residues are
        considered non-matching.
        """
        num_rows = len(self.aln)
        num_cols = self._column_count
        res_matches_cons = ([[False] * num_cols for _ in range(num_rows)])
        cons_seq = [
            self.aln.getGlobalAnnotationData(col, ALN_ANNO_TYPES.consensus_seq)
            for col in range(num_cols)
        ]
        for seq_idx, seq in enumerate(self.aln):
            if not self.aln.seqMatchesRefType(seq):
                continue
            for res_idx, (res, cons_residues) in enumerate(zip(seq, cons_seq)):
                if len(cons_residues) != 1 or res.is_gap:
                    continue
                cons_res = cons_residues[0]
                if (cons_res.long_code != 'UNK' and
                        cons_res.short_code == res.short_code):
                    res_matches_cons[seq_idx][res_idx] = True
        self._cache.res_matches_cons = res_matches_cons
    def _getQualityAlphaByCol(self, col):
        if not self._cache.quality_alpha_by_col:
            self._updateQualityAlphaByCol()
        return self._cache.quality_alpha_by_col[col]
    def _updateQualityAlphaByCol(self):
        """
        Update self._cache.quality_alpha_by_col, which stores the alpha value to use
        for each column when weighting colors by alignment quality.
        """
        num_seqs = len(self.aln)
        min_weight = self.MIN_ALN_QUALITY_WEIGHT
        weight_range = 1 - min_weight
        alpha_by_col = []
        for column in self.aln.columns():
            ref_elem = next(iter(column), None)
            col_res = [elem for elem in column if elem.is_res]
            if ref_elem.is_gap or not col_res:
                alpha_by_col.append(0)
                continue
            match = ref_elem.short_code
            max_matching = sum(1 for res in col_res if res.short_code == match)
            if max_matching == 1:
                weight = 0
            else:
                weight = (max_matching / num_seqs - min_weight) / weight_range
            alpha = int(weight * 255)
            if alpha < 0:
                alpha = 0
            elif alpha > 255:
                alpha = 255
            alpha_by_col.append(alpha)
        self._cache.quality_alpha_by_col = alpha_by_col
    def _updateAverageColorCache(self):
        """
        Update self._cache.average_color, which stores the average color of all
        non-gap residues in a column.
        """
        scheme = self._seq_color_scheme
        average_color_cache = []
        for column in self.aln.columns(omit_gaps=True):
            if not column:
                average_color_cache.append(None)
                continue
            if isinstance(scheme, color.AlignmentRowColorScheme):
                rgbs = (
                    scheme.getColorByResAndAln(res, self.aln) for res in column)
            else:
                rgbs = (scheme.getColorByRes(res) for res in column)
            avg_rgb = [val // len(column) for val in map(sum, zip(*rgbs))]
            average_color_cache.append(avg_rgb)
        self._cache.average_color = average_color_cache
    @table_helper.data_method(*list(
        range(RoleBase.BindingSiteName,
              RoleBase.BindingSiteName + MAX_LIGANDS)))
    def _bindingSiteNameData(self, res, seq, row, col, role):
        ligand_idx = role - RoleBase.BindingSiteName
        return seq.annotations.ligands[ligand_idx]
    @table_helper.data_method(*list(
        range(RoleBase.BindingSiteBackground,
              RoleBase.BindingSiteBackground + MAX_LIGANDS)))
    def _bindingSiteBackgroundData(self, res, seq, row, col, role):
        scheme = self._color_scheme.get(SEQ_ANNO_TYPES.binding_sites)
        if scheme is None:
            return None
        ligand_idx = role - RoleBase.BindingSiteBackground
        if col < len(seq) and res.is_res:
            lig_contacts = seq.getAnnotation(col, SEQ_ANNO_TYPES.binding_sites)
            lig_contact = lig_contacts[ligand_idx]
        else:
            lig_contact = annotation.BINDING_SITE.NoContact
        return scheme.getBrushByKey(lig_contact)
    @table_helper.data_method(*list(
        range(RoleBase.KinaseConservationBackground,
              RoleBase.KinaseConservationBackground + MAX_LIGANDS)))
    def _kinaseConservationBackgroundData(self, res, seq, row, col, role):
        scheme = self._color_scheme.get(SEQ_ANNO_TYPES.kinase_conservation)
        if scheme is None:
            return None
        ligand_idx = role - RoleBase.KinaseConservationBackground
        conservation = None
        if col < len(seq) and res.is_res:
            conservation = res.kinase_conservation.get(ligand_idx)
        return scheme.getBrushByKey(conservation)
    @table_helper.data_method(*list(
        range(RoleBase.DomainName, RoleBase.DomainName + MAX_DOMAINS)))
    def _domainNameData(self, res, seq, row, col, role):
        domain_idx = role - RoleBase.DomainName
        return seq.annotations.domains[domain_idx]
    @table_helper.data_method(*list(
        range(RoleBase.DomainBackground,
              RoleBase.DomainBackground + MAX_DOMAINS)))
    def _domainsBackgroundData(self, res, seq, row, col, role):
        scheme = self._color_scheme.get(SEQ_ANNO_TYPES.domains)
        if scheme is None or res is None:
            return None
        domain_idx = role - RoleBase.DomainBackground
        domains = seq.annotations.domains
        if domains is None:
            return None
        if (res.is_res and res.domains is not None and
                domains[domain_idx] in res.domains):
            key = annotation.Domains.Domain
        else:
            key = annotation.Domains.NoDomain
        return scheme.getBrushByKey(key)
    # TODO: many of the "background" brushes for annotations are really
    #       foreground brushes
    @table_helper.data_method(
        *{
            RoleBase.SeqBackground + i.value
            for i in SEQ_ANNO_TYPES
            if i not in (SEQ_ANNO_TYPES.binding_sites, SEQ_ANNO_TYPES.domains,
                         SEQ_ANNO_TYPES.kinase_conservation)
        })
    def _annotationBackgroundData(self, res, seq, row, col, role):
        ann = self.aln.seq_annotations(role - RoleBase.SeqBackground)
        scheme = self._color_scheme.get(ann)
        if scheme is not None and isinstance(scheme,
                                             color.ResidueRowColorScheme):
            if ann is SEQ_ANNO_TYPES.kinase_features:
                if col >= len(seq) or res.is_gap:
                    return None
                kinase_feature = seq.getAnnotation(col, ann)
                return scheme.getBrushByKey(kinase_feature)
            return scheme.getBrushByRes(res)
    @table_helper.data_method(
        *{RoleBase.GlobalBackground + i.value for i in ALN_ANNO_TYPES})
    def _globalBackgroundData(self, res, seq, row, col, role):
        aln = self.aln
        ann = aln.global_annotations(role - RoleBase.GlobalBackground)
        scheme = self._color_scheme.get(ann)
        if scheme is None:
            return None
        if isinstance(scheme, color.SingleColorScheme):
            return scheme.getBrushByRes()
        cons_residues = aln.getGlobalAnnotationData(
            col, ALN_ANNO_TYPES.consensus_seq)
        if not cons_residues:
            # No residues present at this column in consensus sequence
            return scheme.getBrushByResAndAln(res, aln)
        else:
            cons_res = cons_residues[0]
            if isinstance(cons_res, residue.Nucleotide):
                return self._nucleic_color_scheme.getBrushByRes(cons_res)
            else:
                return scheme.getBrushByResAndAln(cons_residues[0], aln)
    @table_helper.data_method(Qt.ForegroundRole)
    def _foregroundData(self, res, seq, row, col):
        scheme = self._seq_color_scheme
        if self.aln.res_selection_model.isSelected(res):
            return color.SELECTED_TEXT_COLOR
        elif col >= len(seq):
            return scheme.TEXT_COLOR_TERM_GAP
        elif res.is_gap:
            return scheme.TEXT_COLOR_GAP
        elif (
                not self._hasBackgroundColor(res, seq, row, col) or
            (self._options_model_cache.weight_by_quality and
             self._getQualityAlphaByCol(col) < color.NO_BACKGROUND_ALPHA_CUTOFF)
        ):
            # If there's no background color or the background opacity is low
            return self._no_background_text_color
        else:
            rgb = self._residue_highlights.get(res)
            if rgb is None and self._seq_color_scheme.custom:
                rgb = self._seqBaseColor(res, seq, row, col)
            if rgb is not None:
                color_options = (scheme.TEXT_COLOR,
                                 self._no_background_text_color)
                other_color = get_contrasting_color(rgb, options=color_options)
                if not isinstance(other_color, QtGui.QColor):
                    other_color = QtGui.QColor(*other_color)
                return other_color
        return scheme.TEXT_COLOR
    # We only provide foreground role data for annotations that actually use it
    @table_helper.data_method(
        RoleBase.SeqForeground + SEQ_ANNO_TYPES.resnum.value,
        RoleBase.SeqForeground + SEQ_ANNO_TYPES.pfam.value,
        RoleBase.SeqForeground + SEQ_ANNO_TYPES.alignment_set.value)
    def _annotationForegroundData(self, res, seq, row, col, role):
        ann = self.aln.seq_annotations(role - RoleBase.SeqForeground)
        scheme = self._color_scheme[ann]
        if ann is SEQ_ANNO_TYPES.alignment_set:
            rgb = scheme.getColorByResAndAln(res, self.aln)
            if rgb is not None:
                return QtGui.QColor(*rgb)
        return scheme.TEXT_COLOR
    # We only provide foreground role data for annotations that actually use it
    @table_helper.data_method(
        *{
            RoleBase.GlobalForeground + i.value
            for i in (ALN_ANNO_TYPES.indices, ALN_ANNO_TYPES.consensus_seq,
                      ALN_ANNO_TYPES.consensus_symbols)
        })
    def _annotationGlobalForegroundData(self, res, seq, row, col, role):
        # coloring here is based on the consensus sequence
        ann = self.aln.global_annotations(role - RoleBase.GlobalForeground)
        scheme = self._color_scheme[ann]
        cons_residues = self.aln.getGlobalAnnotationData(
            col, ALN_ANNO_TYPES.consensus_seq)
        if not cons_residues:
            # No residues in any sequence at this column; but we still need
            # to return a color to draw the ruler
            return scheme.TEXT_COLOR
        rgb = scheme.getColorByResAndAln(cons_residues[0], self.aln)
        # change brush to gray if the total brightness/value is < 100
        if QtGui.qGray(*rgb) < 100:
            return color.NO_EMPH_TEXT_COLOR
        return scheme.TEXT_COLOR
[docs]    @table_helper.data_method(Qt.FontRole)
    def getFont(self):
        """
        :return: The current font.
        :rtype: QtGui.QFont
        """
        return self._font 
    @table_helper.data_method(
        *{RoleBase.GlobalToolTip + i.value for i in ALN_ANNO_TYPES})
    def _globalAnnotationToolTipData(self, res, seq, row, col, role):
        """
        Return a tooltip string for a global annotation
        :param source_index: The source index for which we need a tooltip
        :type source_index: `QtCore.QModelIndex`
        :return: A tooltip for the specified cell
        :rtype: str
        """
        if self._using_dummy_column:
            return None
        ann = self.aln.global_annotations(role - RoleBase.GlobalToolTip)
        ann_value = self.aln.getGlobalAnnotationData(col, ann)
        if ann is ALN_ANNO_TYPES.indices:
            return "Alignment Index: {}".format(ann_value)
        if ann is ALN_ANNO_TYPES.mean_hydrophobicity:
            return "Mean Hydrophobicity: {}".format(ann_value)
        if ann is ALN_ANNO_TYPES.mean_isoelectric_point:
            return "Mean Isoelectric Point: {}".format(ann_value)
        if ann is ALN_ANNO_TYPES.consensus_symbols:
            shared_text = '\n'.join([
                '" ": not conserved', '"*": fully conserved',
                '":": strongly conserved', '".": weakly conserved'
            ])
            return "Consensus symbol:\n{}\n{}".format(ann_value.tooltip,
                                                      shared_text)
        if ann is ALN_ANNO_TYPES.consensus_seq:
            # Display using 3-letter code, if multiple residues
            # share the highest frequency, separate them by space.
            res_str = ' '.join(res_obj.long_code for res_obj in ann_value)
            freq = self.aln.getGlobalAnnotationData(
                col, ALN_ANNO_TYPES.consensus_freq)
            return "Consensus sequence:\n{}: {}%".format(res_str, freq)
        if ann is ALN_ANNO_TYPES.consensus_freq:
            consensus_res = self.aln.getGlobalAnnotationData(
                col, ALN_ANNO_TYPES.consensus_seq)
            res_str = ' '.join(res_obj.long_code for res_obj in consensus_res)
            return "Consensus frequency:\n{}: {}%".format(res_str, ann_value)
        if ann is ALN_ANNO_TYPES.sequence_logo:
            bits, aa_freq_list = ann_value
            tt_list = ["diversity: %.2f" % round(bits, 2)]
            # frequency high to low, match the drawing in the column
            for aa, freq in reversed(aa_freq_list):
                res_str = aa.long_code
                tt_list.append(f"{res_str}: {bits * freq:.2f} ({freq:.0%})")
            return '\n'.join(tt_list)
    @table_helper.data_method(Qt.ToolTipRole)
    def _toolTipData(self, res, seq, row, col, role):
        if col >= len(seq) or res.is_gap:
            return None
        tooltip = self._getBaseResidueToolTip(res, seq)
        scheme = self._seq_color_scheme
        if not isinstance(seq, sequence.NucleicAcidSequence
                         ) and res not in self._residue_highlights:
            if isinstance(scheme, color.AlignmentRowColorScheme):
                scheme_key_tooltip = scheme.getColorKeyToolTipByResAndAln(
                    res, self.aln)
            else:
                scheme_key_tooltip = scheme.getColorKeyToolTipByRes(res)
            tooltip += f'<br/>{scheme.display_name}: {scheme_key_tooltip}'
        tooltip += f'<br/>Column index: {col + 1}'
        tooltip += self._getTooltipImage(res)
        return qt_utils.wrap_qt_tag(tooltip)
    def _getTooltipImage(self, res):
        no_img = ""
        if (not res.type.nonstandard and res.long_code
                not in residue.NON_STD_AA_TT_MAP) or not res.hasStructure():
            return no_img
        db = nonstandard_residues.get_residue_database()
        aa = db.getAminoAcid(res.long_code)
        if aa is None:
            return no_img
        mol = structure2d.get_rdmol_for_2d_rendering(aa.st)
        settings = sketcher.RendererSettings()
        settings.width = 400
        settings.height = 200
        renderer = sketcher.Renderer(settings)
        # TODO SKETCH-876
        # renderer has no `clear` API so we can't store and reuse it
        renderer.loadStructure(mol)
        img = renderer.getImage()
        # base64 encode image to display as HTML
        buf = QtCore.QBuffer()
        try:
            buf.open(QtCore.QIODevice.WriteOnly)
            img.save(buf, "PNG", 100)
            b64data = bytes(buf.data().toBase64()).decode()
        finally:
            buf.close()
        # Scaling to half height and width to improve appearance on hidpi
        width = img.width() // 2
        height = img.height() // 2
        img = f'<img src="data:image/png;base64, {b64data}" width={width} height={height}>'
        return f'<hr/><div>{img}</div>'
    @table_helper.data_method(*list(
        range(RoleBase.BindingSiteToolTip,
              RoleBase.BindingSiteToolTip + MAX_LIGANDS)))
    def _bindingSiteToolTipData(self, res, seq, row, col, role):
        if col >= len(seq) or res.is_gap or not seq.annotations.ligands:
            return ""
        ann = SEQ_ANNO_TYPES.binding_sites
        ligand_idx = role - RoleBase.BindingSiteToolTip
        lig_contacts = seq.getAnnotation(col, ann)
        binding_site = lig_contacts[ligand_idx]
        if binding_site is annotation.BINDING_SITE.NoContact:
            return ""
        dist = self._options_model_cache.binding_site_distance.value
        if binding_site is annotation.BINDING_SITE.CloseContact:
            desc = f"within {dist - 1}Å of "
        elif binding_site is annotation.BINDING_SITE.FarContact:
            desc = f"within {dist + 1}Å of "
        else:
            assert False
        ligand = seq.annotations.ligands[ligand_idx]
        return f"{ann.title}: {desc}{ligand}"
    @table_helper.data_method(*list(
        range(RoleBase.KinaseConservationToolTip,
              RoleBase.KinaseConservationToolTip + MAX_LIGANDS)))
    def _kinaseConservationToolTipData(self, res, seq, row, col, role):
        if col >= len(seq) or res.is_gap or not seq.annotations.ligands:
            return ""
        ann = SEQ_ANNO_TYPES.kinase_conservation
        ligand_idx = role - RoleBase.KinaseConservationToolTip
        conservation = res.kinase_conservation.get(ligand_idx)
        if conservation is None:
            return ""
        ligand = seq.annotations.ligands[ligand_idx]
        return f"{ann.title} ({ligand}): {conservation.value}"
    @table_helper.data_method(*list(
        range(RoleBase.DomainToolTip, RoleBase.DomainToolTip + MAX_DOMAINS)))
    def _domainsToolTipData(self, res, seq, row, col, role):
        if col >= len(seq) or res.is_gap:
            return ""
        domain_idx = role - RoleBase.DomainToolTip
        domains = seq.annotations.domains
        if domains is not None:
            current_domain = domains[domain_idx]
            if res.domains is not None and current_domain in res.domains:
                return f"{SEQ_ANNO_TYPES.domains.title}: {current_domain}"
        return ""
    @table_helper.data_method(
        *{
            RoleBase.SeqToolTip + i.value for i in SEQ_ANNO_TYPES if i not in {
                SEQ_ANNO_TYPES.alignment_set, SEQ_ANNO_TYPES.binding_sites,
                SEQ_ANNO_TYPES.domains, SEQ_ANNO_TYPES.kinase_conservation
            }
        })
    def _seqAnnotationToolTipData(self, res, seq, row, col, role):
        if col >= len(seq) or res.is_gap:
            return None
        ann = self.aln.seq_annotations(role - RoleBase.SeqToolTip)
        tooltip = self._getBaseResidueToolTip(res, seq)
        if ann in (SEQ_ANNO_TYPES.disulfide_bonds,
                   SEQ_ANNO_TYPES.pred_disulfide_bonds):
            # The disulfide bond will be retrieved in _getSeqAnnoToolTip
            ann_value = None
        elif ann is SEQ_ANNO_TYPES.antibody_cdr:
            ann_value = seq.annotations.getAntibodyCDR(
                col, scheme=self._options_model_cache.antibody_cdr_scheme)
        elif ann in (SEQ_ANNO_TYPES.pairwise_constraints,
                     SEQ_ANNO_TYPES.proximity_constraints):
            return ""
        else:
            ann_value = seq.getAnnotation(col, ann)
        ann_info = self._getSeqAnnoToolTip(res, seq, ann_value, ann)
        if ann_info:
            tooltip += "<br>" + ann_info
        return qt_utils.wrap_qt_tag(tooltip)
    def _getSeqAnnoToolTip(self, res, seq, value, anno_type):
        """
        Returns a string describing sequence annotation information
        for a given residue in a sequence.
        :param res: The residue for the element
        :type res: `schrodinger.protein.residue.Residue`
        :param seq: The sequence for the element
        :type seq: `schrodinger.protein.sequence.ProteinSequence`
        :param value: The value of the annotation
        :type value: (varies depending on annotation type)
        :param anno_type: The annotation type to get the tooltip for
        :type ann_type: `schrodinger.protein.annotation.\
            ProteinSequenceAnnotations.ANNOTATION_TYPES` or
            `schrodinger.protein.annotation.\
             ProteinAlignmentAnnotations.ANNOTATION_TYPES`
        """
        if isinstance(seq, sequence.NucleicAcidSequence):
            # Since many annotations don't apply to nucleic acid sequences, we
            # just return an empty tooltip to avoid errors. We will hide
            # irrelevant annotation rows for nucleic acids in MSV-1505.
            return ''
        label = anno_type.title
        if anno_type is SEQ_ANNO_TYPES.b_factor and value is not None:
            anno_desc = "{label}: {value:0.3f}".format(label=label, value=value)
            return anno_desc
        if anno_type in (SEQ_ANNO_TYPES.secondary_structure,
                         SEQ_ANNO_TYPES.pred_secondary_structure):
            if value is None:
                return ''
            res_idx = seq.index(res)
            ssa = seq.secondary_structures
            struc_type = residue.SSA_TT_MAP[value]
            if anno_type == SEQ_ANNO_TYPES.pred_secondary_structure:
                ssa = seq.pred_secondary_structures
                struc_type = f"<b>Prediction:</b><br>{struc_type}"
            # Get the corresponding SSA limits for this particular residue
            ssa_starts = [ssa_.limits[0] for ssa_ in ssa]
            ssa_idx = bisect.bisect(ssa_starts, res_idx) - 1
            start_index, end_index = ssa[ssa_idx].limits
            return f"{struc_type} {residue.get_formatted_residue_range(seq[start_index], seq[end_index])}"
        if anno_type in (SEQ_ANNO_TYPES.disulfide_bonds,
                         SEQ_ANNO_TYPES.pred_disulfide_bonds):
            return self._getDisulfideBondToolTip(anno_type, res)
        if anno_type is SEQ_ANNO_TYPES.antibody_cdr:
            cdr = value
            if cdr.label is annotation.AntibodyCDRLabel.NotCDR:
                return ''
            return f"CDR {cdr.label.name}: {residue.get_formatted_residue_range(seq[cdr.start], seq[cdr.end])}"
        if anno_type is SEQ_ANNO_TYPES.kinase_features:
            kinase_feature = value
            if kinase_feature is None or kinase_feature is annotation.KinaseFeatureLabel.NO_FEATURE:
                return ''
            def find_boundary(forward=True):
                # find the start and end of this kinase feature
                end = len(seq) if forward else -1
                step = 1 if forward else -1
                prev_res = res
                for ind in range(res.idx_in_seq, end, step):
                    cur_res = seq[ind]
                    if cur_res.is_gap:
                        continue
                    if cur_res.kinase_features != res.kinase_features:
                        break
                    prev_res = cur_res
                return prev_res
            start_res = find_boundary(False)
            end_res = find_boundary(True)
            return f'{kinase_feature.value}: {residue.get_formatted_residue_range(start_res, end_res)}'
        if (anno_type is SEQ_ANNO_TYPES.window_hydrophobicity or
                anno_type is SEQ_ANNO_TYPES.window_isoelectric_point):
            # Show the annotation's raw value at the residue
            key = anno_type.name[len("window_"):]
            raw_val = getattr(res, key)
            raw_anno_desc = "{label}: {value:0.3f}".format(label=label,
                                                           value=raw_val)
            # Show the annotation's windowed average, if it exists
            windowed_anno_desc = ""
            if value is not None:
                if anno_type is SEQ_ANNO_TYPES.window_hydrophobicity:
                    window_padding = seq.annotations.hydrophobicity_window_padding
                elif anno_type is SEQ_ANNO_TYPES.window_isoelectric_point:
                    window_padding = seq.annotations.isoelectric_point_window_padding
                window_size = 2 * window_padding + 1
                windowed_anno_desc = (
                    "\n{label} in {n}-residue window: {value:0.3f}".format(
                        label=label, n=window_size, value=value))
            return raw_anno_desc + windowed_anno_desc
        if anno_type in (SEQ_ANNO_TYPES.pred_accessibility,
                         SEQ_ANNO_TYPES.pred_disordered,
                         SEQ_ANNO_TYPES.pred_domain_arr):
            if value is None:
                return label
            value = value.name.lower().capitalize()
            return f'<b>Prediction:</b><br>{label}: {value}'
        value = residue.CB_TT_MAP.get(value, value)
        anno_desc = "{label}: {value}".format(label=label, value=value)
        return anno_desc
    def _getDisulfideBondToolTip(self, anno, res):
        if anno is SEQ_ANNO_TYPES.disulfide_bonds:
            bond = res.disulfide_bond
        elif anno is SEQ_ANNO_TYPES.pred_disulfide_bonds:
            bond = res.pred_disulfide_bond
        else:
            raise ValueError('`anno` should be either `disulfide_bond` or '
                             'pred_disulfide_bond.')
        if not bond:
            return ''
        label = anno.title
        if bond.res_pair[0] == res:
            partner_res = bond.res_pair[1]
        else:
            partner_res = bond.res_pair[0]
        # Note: returns None if partner is deleted or inter-sequence
        if bond.isValid() and bond.is_intra_sequence:
            return f"<b>Prediction:</b><br>{label} with {partner_res.long_code}{partner_res.resnum}{partner_res.inscode}"
        else:
            return ""
    @table_helper.data_method(RoleBase.SeqRowHeightScale +
                              SEQ_ANNO_TYPES.binding_sites.value)
    def _bindingSiteRowHeightScaleData(self, res, seq):
        return 1 if seq.annotations.ligands else 0
    @table_helper.data_method(RoleBase.SeqRowHeightScale +
                              SEQ_ANNO_TYPES.domains.value)
    def _domainsRowHeightScaleData(self, res, seq):
        return 1 if seq.annotations.domains else 0
    @table_helper.data_method(RoleBase.SeqRowHeightScale +
                              SEQ_ANNO_TYPES.disulfide_bonds.value)
    def _disulfideBondsRowHeightScaleData(self, res, seq):
        return 1 if len(seq.disulfide_bonds) > 0 else 0
    @table_helper.data_method(RoleBase.SeqRowHeightScale +
                              SEQ_ANNO_TYPES.pred_disulfide_bonds.value)
    def _predDisulfideBondsRowHeightScaleData(self, res, seq):
        return 1 if len(seq.pred_disulfide_bonds) > 0 else 0
    @table_helper.data_method(RoleBase.SeqRowHeightScale +
                              SEQ_ANNO_TYPES.pred_disordered.value)
    def _predDisorderedRegionsRowHeightScaleData(self, res, seq):
        return seq.hasDisorderedRegionsPredictions()
    @table_helper.data_method(RoleBase.SeqRowHeightScale +
                              SEQ_ANNO_TYPES.pred_domain_arr.value)
    def _predArrangementRowHeightScaleData(self, res, seq):
        return seq.hasDomainArrangementPredictions()
    @table_helper.data_method(RoleBase.SeqRowHeightScale +
                              SEQ_ANNO_TYPES.pred_accessibility.value)
    def _predAccessibilityRowHeightScaleData(self, res, seq):
        return seq.hasSolventAccessibility()
    @table_helper.data_method(RoleBase.SeqRowHeightScale +
                              SEQ_ANNO_TYPES.pfam.value)
    def _pfamHeightScaleData(self, res, seq):
        return not all(p is None for p in seq.annotations.pfam)
    @table_helper.data_method(RoleBase.SeqRowHeightScale +
                              SEQ_ANNO_TYPES.antibody_cdr.value)
    def _antibodyCdrRowHeightScaleData(self, res, seq):
        return 1 if seq.annotations.isAntibodyChain() else 0
    @table_helper.data_method(
        *{
            RoleBase.SeqRowHeightScale + i.value
            for i in (SEQ_ANNO_TYPES.kinase_features,
                      SEQ_ANNO_TYPES.kinase_conservation)
        })
    def _kinaseFeatureRowHeightScaleData(self, res, seq):
        return 1 if seq.isKinaseChain() else 0
    @table_helper.data_method(RoleBase.SeqRowHeightScale +
                              SEQ_ANNO_TYPES.pred_secondary_structure.value)
    def _predSecondaryStructureRowHeightScaleData(self, res, seq):
        return seq.hasSSAPredictions()
    @table_helper.data_method(RoleBase.SeqRowHeightScale +
                              SEQ_ANNO_TYPES.secondary_structure.value)
    def _secondaryStructureRowHeightScaleData(self, res, seq):
        secondary_strucs = seq.secondary_structures
        if (len(secondary_strucs) == 1 and
                secondary_strucs[0].ssa_type is None):
            return 0
        else:
            return 1
    @table_helper.data_method(RoleBase.SeqRowHeightScale +
                              SEQ_ANNO_TYPES.alignment_set.value)
    def _alignmentSetRowHeightScaleData(self, res, seq):
        if self.aln.alnSetForSeq(seq) is None:
            return 0
        else:
            return 1
    @table_helper.data_method(RoleBase.SeqRowHeightScale +
                              SEQ_ANNO_TYPES.pairwise_constraints.value)
    def _pairwiseConstraintRowHeightScaleData(self, res, seq):
        return 1 if seq is self.aln.getReferenceSeq() else 0
    @table_helper.data_method(RoleBase.SeqRowHeightScale +
                              SEQ_ANNO_TYPES.proximity_constraints.value)
    def _proximityConstraintRowHeightScaleData(self, res, seq):
        return 1 if seq is self.aln.getReferenceSeq() else 0
    @table_helper.data_method(CustomRole.ChainDivider)
    def _chainDividerData(self, res, seq, row, col):
        """
        If split chain mode is disabled and this residue is the start of a new
        chain, return the color to use for the chain divider.  Otherwise, return
        None.
        :rtype: QtGui.QColor or None
        """
        if self._split_chain_mode or col not in seq.chain_offsets:
            return None
        elif self.aln.res_selection_model.isSelected(res):
            return self._seq_color_scheme.SEL_CHAIN_DIVIDER_COLOR
        else:
            return self._seq_color_scheme.CHAIN_DIVIDER_COLOR
[docs]    def flags(self, index):
        """
        See Qt documentation for method documentation
        """
        return Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled 
    @table_helper.data_method(CustomRole.ColSelected)
    def _colSelectedData(self, res, seq, row, col):
        """
        Whether all residues in a column have been selected.
        """
        selection = self.aln.res_selection_model.getSelection()
        column = {
            elem for elem in self.aln.getColumn(col) if elem and not elem.is_gap
        }
        return column.issubset(selection)
    def _setData(self, col, seq, value, role, row_num):
        """
        See table_helper for argument documentation.
        """
        if role == CustomRole.ReferenceSequence:
            if value:
                self.aln.setReferenceSeq(seq)
            else:
                self.aln.setReferenceSeq(None)
            return True
        elif role == CustomRole.Included:
            seq.visibility = value
            return True
        elif role == CustomRole.ReplacementEdit:
            # value is a SeqSliceReplacement object
            to_replace = seq[col:col + value.num_to_replace]
            old_txt = "".join(
                " " if res.is_gap else str(res) for res in to_replace)
            if value.new_residues == old_txt:
                # Don't do anything if the single letter codes are the same so
                # that we don't lose any information (like three-letter residue
                # names for residues with single-letter codes of "X")
                return True
            mutation = (row_num, col, col + value.num_to_replace,
                        list(value.new_residues))
            self.aln.mutateResidues(*mutation, select=True)
            return True
        elif role == CustomRole.InsertionEdit:
            self.aln.addElements(seq, col, list(value), select=True)
            return True
        elif role == CustomRole.SeqExpanded:
            self.aln.setSeqExpanded(seq, value)
            # sequence expansion can affect data in the fixed columns but not in
            # the alignment view, so we don't need to emit a dataChanged signal
            # here.  fixedColumnDataChanged will be emitted in
            # _onSeqExpansionChanged if it's needed.
            return self.NO_DATA_CHANGED
        return False
[docs]    def annotationTypes(self):
        """
        Get the current annotation types
        :return: A tuple of:
              - The global annotation enum
              - The sequence annotation enum
        :rtype: tuple
        """
        try:
            return self.aln.global_annotations, self.aln.seq_annotations
        except AttributeError:
            return [], [] 
[docs]    def resetAnnotation(self, ann):
        for seq in self.aln:
            seq.annotations.resetAnnotation(ann) 
[docs]    def strucTitles(self):
        """
        Get all structure titles
        :return: A list containing all structure titles
        :rtype: list
        """
        struc_names = []
        for idx, seq in enumerate(self.aln, start=1):
            struc_names.append(
                seq.name if seq.name else 'Sequence {0}'.format(idx))
        return struc_names 
    def _alignmentNumColumnsAboutToChange(self, old_length, new_length):
        """
        Respond to the alignmentNumColumnsAboutToChange signal by getting ready to
        insert or remove columns
        :param old_length: The current length of the alignment
        :type old_length: int
        :param new_length: The new length of the alignment
        :type new_length: int
        """
        index = QtCore.QModelIndex()
        new_using_dummy_column = None
        if old_length == 0:
            # We're inserting "real" columns, which means we can get rid of our
            # dummy column
            old_length = self._column_count
            new_using_dummy_column = False
        if new_length == 0 and len(self.aln) > 0:
            # We're removing all "real" columns but the alignment still has
            # sequences in it, so we need to add a dummy column.
            new_using_dummy_column = True
            new_length = 1
        if new_length < old_length:
            self.beginRemoveColumns(index, new_length,
                                    old_length - ZERO_INDEXED)
        elif new_length > old_length:
            self.beginInsertColumns(index, old_length,
                                    new_length - ZERO_INDEXED)
        else:
            # We've swapped a real column for a dummy column or vice versa, so
            # we're not actually changing the number of columns
            self._ignoring_col_change = True
        if new_using_dummy_column is not None:
            self._using_dummy_column = new_using_dummy_column
    def _alignmentNumColumnsChanged(self, old_length, new_length):
        """
        Respond to the alignmentNumColumnsChanged signal by finishing inserting or
        removing columns
        :param old_length: The previous length of the alignment
        :type old_length: int
        :param new_length: The current length of the alignment
        :type new_length: int
        """
        if self._ignoring_col_change:
            # We swapped a real column for a dummy column or vice versa, so
            # we're not actually changing the number of columns
            self._ignoring_col_change = False
            return
        elif new_length == 0 and len(self.aln) > 0:
            # We've removed all "real" columns but the alignment still has
            # sequences in it, so we need to add a dummy column.
            self._column_count = 1
        else:
            self._column_count = new_length
        if new_length < old_length:
            self.endRemoveColumns()
        elif new_length > old_length:
            self.endInsertColumns()
    def _sequencesAboutToBeInserted(self, first_seq_index, last_seq_index):
        """
        Respond to the sequencesAboutToBeInserted signal by inserting a dummy
        column if needed and then emitting rowsAboutToBeInserted.
        :param first_seq_index: The index of the first inserted sequence.
        :type first_seq_index: int
        :param last_seq_index: The index of the last inserted sequence.
        :type last_seq_index: int
        """
        invalid_index = QtCore.QModelIndex()
        # If the alignment length was changing, the alignment would have already
        # emitted alignmentNumColumnsAboutToChange and alignmentNumColumnsChanged, so if
        # self._column_count is still 0 we know that all inserted sequences are
        # zero length, which means that we need to insert a dummy column.
        if self._column_count == 0:
            self.beginInsertColumns(invalid_index, 0, 0)
            self._column_count = 1
            self._using_dummy_column = True
            self.endInsertColumns()
        self.beginInsertRows(invalid_index, first_seq_index, last_seq_index)
    def _sequencesAboutToBeRemoved(self, first_seq_index, last_seq_index):
        """
        Respond to the sequencesAboutToBeRemoved signal by emitting
        rowsAboutToBeRemoved.
        :param first_seq_index: The index of the first removed sequence.
        :type first_seq_index: int
        :param last_seq_index: The index of the last removed sequence.
        :type last_seq_index: int
        """
        self.beginRemoveRows(QtCore.QModelIndex(), first_seq_index,
                             last_seq_index)
    def _sequencesRemoved(self, first_seq_index, last_seq_index):
        """
        Respond to the sequencesRemoved signal by emitting
        rowsRemoved and removing the dummy column if needed.
        :param first_seq_index: The index of the first removed sequence.
        :type first_seq_index: int
        :param last_seq_index: The index of the last removed sequence.
        :type last_seq_index: int
        """
        self.endRemoveRows()
        if self._using_dummy_column and len(self.aln) == 0:
            # We just removed the last sequences from the alignment, so we no
            # longer need the dummy column
            invalid_index = QtCore.QModelIndex()
            self.beginRemoveColumns(invalid_index, 0, 0)
            self._column_count = 0
            self._using_dummy_column = False
            self.endRemoveColumns()
    def _sequenceResiduesChanged(self):
        """
        Response to changes within a sequence by updating the appropriate cells
        """
        self._refreshAllData()
    def _sequenceNameChanged(self, seq):
        """
        Respond to a sequence name changing by updating the appropriate header
        row
        :param seq: The sequence that changed names
        :type seq: `schrodinger.protein.sequence.Sequence`
        """
        row = self.aln.index(seq)
        self.fixedColumnDataChanged.emit(CustomRole.RowTitle, row)
        self.fixedColumnDataChanged.emit(CustomRole.ChainCol, row)
    def _annotationTitleChanged(self, seq):
        """
        Respond to an annotation title changing by updating the appropriate
        header row
        :param seq: The sequence whose annotation changed names
        :type seq: `schrodinger.protein.sequence.Sequence`
        """
        row = self.aln.index(seq)
        self.fixedColumnDataChanged.emit(CustomRole.RowTitle, row)
        self.rowHeightChanged.emit()  # If the number of ligands changes
    def _sequenceStructureChanged(self, seq, seq_index):
        """
        Respond to a sequence structure changing by updating the appropriate
        cells.
        :param seq: The sequence whose structure changed
        :type seq: `schrodinger.protein.sequence.Sequence`
        :param seq_index: The index of the sequence whose structure changed
        :type seq_index: int
        """
        self.sequenceStructureChanged.emit()
        # Note that this emits dataChanged for *all* the cells in the row; if
        # this proves to be too slow we can optimize.
        left = self.index(seq_index, 0)
        right = self.index(seq_index, len(seq) - 1)
        self.dataChanged.emit(left, right)
[docs]    def onHomologyStatusChanged(self, seq):
        row = self.aln.index(seq)
        self.fixedColumnDataChanged.emit(CustomRole.HomologyStatus, row) 
[docs]    def setResSelectionState(self, selection, selected, current=False):
        """
        Mark the residues specified by `selection` as either selected or
        deselected.
        :param selection: A selection containing the entries to update.
        :type selection: `QtCore.QItemSelection`
        :param selected: Whether the residues should be selected (True) or
            deselected (False).
        :type selected: bool
        :param current: Whether this selection change should only affect the
            "current" selection.  Note that "current" here means "the portion of
            the selection that's in the process of being updated," i.e., the
            selection that's from the mouse click (or click and drag) that we're
            currently in the middle of.  This is equivalent to passing the
            `QItemSelectionModel::Current | QItemSelectionModel::Clear` flags to
            `QItemSelectionModel::select`.
        :type current: bool
        """
        if self._options_model.pick_mode is PickMode.HMBindingSite:
            return
        residues = set()
        for index in selection.indexes():
            row, col = index.row(), index.column()
            seq = self.aln[row]
            if col >= len(seq):
                continue
            res = seq[col]
            residues.add(res)
        self._handleResSelection(residues, selected, current) 
[docs]    def setResRangeSelectionState(self,
                                  from_index,
                                  to_index,
                                  selected,
                                  columns,
                                  current=False):
        """
        Mark all residues between `from_index` and `to_index` as either selected
        or deselected.
        :param from_index: The first index to select or deselect.
        :type from_index: QtCore.QModelIndex
        :param to_index: The last index to select or deselect.
        :type to_index: QtCore.QModelIndex
        :param selected: Whether the residues should be selected (True) or
            deselected (False).
        :type selected: bool
        :param columns: Whether all residues in the specified columns should be
            selected or deselected.
        :type columns: bool
        :param current: Whether these selection changes should only affect the
            "current" selection.  Note that "current" here means "the portion of
            the selection that's in the process of being updated," i.e., the
            selection that's from the mouse click (or click and drag) that we're
            currently in the middle of.  This is equivalent to passing the
            `QItemSelectionModel::Current | QItemSelectionModel::Clear` flags to
            `QItemSelectionModel::select`.
        :type current: bool
        """
        if not from_index.isValid() or not to_index.isValid():
            return
        if columns:
            seqs = self.aln
        else:
            from_row = from_index.row()
            to_row = to_index.row()
            if to_row < from_row:
                to_row, from_row = from_row, to_row
            seqs = (self.aln[i] for i in range(from_row, to_row + 1))
        from_col = from_index.column()
        to_col = to_index.column()
        if to_col < from_col:
            to_col, from_col = from_col, to_col
        residues = set()
        for seq in seqs:
            residues.update(seq[from_col:to_col + 1])
        self._handleResSelection(residues, selected, current) 
    def _onPickModeChanged(self, mode):
        """
        :type mode: picking.PickMode
        """
        if (mode is PickMode.HMChimera and
                not self.aln.homology_composite_residues):
            picking.handle_reset_pick(self.aln, mode)
        self._refreshAllData()
    def _handleResSelection(self, residues, selected, current=False):
        """
        :param residues: Change the state of these residues
        :type residues: set[residue.Residue]
        :param selected: Whether to select or deselect the residues
        :type selected: bool
        :param current: Whether these selection changes should only affect the
            "current" selection.  This only has an effect when not in picking
            mode.  Note that "current" here means "the portion of the selection
            that's in the process of being updated," i.e., the selection that's
            from the mouse click (or click and drag) that we're currently in the
            middle of.  This is equivalent to passing the
            `QItemSelectionModel::Current | QItemSelectionModel::Clear` flags to
            `QItemSelectionModel::select`.
        :type current: bool
        """
        if self._structure_model:
            split_aln = self._page_model.split_aln
            residues = list(residues)
            if (residues and isinstance(residues[0],
                                        residue.CombinedChainResidueWrapper)):
                residues = [res.split_res for res in residues]
            residues = self._structure_model.mapResidues(residues)
            residues = [res for res in residues if res.sequence in split_aln]
        pick_mode = self._options_model.pick_mode
        if pick_mode is not None:
            picking.handle_pick(self.aln, pick_mode, residues, selected)
        elif current:
            self.aln.res_selection_model.setCurrentSelectionState(
                residues, selected)
        else:
            self.aln.res_selection_model.setSelectionState(residues, selected)
[docs]    def setSeqSelectionState(self, selection, selected):
        seqs = {self.aln[index.row()] for index in selection.indexes()}
        self.aln.seq_selection_model.setSelectionState(seqs, selected) 
[docs]    def clearResSelection(self):
        self.aln.res_selection_model.clearSelection() 
[docs]    def handleBindingSitePick(self, index, ligand_idx):
        pick_mode = self._options_model.pick_mode
        if pick_mode is not PickMode.HMBindingSite:
            return
        res, seq, row, col = self._genDataArgs(index)
        lig_contacts = seq.getAnnotation(col, SEQ_ANNO_TYPES.binding_sites)
        lig_contact = lig_contacts[ligand_idx]
        if lig_contact is annotation.BINDING_SITE.NoContact:
            return
        aln = self.getAlignment()
        lig = seq.annotations.ligands[ligand_idx]
        picking.handle_pick(aln, pick_mode, res, lig) 
[docs]    def handleProximityPick(self, index):
        pick_mode = self._options_model.pick_mode
        if pick_mode is not PickMode.HMProximity:
            return
        res, *_ = self._genDataArgs(index)
        aln = self.getAlignment()
        picking.handle_pick(aln, pick_mode, res) 
[docs]    def expandSelectionToAnnotationValues(self, anno, ann_index):
        cdr_scheme = None
        if anno is SEQ_ANNO_TYPES.antibody_cdr:
            cdr_scheme = self._options_model_cache.antibody_cdr_scheme
        aln = self.getAlignment()
        aln.expandSelectionToAnnotationValues(anno, ann_index, cdr_scheme) 
[docs]    def getIndexForRes(self, res):
        """
        Get the model index for the given residue.
        :param res: A residue
        :type  res: schrodinger.protein.residue.Residue
        :return: The model index of the residue
        :rtype: QModelIndex
        """
        seq = res.sequence
        return self.index(self.aln.index(seq), seq.index(res)) 
[docs]    def getSelectedResIndices(self):
        """
        Get indices for all selected residues.
        :rtype: list[QtCore.QModelIndex]
        """
        selected = self.aln.res_selection_model.getSelectionIndices()
        return [self.index(seq_i, res_i) for (seq_i, res_i) in selected] 
[docs]    def isWorkspaceAln(self):
        """
        :return: Whether this model represents the workspace alignment.
        :rtype: bool
        """
        try:
            return self.aln.isWorkspace()
        except AttributeError:
            # The alignment isn't an undoable alignment, so it doesn't have an
            # isWorkspace method
            return False 
[docs]    def getResnumForColumn(self, seq, col_index):
        """
        Get cached, filtered display resnum for column.
        :param seq: The sequence
        :type  seq: schrodinger.protein.sequence.Sequence
        :param col_index: The index of the column to display
        :type  col_index: int
        :return: Formatted resnum for display
        :rtype: str or None
        """
        if self._cache.resnum is None:
            self._cache.resnum = IdDict()
        seq_resnums = self._cache.resnum.get(seq)
        if seq_resnums is None:
            seq_resnums = list(self._filterResnumData(seq))
            self._cache.resnum[seq] = seq_resnums
        return seq_resnums[col_index] 
    @classmethod
    def _filterResnumData(cls, seq):
        """
        Keep residue numbers that are divisible by RESNUM_INCR and at least
        RESNUM_INCR-1 residues apart.
        :param seq: The sequence
        :type  seq: schrodinger.protein.sequence.Sequence
        :return: Residue numbers for display
        :rtype: generator(str or None)
        """
        min_resnum_spacing = RESNUM_INCR - 1
        # Counter to track when a resnum was last shown - incremented so it can
        # return True in the first loop
        last_shown_idx = min_resnum_spacing + 1
        for res in seq:
            display_resnum = None
            if (res.is_res and res.resnum is not None and
                    res.resnum % RESNUM_INCR == 0 and
                    last_shown_idx > min_resnum_spacing):
                # Reset idx and format resnum for display
                last_shown_idx = 0
                display_resnum = str(res.resnum)
            yield display_resnum
            last_shown_idx += 1
    def _defaultFont(self):
        """
        Return the default font to be used.
        :return Default font
        :rtype: `QtGui.QFont`
        """
        return QtGui.QFont("Objective", weight=QtGui.QFont.Medium)
    def _updateFonts(self):
        """
        Create a font object in the current font size, and notify the view that
        font size has changed.
        """
        self._font = self._defaultFont()
        self._font.setPointSize(self._options_model_cache.font_size)
        invalid_index = QtCore.QModelIndex()
        self.dataChanged.emit(invalid_index, invalid_index)
        self.textSizeChanged.emit()
    def _getBaseResidueToolTip(self, res, seq):
        """
        Return a string containing residue and sequence information
        :param res: The residue for the element
        :type res: `schrodinger.protein.residue.Residue`
        :param seq: The sequence for the element
        :type seq: `schrodinger.protein.sequence.ProteinSequence`
        :rtype: str
        :return: Residue and sequence information
        """
        if res.chain:
            seq_chain_info = f"{seq.name} - Chain {res.chain}"
        else:
            seq_chain_info = seq.name
        res_info = f"{res.long_code} {res.resnum}{res.inscode}"
        tooltip = f"{res_info} ({seq_chain_info})"
        if seq.hasStructure() and res.is_res and not res.hasStructure():
            tooltip += " (structureless)"
        if res.long_code in residue.NON_STD_AA_TT_MAP:
            tooltip = f"<p style='display:inline;white-space:pre;'>{tooltip}<i> / {residue.NON_STD_AA_TT_MAP[res.long_code]}</i>"
        return tooltip
[docs]    def updateColorScheme(self, row_type, scheme):
        """
        Used to set the color scheme of a specific row type.
        """
        if isinstance(scheme, color.PositionScheme):
            scheme.setLength(self.getAlignment().num_columns)
        scheme = copy.deepcopy(scheme)
        if row_type is RowType.Sequence:
            # Consensus sequence is always the same color scheme as sequence
            self._color_scheme[ALN_ANNO_TYPES.consensus_seq] = scheme
            # update caches
            self._seq_color_scheme = scheme
            self._cache.average_color = None
        else:
            self._color_scheme[row_type] = scheme
        self._refreshAllData() 
[docs]    def getSeqColorScheme(self):
        """
        :return: The sequence color scheme currently in use
        :rtype: color.AbstractRowColorScheme
        """
        return copy.deepcopy(self._seq_color_scheme) 
[docs]    def updateResidueColors(self, key_to_color_map):
        """
        Update the colors of residues in the sequence rows.
        :param key_to_color_map: A map from residue keys to colors. Each color
            is represented by a tuple of (r, g, b) values.
        :type key_to_color_map: dict(residue.ResidueKey, tuple(int, int, int))
        """
        # Convert color map from keying by residue info to keying by residue
        res_to_color_map = {}
        for seq in self.aln:
            for res in seq.residues():
                key = res.getKey()
                color = key_to_color_map.get(key)
                if color is not None:
                    res_to_color_map[res] = color
        seq_scheme = self._seq_color_scheme
        seq_scheme.updateCustomResColors(res_to_color_map)
        self._refreshAllData() 
[docs]    def getResidueColors(self):
        """
        Get the color of residues in the sequence rows
        :return: The colors of each residue in the MSV. Each residue is
            represented by a `residue.ResidueKey` and each color is represented
            by a tuple of (r, g, b) values.
        :rtype key_to_color_map: dict(residue.ResidueKey, tuple(int, int, int))
        """
        color_map = {}
        for row, seq in enumerate(self.aln):
            if not seq.hasStructure():
                continue
            for col, res in enumerate(seq):
                if res.is_gap:
                    continue
                key = res.getKey()
                if key is None:
                    continue
                bbrush = self._sequenceBackgroundData(res, seq, row, col)
                if bbrush is not None:
                    r, g, b, _ = bbrush.color().getRgb()
                    color_map[key] = (r, g, b)
        return color_map 
[docs]    def onResHighlightChanged(self):
        self._residue_highlights = self.aln.getHighlightColorMap()
        self._refreshAllData() 
[docs]    def moveSelectionBelow(self, index):
        """
        Move all selected sequences immediately below the specified index.  Used
        for drag-and-drop.
        :param proxy_index: The index to move selected sequences below.
        :type proxy_index: QtCore.QModelIndex
        """
        if not index.isValid():
            raise RuntimeError("Invalid drop destination")
        after_seq = self.aln[index.row()]
        self.aln.moveSelectedSequences(after_seq) 
[docs]    def getAdjacentIndexForEditing(self, index, direction):
        """
        Return a new index next to the specified index in the given direction.
        An invalid index will be returned if::
          - No such index exists (i.e. asking for a residue above the first
          sequence in the alignment)
          - The new index refers to a structured residue (and therefore isn't
          editable)
          - The new index refers to a position that's past the end of a
          sequence (and therefore isn't editable)
        :param index: The starting index
        :type index: QtCore.QModelIndex
        :param direction: Which direction to go in
        :type direction: Adjacent
        :return: The new index
        :rtype: QtCore.QModelIndex
        """
        row = index.row()
        col = index.column()
        if direction is Adjacent.Up:
            row -= 1
        elif direction is Adjacent.Down:
            row += 1
        elif direction is Adjacent.Left:
            col -= 1
        elif direction is Adjacent.Right:
            col += 1
        else:
            raise ValueError("Invalid direction")
        if row < 0 or col < 0:
            return QtCore.QModelIndex()
        try:
            seq = self.aln[row]
            res = seq[col]
        except IndexError:
            return QtCore.QModelIndex()
        if seq.hasStructure() and res.hasStructure():
            return QtCore.QModelIndex()
        return self.index(row, col) 
[docs]    def isSingleBlockSelected(self):
        """
        Is a single block of residues (i.e. contiguous residues/gaps from one
        sequence or multiple adjacent sequences) selected?
        :rtype: bool
        """
        return self.aln.res_selection_model.isSingleBlockSelected() 
[docs]    def alnSetResSelected(self):
        """
        Whether any selected residues are in sequences in an alignment set
        :rtype: bool
        """
        return self.aln.alnSetResSelected() 
[docs]    def initializeCustomFonts(self):
        """
        Initializes custom fonts used in the viewmodel
        """
        custom_font_paths = [
            ":/msv/fonts/Objective-Black-Italic.otf",
            ":/msv/fonts/Objective-Black.otf",
            ":/msv/fonts/Objective-Bold-Italic.otf",
            ":/msv/fonts/Objective-Bold.otf",
            ":/msv/fonts/Objective-ExtraBold-Italic.otf",
            ":/msv/fonts/Objective-ExtraBold.otf",
            ":/msv/fonts/Objective-Italic.otf",
            ":/msv/fonts/Objective-Light-Italic.otf",
            ":/msv/fonts/Objective-Light.otf",
            ":/msv/fonts/Objective-Medium-Italic.otf",
            ":/msv/fonts/Objective-Medium.otf",
            ":/msv/fonts/Objective-Regular.otf",
            ":/msv/fonts/Objective-Super-Italic.otf",
            ":/msv/fonts/Objective-Super.otf",
            ":/msv/fonts/Objective-Thin-Italic.otf",
            ":/msv/fonts/Objective-Thin.otf",
        ]
        global _font_added
        if not _font_added:
            for font_path in custom_font_paths:
                success = QtGui.QFontDatabase.addApplicationFont(font_path)
                if success == -1:
                    raise RuntimeError(
                        f"Custom font could not be loaded at {font_path}")
            _font_added = True  
[docs]class OptionsModelCache(object):
    """
    An object used to store display options that `SequenceAlignmentModel` needs
    access to. See `SequenceAlignmentModel._setOptionsModel` for additonal
    information. This class is required because we cannot set attributes on
    instances of `object`, as instances of that class do not have a `__dict__`
    attribute. Subclasses of `object` don't have that restriction.
    """ 
    # This class intentionally left blank
[docs]class DropProxyMixin:
    """
    A mixin for proxies involved in drag-and-drop.
    """
[docs]    def moveSelectionBelow(self, proxy_index):
        """
        Move all selected sequences immediately below the specified index.  Used
        for drag-and-drop.
        :param proxy_index: The index to move selected sequences below.
        :type proxy_index: QtCore.QModelIndex
        """
        source_index = self.mapToSource(proxy_index)
        self.sourceModel().moveSelectionBelow(source_index)  
[docs]class MouseOverPassthroughMixin:
    """
    A mixin for proxies involved in mouse-over state.
    """
[docs]    def setMouseOverIndex(self, proxy_index):
        source_index = (self.mapToSource(proxy_index)
                        if proxy_index is not None else None)
        self.sourceModel().setMouseOverIndex(source_index) 
[docs]    def setContextOverIndex(self, proxy_index):
        source_index = (self.mapToSource(proxy_index)
                        if proxy_index is not None else None)
        self.sourceModel().setContextOverIndex(source_index) 
[docs]    def isMouseOverIndex(self, proxy_index):
        source_index = self.mapToSource(proxy_index)
        return self.sourceModel().isMouseOverIndex(source_index)  
[docs]class AnnotationSelectionPassthroughMixin:
[docs]    def setAnnSelectionState(self, proxy_index, selected):
        source_index = self.mapToSource(proxy_index)
        self.sourceModel().setAnnSelectionState(source_index, selected)  
[docs]class GetAlignmentProxyMixin:
    """
    A mixin for proxies that fetches the alignment through their source model.
    This proxy also caches the source model in Python to avoid a C++ call when
    calling sourceModel().  Used in both the fixed and scrollable columns.
    """
[docs]    def __init__(self, *args, **kwargs):
        self._source_model = None
        super().__init__(*args, **kwargs) 
[docs]    def setSourceModel(self, model):
        # See QAbstractItemView documentation for method documentation
        super().setSourceModel(model)
        self._source_model = model 
[docs]    def sourceModel(self):
        # See QAbstractItemView documentation for method documentation
        return self._source_model 
[docs]    def getAlignment(self):
        """
        Return the underlying alignment
        :return: The alignment if the source model exists; else None
        :rtype: schrodinger.protein.alignment.BaseAlignment
        """
        if self._source_model is not None:
            return self._source_model.getAlignment()  
[docs]class SeqExpansionProxyMixin:
    """
    A mixin for tree models that transmit sequence expansion information to
    their view.  Used in both the fixed and scrollable columns.
    """
    seqExpansionChanged = QtCore.pyqtSignal(list, bool)
[docs]    def setSourceModel(self, model):
        # See QAbstractItemView documentation for method documentation
        old_model = self.sourceModel()
        signals = {'seqExpansionChanged': self._seqExpansionChanged}
        if old_model is not None:
            table_helper.disconnect_signals(old_model, signals)
        super(SeqExpansionProxyMixin, self).setSourceModel(model)
        table_helper.connect_signals(model, signals) 
    def _seqExpansionChanged(self, source_indices, expanded):
        """
        Pass along a seqExpansionChanged signal from the source model after
        mapping the indices to be expanded or collapsed.
        :param source_indices: A list of all source model indices
            (`QtCore.QModelIndex`) to expand or collapse.
        :type source_indices: list
        :param expanded: True if the group should be expanded.  False if it
            should be collapsed.
        :type expanded: bool
        """
        proxy_indices = list(map(self.mapFromSource, source_indices))
        self.seqExpansionChanged.emit(proxy_indices, expanded) 
[docs]class ProxyMixin(SeqExpansionProxyMixin, DropProxyMixin, GetAlignmentProxyMixin,
                 table_speed_up.MultipleRolesRoleProxyPassthroughMixin):
    """
    A mixin class that provides functionality common to all the annotation
    proxies (but not the fixed column proxies).
    We also cache the sourceModel to avoid going through the C++ layer.
    Note that this mixin should be listed first in the parents for the proxy
    classes that use it.
    :ivar fixedColumnDataChanged: Signal emitted when the data in a fixed column
        is changed. Passes a tuple of the role and row index that are changing.
    :vartype fixedColumnDataChanged: `QtCore.pyqtSignal` emitting a tuple of
        (`enum.Enum`, int)
    """
    residueFormatChanged = QtCore.pyqtSignal()
    residueSelectionChanged = QtCore.pyqtSignal()
    fixedColumnDataChanged = QtCore.pyqtSignal(int, int)
    rowHeightChanged = QtCore.pyqtSignal()
    textSizeChanged = QtCore.pyqtSignal()
    predictionsChanged = QtCore.pyqtSignal()
    secondaryStructureChanged = QtCore.pyqtSignal()
    domainsChanged = QtCore.pyqtSignal()
    alnSetChanged = QtCore.pyqtSignal()
    kinaseFeaturesChanged = QtCore.pyqtSignal()
    kinaseConservationChanged = QtCore.pyqtSignal()
    sequenceStructureChanged = QtCore.pyqtSignal()
[docs]    def getIndexForRes(self, res):
        """
        Get the model index for the given residue.
        :param res: A residue
        :type  res: schrodinger.protein.residue.Residue
        :return: The model index of the residue
        :rtype: QModelIndex
        """
        source_index = self.sourceModel().getIndexForRes(res)
        return self.mapFromSource(source_index) 
[docs]    def setResSelectionState(self, selection, selected, current=False):
        """
        See `SequenceAlignmentModel.setResSelectionState` for method
        documentation.
        """
        source_selection = self.mapSelectionToSource(selection)
        self.sourceModel().setResSelectionState(source_selection, selected,
                                                current) 
[docs]    def setResRangeSelectionState(self,
                                  from_index,
                                  to_index,
                                  selected,
                                  columns,
                                  current=False):
        """
        See `SequenceAlignmentModel.setResRangeSelectionState` for method
        documentation.
        """
        from_source_index = self.mapToSource(from_index)
        to_source_index = self.mapToSource(to_index)
        self.sourceModel().setResRangeSelectionState(from_source_index,
                                                     to_source_index, selected,
                                                     columns, current) 
[docs]    def setSeqSelectionState(self, selection, selected):
        """
        Mark the sequences specified by `selection` as either selected or
        deselected.
        :param selection: A selection containing the entries to update.
        :type selection: QtCore.QItemSelection
        :param selected: Whether the entries should be selected (True) or
            deselected (False).
        :type selected: bool
        """
        source_selection = self.mapSelectionToSource(selection)
        self.sourceModel().setSeqSelectionState(source_selection, selected) 
[docs]    def clearResSelection(self):
        self.getAlignment().res_selection_model.clearSelection() 
[docs]    def clearSeqSelection(self):
        self.getAlignment().seq_selection_model.clearSelection() 
[docs]    def getResSelection(self):
        return self.getAlignment().res_selection_model.getSelection() 
[docs]    def getSelectedResIndices(self):
        """
        Get indices for all selected residues.
        :rtype: list[QtCore.QModelIndex]
        """
        source_indices = self.sourceModel().getSelectedResIndices()
        return list(map(self.mapFromSource, source_indices)) 
[docs]    def getSingleLetterCodeForSelectedResidues(self):
        """
        Get the single letter code for all selected residues.  This method
        assumes that a single block of residues from a single sequence is
        selected.
        :rtype: str
        """
        selection = self.getAlignment().res_selection_model.getSelection()
        selection = sorted(selection, key=lambda res: res.idx_in_seq)
        return "".join(" " if res.is_gap else str(res) for res in selection) 
[docs]    def handlePick(self, proxy_index):
        source_index = self.mapToSource(proxy_index)
        self.sourceModel().handlePick(source_index) 
[docs]    def expandSelectionToAnnotationValues(self, anno, ann_index):
        self.sourceModel().expandSelectionToAnnotationValues(anno, ann_index) 
[docs]    def setSourceModel(self, model):
        # See Qt documentation for argument documentation
        old_model = self.sourceModel()
        signals = {
            'residueFormatChanged': self.residueFormatChanged,
            'residueSelectionChanged': self.residueSelectionChanged,
            'fixedColumnDataChanged': self._sourceFixedColumnDataChanged,
            'rowHeightChanged': self.rowHeightChanged,
            'textSizeChanged': self.textSizeChanged,
            'predictionsChanged': self.predictionsChanged,
            'kinaseFeaturesChanged': self.kinaseFeaturesChanged,
            'kinaseConservationChanged': self.kinaseConservationChanged,
            'secondaryStructureChanged': self.secondaryStructureChanged,
            'alnSetChanged': self.alnSetChanged,
            'domainsChanged': self.domainsChanged,
            'sequenceStructureChanged': self.sequenceStructureChanged,
        }
        if old_model is not None:
            table_helper.disconnect_signals(old_model, signals)
        table_helper.connect_signals(model, signals)
        self.sequenceCount = model.sequenceCount
        self.annotationTypes = model.annotationTypes
        self.resetAnnotation = model.resetAnnotation
        super().setSourceModel(model) 
[docs]    def getResidueDisplayMode(self):
        """
        Return the current residue display mode setting.
        :return: The current residue display mode.
        :rtype: `enum.Enum`
        """
        return self.sourceModel().getResidueDisplayMode() 
    @QtCore.pyqtSlot(int, int)
    def _sourceFixedColumnDataChanged(self, role, source_row):
        """
        Emit fixedColumnDataChanged in response to receiving the same
        signal from the source model.
        :param role: The role that data has changed for.
        :type role: int
        :param source_row: The source row that data has changed for.
        :type source_row: int
        """
        source_index = self.sourceModel().index(source_row, 0)
        proxy_index = self.mapFromSource(source_index)
        proxy_row = proxy_index.row()
        if proxy_row >= 0:
            self.fixedColumnDataChanged.emit(role, proxy_row)
[docs]    def isWorkspaceAln(self):
        """
        :return: Whether this model represents the workspace alignment.
        :rtype: bool
        """
        return self.sourceModel().isWorkspaceAln() 
[docs]    def getFont(self):
        """
        :return: The current font.
        :rtype: QtGui.QFont
        """
        return self.sourceModel().getFont() 
[docs]    def getAdjacentIndexForEditing(self, proxy_index, direction):
        """
        See `SequenceAlignmentModel.getAdjacentIndexForEditing` for method
        documentation.
        """
        source_index = self.mapToSource(proxy_index)
        source_adjacent = self.sourceModel().getAdjacentIndexForEditing(
            source_index, direction)
        return self.mapFromSource(source_adjacent) 
[docs]    def isSingleBlockSelected(self):
        """
        Is a single block of residues (i.e. contiguous residues/gaps from one
        sequence or multiple adjacent sequences) selected?
        :rtype: bool
        """
        return self.sourceModel().isSingleBlockSelected() 
[docs]    def alnSetResSelected(self):
        """
        Whether any selected residues are in sequences in an alignment set
        :rtype: bool
        """
        return self.sourceModel().alnSetResSelected()  
# Data structures used to record information about row insertions and removals
# in NestedProxy.
_ROW_INDEL_FIELDS = ("parent_row", "parent_int_id", "start", "end")
RowInsertionInfo = collections.namedtuple("RowInsertionInfo", _ROW_INDEL_FIELDS)
RowRemovalInfo = collections.namedtuple("RowRemovalInfo", _ROW_INDEL_FIELDS)
[docs]class NestedProxy(ModelMixin, QtCore.QAbstractProxyModel):
    """
    A base class for proxy models that contain one level of nesting.  The
    internal ID of indices will be either `TOP_LEVEL` or the row number of the
    parent row.
    Note that QAbstractItemModel assumes that internal IDs are pointers to
    objects that represent the parent row.  As such, it never updates internal
    IDs when updating persistent model indices.  Because of this, this class
    reimplements beginInsertRows, endInsertRows, beginRemoveRows, and
    endRemoveRows to correctly update internal IDs.  These methods aren't
    virtual in QAbstractItemModel, but they're only called from Python in all
    NestedProxy subclasses so our reimplementations get called instead of the
    QAbstractItemModel implementations.
    This reimplementation follows the general pattern of the QAbstractItemModel
    implementation in order to minimize differences with QAbstractItemModel and
    hopefully avoid any potential issues caused by the change.  As such, we
    update the persistent indices in endInsertRows/endRemoveRows instead of
    beginInsertRows/beginRemoveRows, and we keep a stack of row
    insertion/removal requests.  In theory, this allows for nested row
    insertions and removals.  In practice, Qt's support for nested row
    insertions and removals is spotty.  QSortFilterProxyModels don't support
    them, and there's no documentation on the various limitations and
    requirements when using other models and proxies.  Properly handling nested
    row insertions and removals also tends to increase in complexity with the
    depth of the proxy stack, so we intentionally avoid them in all of the MSV
    viewmodels.
    """
[docs]    def __init__(self, parent=None):
        super().__init__(parent)
        # The stack of row insertion and deletion information
        self._indel_stack = [] 
[docs]    def index(self, row, column, parent=None):
        # See Qt documentation for method documentation
        if not (0 <= column < self.columnCount() and
                0 <= row < self.rowCount(parent)):
            return QtCore.QModelIndex()
        if parent is None or not parent.isValid():
            internal_id = TOP_LEVEL
        elif parent.internalId() != TOP_LEVEL:
            return QtCore.QModelIndex()
        else:
            internal_id = parent.row()
        return self.createIndex(row, column, internal_id) 
[docs]    def parent(self, index):
        # See Qt documentation for method documentation
        if not index.isValid():
            return QtCore.QModelIndex()
        internal_id = index.internalId()
        if internal_id == TOP_LEVEL:
            return QtCore.QModelIndex()
        else:
            return self.createIndex(internal_id, 0, TOP_LEVEL) 
[docs]    def hasChildren(self, parent):
        # See Qt documentation for method documentation
        return self.rowCount(parent) > 0 
[docs]    def buddy(self, index):
        # See Qt documentation for method documentation
        return index 
[docs]    def beginInsertRows(self, parent, start, end):
        # See Qt documentation for method documentation
        assert 0 <= start <= end
        info = RowInsertionInfo(parent.row(), parent.internalId(), start, end)
        self._indel_stack.append(info)
        self.rowsAboutToBeInserted.emit(parent, start, end) 
[docs]    def beginRemoveRows(self, parent, start, end):
        # See Qt documentation for method documentation
        assert 0 <= start <= end
        info = RowRemovalInfo(parent.row(), parent.internalId(), start, end)
        self._indel_stack.append(info)
        self.rowsAboutToBeRemoved.emit(parent, start, end) 
[docs]    def endInsertRows(self):
        # See Qt documentation for method documentation
        info = self._indel_stack.pop()
        assert isinstance(info, RowInsertionInfo)
        inserted_count = info.end - info.start + 1
        self._endIndelRows(info, inserted_count, self.rowsInserted) 
[docs]    def endRemoveRows(self):
        # See Qt documentation for method documentation
        info = self._indel_stack.pop()
        assert isinstance(info, RowRemovalInfo)
        delta = -(info.end - info.start + 1)
        self._endIndelRows(info, delta, self.rowsRemoved) 
    def _endIndelRows(self, info, delta, signal):
        """
        Update persistent indices before emitting rowsInserted or rowsRemoved.
        :param info: The RowInsertionInfo or RowRemovalInfo object describing
            the insertion or removal.
        :type info: RowInsertionInfo or RowRemovalInfo
        :param delta: The number of rows that were inserted or removed.  Should
            be negative if rows were removed.
        :type delta: int
        :param signal: The signal to emit.
        :type signal: QtCore.pyqtSignal
        """
        new_pindices = []
        old_pindices = []
        for pindex in self.persistentIndexList():
            new_pindex = None
            pindex_row = pindex.row()
            pindex_int_id = pindex.internalId()
            if pindex_row < 0:
                # The persistent index is already invalid
                continue
            if info.parent_row < 0:
                group_matches = pindex_int_id == TOP_LEVEL
            else:
                group_matches = pindex_int_id == info.parent_row
            if group_matches:
                # This persistent index is in the same group that the indel
                # happened in.
                if delta < 0 and info.start <= pindex_row <= info.end:
                    # This persistent index is one of the rows that was removed
                    new_pindex = QtCore.QModelIndex()
                elif pindex_row >= info.start:
                    # This persistent index is after the indel, so we have to
                    # update the row number of index
                    new_pindex = self.createIndex(pindex_row + delta,
                                                  pindex.column(),
                                                  pindex_int_id)
            elif info.parent_row < 0 and pindex_int_id != TOP_LEVEL:
                # The indel was into the top level and this persistent index is
                # a child row
                if delta < 0 and info.start <= pindex_int_id <= info.end:
                    # This persistent index's parent row was removed
                    new_pindex = QtCore.QModelIndex()
                elif info.start <= pindex_int_id:
                    # This persistent index's parent row is after the indel, so
                    # we have to update the internal id
                    new_pindex = self.createIndex(pindex_row, pindex.column(),
                                                  pindex_int_id + delta)
            if new_pindex is not None:
                old_pindices.append(pindex)
                new_pindices.append(new_pindex)
        if old_pindices:
            self.changePersistentIndexList(old_pindices, new_pindices)
        if info.parent_row < 0:
            parent = QtCore.QModelIndex()
        else:
            parent = self.createIndex(info.parent_row, 0, info.parent_int_id)
        signal.emit(parent, info.start, info.end) 
# Lists of sequence numbers used in AnnotationProxyModel when rows are
# grouped by annotation.  `all` contains sequence numbers of all sequences.
# `structured_only` contains sequence numbers only for those sequences that
# have an associated structure.
SequenceNums = collections.namedtuple("SequenceNums",
                                      ("all", "structured_only"))
[docs]class SequenceInfo(object):
    """
    Information about a single sequence.  Used in `AnnotationProxyModel`.
    """
[docs]    def __init__(self, has_struc=None, anns=None):
        """
        :param has_struc: Whether the sequence has an associated structure.
            May be `None` while the sequence is in the process of being added.
        :type has_struc: bool or NoneType
        :param anns: A list of the currently shown sequence annotations
            for this sequence.  Only populated when `AnnotationProxyModel` is
            grouping rows by sequence.  Will be `None` if the rows are grouped by
            annotation.
        :type anns: list or NoneType
        """
        self.has_struc = has_struc
        if anns is None:
            anns = []
        self.anns = anns  
[docs]class GroupByAnnotationInfo(object):
    """
    Information about a sequence annotation.  Used in `AnnotationProxyModel`.
    """
[docs]    def __init__(self, ann, seq_numbers, ann_indexes=None):
        """
        :param ann: The annotation type
        :type ann: `enum.Enum`
        :param seq_numbers: A list of sequences (represented by an integer
            index) to display for this annotation.
        :type seq_numbers: list
        :param ann_indexes: A tuple of tuples of annotation indexes for
            multi-value annotations (may have multiple rows per sequence)
            or None for standard annotations (one row per sequence).
        :type ann_indexes: tuple[tuple[int]] or NoneType
        """
        self._source_row_map = None
        self._ann = ann
        self.seqs = seq_numbers
        self._ann_indexes = ann_indexes 
    @property
    def ann(self):
        return self._ann
    @property
    def ann_indexes(self):
        return self._ann_indexes
    @ann_indexes.setter
    def ann_indexes(self, value):
        self._ann_indexes = value
        self._source_row_map = None
[docs]    def __len__(self):
        """
        Return the number of rows needed to display this annotation for all
        sequences. For standard annotations, equal to the number of sequences.
        """
        if self.ann_indexes is None:
            return len(self.seqs)
        else:
            return sum(len(sublist) for sublist in self.ann_indexes) 
[docs]    def getSourceRowInfo(self, proxy_row):
        """
        Map `proxy_row` to the correct source row and annotation index.
        :return: source row and multi-row annotation index. Annotation index
            will be None for standard annotations.
        :rtype: tuple(int, int or NoneType)
        """
        assert proxy_row >= 0
        if self.ann_indexes is None:
            return (self.seqs[proxy_row], None)
        else:
            if self._source_row_map is None:
                source_row_map = []
                for _source_row_idx, ann_indexes in enumerate(self.ann_indexes):
                    for _ann_idx in ann_indexes:
                        source_row_map.append((_source_row_idx, _ann_idx))
                self._source_row_map = source_row_map
            source_row_idx, ann_idx = self._source_row_map[proxy_row]
            return (self.seqs[source_row_idx], ann_idx) 
[docs]    def getProxyRowStart(self, source_row):
        """
        Return the proxy row number representing the first row *after* any
        proxy rows corresponding to the given source row. This is used when a
        new source sequence is being inserted after source_row (which must
        already be present in `self.seqs`). This method will return the proxy
        index where insertion will start.
        :rtype: int
        """
        if self.ann_indexes is None:
            return source_row
        source_row_idx = self.seqs.index(source_row)
        proxy_row_start = 0
        for idx in range(source_row_idx + 1):
            ann_idxs = self.ann_indexes[idx]
            proxy_row_start += len(ann_idxs)
        return proxy_row_start  
[docs]class PerRowFlagCacheProxyMixin(table_speed_up.AbstractFlagCacheProxyMixin):
    """
    A mixin to cache flags on a per-row basis instead of per-cell (which is
    what `table_speed_up.FlagCacheProxyMixin` does) since flags are the same
    across rows here.
    :note: This mixin assumes that all indices in the same row have the same
        internal ID. This is true for any NestedProxy subclasses, but is *not*
        generally true for Qt tree models.
    """
[docs]    def flags(self, index):
        # See Qt documentation for method documentation
        index_hashable = (index.row(), index.internalId())
        try:
            return self._flag_cache[index_hashable]
        except KeyError:
            flag = super(PerRowFlagCacheProxyMixin, self).flags(index)
            self._flag_cache[index_hashable] = flag
            return flag  
[docs]class SequenceFilterProxyModel(PerRowFlagCacheProxyMixin, ProxyMixin,
                               NestedProxy):
    """
    A proxy model to hide certain sequences. Used by "Find sequence in list."
    """
[docs]    def __init__(self, parent=None):
        super().__init__(parent)
        self._row_visibilities = None
        self._source_proxy_indexes = None
        self._proxy_source_indexes = None
        self._row_count = 0
        self._column_count = 0
        self._is_active = False
        self._invalidate_row_timer = QtCore.QTimer(self)
        self._invalidate_row_timer.setSingleShot(True)
        self._invalidate_row_timer.timeout.connect(self._invalidateRowFilter) 
[docs]    def setActive(self, active):
        if not active:
            self._invalidate_row_timer.stop()
        refresh = active and self._is_active is False
        self._is_active = active
        if refresh:
            self._invalidateRowFilter() 
    def _onHiddenSeqsChanged(self):
        self._cacheRowVisibilities()
        self._invalidate_row_timer.stop()  # Prevent double call
        self._invalidateRowFilter()
[docs]    @table_helper.model_reset_method
    def setSourceModel(self, model):
        super().setSourceModel(model)
        # Passthrough signals
        model.columnsAboutToBeInserted.connect(self.beginInsertColumns)
        model.columnsInserted.connect(self.endInsertColumns)
        model.columnsAboutToBeRemoved.connect(self.beginRemoveColumns)
        model.columnsRemoved.connect(self.endRemoveColumns)
        model.layoutAboutToBeChanged.connect(self.layoutAboutToBeChanged)
        model.modelAboutToBeReset.connect(self.beginResetModel)
        # Slots
        model.modelReset.connect(self._onSourceModelReset)
        model.dataChanged.connect(self._onSourceDataChanged)
        model.layoutChanged.connect(self._onSourceLayoutChanged)
        # Can ignore rowsAboutToBeInserted because this model does a delayed
        # reset whenever rows are inserted
        model.rowsInserted.connect(self._onSourceRowsInserted)
        model.rowsAboutToBeRemoved.connect(self._onSourceRowsAboutToBeRemoved)
        model.rowsRemoved.connect(self._onSourceRowsRemoved)
        model.fixedColumnDataChanged.connect(self._onFixedColumnDataChanged)
        model.hiddenSeqsChanged.connect(self._onHiddenSeqsChanged)
        self._initializeBookkeeping() 
    # # # # # # # # # # # # # # # # # # # # # # #
    # Implementations of Qt pure virtual methods
    # # # # # # # # # # # # # # # # # # # # # # #
[docs]    def mapFromSource(self, source_index):
        if not source_index.isValid():
            return QtCore.QModelIndex()
        proxy_row = self._source_proxy_indexes[source_index.row()]
        if proxy_row is None:
            return QtCore.QModelIndex()
        return self.index(proxy_row, source_index.column()) 
[docs]    def mapToSource(self, proxy_index):
        if not proxy_index.isValid():
            return QtCore.QModelIndex()
        source_row = self._proxy_source_indexes[proxy_index.row()]
        return self.sourceModel().index(source_row, proxy_index.column()) 
[docs]    def columnCount(self, parent=None):
        return self._column_count 
[docs]    def rowCount(self, parent=None):
        if parent is None or not parent.isValid():
            return self._row_count
        else:
            return 0 
    # # # # # # # # # # # # # # # # # # # # # # #
    # Implementations of MSV pure virtual methods
    # # # # # # # # # # # # # # # # # # # # # # #
[docs]    def rowData(self, proxy_row, cols, roles):
        source_row = self._proxy_source_indexes[proxy_row]
        return self.sourceModel().rowData(source_row, cols, roles) 
    # # # # # # # # # # # # # # # # # # # # # # #
    # Reimplementations of Qt virtual methods
    # # # # # # # # # # # # # # # # # # # # # # #
[docs]    def endInsertColumns(self):
        # See Qt documentation for method documentation
        self._column_count = self.sourceModel().columnCount()
        super().endInsertColumns() 
[docs]    def endRemoveColumns(self):
        # See Qt documentation for method documentation
        self._column_count = self.sourceModel().columnCount()
        super().endRemoveColumns() 
    # # # # # # # # # # # # # # # # # # # # # # #
    # Slots for Qt signals
    # # # # # # # # # # # # # # # # # # # # # # #
    def _onSourceModelReset(self):
        self._initializeBookkeeping()
        self.endResetModel()
    def _onSourceLayoutChanged(self):
        self._initializeBookkeeping()
        self._invalidateAllPersistentIndices()
        self.layoutChanged.emit()
    def _onSourceDataChanged(self, top_left, bottom_right):
        if self._invalidate_row_timer.isActive():
            return
        if not top_left.isValid() and not bottom_right.isValid():
            self.dataChanged.emit(top_left, bottom_right)
            return
        top_row, top_col = top_left.row(), top_left.column()
        bottom_row, bottom_col = bottom_right.row(), bottom_right.column()
        top_row = self._source_proxy_indexes[top_row]
        proxy_top_left = self.index(top_row, top_col)
        bottom_row = self._source_proxy_indexes[bottom_row]
        proxy_bottom_right = self.index(bottom_row, bottom_col)
        self.dataChanged.emit(proxy_top_left, proxy_bottom_right)
    def _onSourceRowsInserted(self, parent, start, end):
        self._onFixedColumnDataChanged()
    def _onSourceRowsAboutToBeRemoved(self, parent, start, end):
        proxy_start, proxy_end = self._getAcceptedRowRange(start, end)
        if proxy_start is None:
            return
        self.beginRemoveRows(parent, proxy_start, proxy_end)
    def _onSourceRowsRemoved(self, parent, start, end):
        proxy_start, proxy_end = self._getAcceptedRowRange(start, end)
        if proxy_start is None:
            return
        del self._row_visibilities[start:end + 1]
        self._cacheSourceProxyIndexes()
        self.endRemoveRows()
    # # # # # # # # # # # # # # # # # # # # # # #
    # Private helper methods
    # # # # # # # # # # # # # # # # # # # # # # #
    def _getAcceptedRowRange(self, start, end):
        accepted_children = self._proxy_source_indexes
        if not accepted_children:
            return None, None
        proxy_start = bisect.bisect_left(accepted_children, start)
        if proxy_start == len(accepted_children):
            return None, None
        proxy_end = bisect.bisect_right(accepted_children, end) - 1
        if proxy_end == -1 or proxy_end < proxy_start:
            return None, None
        return proxy_start, proxy_end
    def _onFixedColumnDataChanged(self):
        self._initializeBookkeeping()
        if self._is_active:
            self._invalidate_row_timer.start()
    def _invalidateRowFilter(self):
        self.beginResetModel()
        self.endResetModel()
    def _initializeBookkeeping(self):
        """
        Clear and re-populate all of the bookkeeping data needed for this proxy.
        """
        source_model = self.sourceModel()
        self._column_count = source_model.columnCount()
        self._cacheRowVisibilities()
    def _cacheRowVisibilities(self):
        source_model = self.sourceModel()
        self._row_visibilities = source_model.getRowVisibilities()
        self._cacheSourceProxyIndexes()
    def _cacheSourceProxyIndexes(self):
        source_proxy_indexes = []
        proxy_source_indexes = []
        seen_visible = 0
        for source_index, vis in enumerate(self._row_visibilities):
            if vis:
                proxy_index = seen_visible
                proxy_source_indexes.append(source_index)
                seen_visible += 1
            else:
                proxy_index = None
            source_proxy_indexes.append(proxy_index)
        self._source_proxy_indexes = source_proxy_indexes
        self._proxy_source_indexes = proxy_source_indexes
        self._row_count = seen_visible 
[docs]class AnnotationProxyModel(PerRowFlagCacheProxyMixin,
                           table_speed_up.MultipleRolesRoleProxyMixin,
                           ProxyMixin, NestedProxy):
    """
    A proxy model that creates children rows for currently displayed annotations
    and adds a spacer row in between sequences.  This proxy can be toggled
    between grouping rows by sequence and grouping rows by annotation.
    :cvar groupByChanged: A signal emitted when the model is toggled between
        group-by-sequence and group-by-annotation.  Emitted with the new
        group-by setting (`GroupBy`).
    :vartype groupByChanged: `QtCore.pyqtSignal`
    :cvar tableWidthChangedSignal: A signal emitted when the table width is
        changed.  This signal is emitted in response to the view calling
        `tableWidthChanged`.
    :vartype tableWidthChangedSignal: `QtCore.pyqtSignal`
    :cvar ROLE_MAPPINGS: A dictionary for mapping Qt roles from view roles to
        `SequenceAlignmentModel` roles. Because `SequenceAlignmentModel`
        doesn't have separate rows for annotations, it uses different role
        numbers to differentiate between sequences, sequence annotations, and
        global annotations. This dictionary is formatted as {view role: (role
        for sequences, role base for sequence annotations, role base for global
        annotations)}. See `_mappedRole` for additional information. :vartype
        ROLE_MAPPINGS: dict(int, tuple(int or None, int, int))
     :cvar ROW_HEIGHT_SCALE_ANNS: Sequence annotations for which we should
         request `CustomRole.RowHeightScale` data from `SequenceAlignmentModel`.
         A row height scale of 1 will be returned for all other row types.
     :vartype ROW_HEIGHT_SCALE_ANNS: tuple(int)
     :cvar NO_GLOBAL_DATA_ROLES: Roles that should not return any data for
         global annotation rows.  Otherwise, global annotation rows will be
         mapped to the 0th row in `SequenceAlignmentModel` (i.e. the reference
         sequence row) and data will be fetched as normal.
     :vartype NO_GLOBAL_DATA_ROLES: set(int)
    :ivar _all_global_ann: An enum containing all global annotations present in
        the alignment.  If no source model has been set, is an empty list.
    :vartype _all_global_ann: `enum.Enum` or list
    :ivar _all_seq_ann: An enum containing all sequence annotations present in
        the alignment.  If no source model has been set, is an empty list.
    :vartype _all_seq_ann: `enum.Enum` or list
    :ivar _all_structureless_seq_ann: All sequence annotations present in the
        alignment that don't require a structure.
    :vartype _all_structureless_seq_ann: list
    :ivar _shown_global_ann: A list of all global annotations that are currently
        displayed.  Annotations are listed in the order that they are displayed.
    :vartype _shown_global_ann: list
    :ivar _shown_row_types: A set of all global and sequence annotations that
        are currently displayed.
    :vartype _shown_row_types: set
    :ivar _seq_info: A list of sequences in the table.  Each sequence is
        represented by a `SequenceInfo` object.  The `SequenceInfo.has_struc`
        values are always populated for each sequence.  The `SequenceInfo.ann`
        values are only populated when the table is grouped by sequence.  They
        are `None` when the table is grouped by annotation.
    :vartype _seq_info: list
    :ivar _group_by_ann_info: A list of annotations currently displayed in the
        table.  Each annotation is represented by a `GroupByAnnotationInfo`
        object. This value is only populated when the table is grouped by
        annotation.  It is `None` when the table is grouped by sequence.
    :vartype _group_by_ann_info: list
    :ivar _group_by_ann_seq_nums: A `SequenceInfo` object containing sequence
        numbers for the annotations that do and do not require a structure.
        Note that the `GroupByAnnotationInfo.seqs` values in
        `_group_by_ann_info` point to the sequence number lists in this object.
        As such, sequence numbers for all annotations can be updated by updating
        this object.  This value is only populated when the table is grouped by
        annotation.  It is `None` when the table is grouped by sequence.
    :type param: `SequenceInfo`
    :note: Data methods in this class (i.e. methods decorated with
        `@table_helper.data_method`) may take up to five arguments:
        ann_type
          If the row represents an annotation, an `AnnotationType` enum
          representing the annotation type.  If the row represents a sequence or
          a spacer, a `RowType` enum representing the row type.
        ann
          If the row represents an annotation, an `annotation._AnnotationEnum`
          enum.  None otherwise.
        ann_index
          If the row represents a multi-value annotation, an int representing
          the annotation index. None otherwise.
        source_index
          The `QModelIndex` object for the source model index that corresponds
          to the index that data is being requested for.
        source_model
          The source model.
        multiple_roles_data
          A {role: data} dictionary of potentially relevant data fetched from
          the source model.
        Data methods that take fewer than five arguments will be called with the
        first N arguments from the above list.  Note that data methods can be
        called in three different ways, which will result in different
        combinations of arguments being populated:
        - Via an `index.data(role)` or `model.data(index, role)` call.  In this
        case, `data` will call the relevant data method and
        `multiple_roles_data` will be None.
        - Via a `model.data(index, CustomRole.MultipleRoles, multiple_roles)`
        call.  In this case, `_multipleRolesData` will call the relevant data
        methods and source_index and source_model will be None.
        If a data method needs data from the source model, `_mapRolesToSource`
        should be updated so that data for the appropriate roles are fetched.
        - Via a `rowData` call.  As with the `_multipleRolesData` call,
        source_index and source_model will be None, and if a
        data method needs data from the source model, `_mapRolesToSource`
        should be updated so that data for the appropriate roles are fetched.
    """
    groupByChanged = QtCore.pyqtSignal(object)
    tableWidthChangedSignal = QtCore.pyqtSignal(int)
    ROLE_MAPPINGS = {
        Qt.DisplayRole:
            (Qt.DisplayRole, RoleBase.SeqAnnotation, RoleBase.GlobalAnnotation),
        CustomRole.DataRange:
            (None, RoleBase.SeqAnnotationRange, RoleBase.GlobalAnnotationRange),
        Qt.BackgroundRole: (Qt.BackgroundRole, RoleBase.SeqBackground,
                            RoleBase.GlobalBackground),
        Qt.ForegroundRole: (Qt.ForegroundRole, RoleBase.SeqForeground,
                            RoleBase.GlobalForeground),
        Qt.ToolTipRole:
            (Qt.ToolTipRole, RoleBase.SeqToolTip, RoleBase.GlobalToolTip),
    }
    MULTI_ROW_ANN_BACKGROUND_ROLE_BASE = {
        SEQ_ANNO_TYPES.binding_sites: RoleBase.BindingSiteBackground,
        SEQ_ANNO_TYPES.kinase_conservation:
            RoleBase.KinaseConservationBackground,
        SEQ_ANNO_TYPES.domains: RoleBase.DomainBackground,
    }
    MULTI_ROW_ANN_TOOLTIP_ROLE_BASE = {
        SEQ_ANNO_TYPES.binding_sites: RoleBase.BindingSiteToolTip,
        SEQ_ANNO_TYPES.kinase_conservation: RoleBase.KinaseConservationToolTip,
        SEQ_ANNO_TYPES.domains: RoleBase.DomainToolTip,
    }
    ROW_HEIGHT_SCALE_ANNS = (
        SEQ_ANNO_TYPES.alignment_set,
        SEQ_ANNO_TYPES.binding_sites,
        SEQ_ANNO_TYPES.kinase_conservation,
        SEQ_ANNO_TYPES.domains,
        SEQ_ANNO_TYPES.disulfide_bonds,
        SEQ_ANNO_TYPES.antibody_cdr,
        SEQ_ANNO_TYPES.kinase_features,
        SEQ_ANNO_TYPES.pairwise_constraints,
        SEQ_ANNO_TYPES.secondary_structure,
        SEQ_ANNO_TYPES.pfam,
        SEQ_ANNO_TYPES.pred_accessibility,
        SEQ_ANNO_TYPES.pred_domain_arr,
        SEQ_ANNO_TYPES.pred_disordered,
        SEQ_ANNO_TYPES.pred_disulfide_bonds,
        SEQ_ANNO_TYPES.pred_secondary_structure,
        SEQ_ANNO_TYPES.proximity_constraints,
    )
    NO_GLOBAL_DATA_ROLES = {
        CustomRole.EntryID, CustomRole.HasStructure, CustomRole.Included,
        CustomRole.ResSelected, CustomRole.Seq
    }
[docs]    def __init__(self, parent=None):
        # See Qt documentation for argument documentation
        super().__init__(parent)
        self._options_model = None
        self._all_global_ann = []
        self._all_seq_ann = []
        self._all_structureless_seq_ann = []
        self._shown_global_ann = []
        self._shown_row_types = set()
        self._seq_info = []
        self._group_by_ann_info = None
        self._group_by_ann_seq_nums = None
        self._column_count = 0
        self._show_alignment_set = False
        self._mouse_over_index = None
        self._context_over_index = None
        self._show_spacers = True
        self._show_spacers_before_row = -1
        # The last seq should not have a spacer row, but it will temporarily
        # have a spacer row if sequences are being added or removed at the end
        # of the alignment
        self._last_seq_has_spacer = False
        # make sure that we clear the flags cache as soon as we finish inserting
        # or removing a row
        self.rowsInserted.connect(self._flag_cache.clear)
        self.rowsRemoved.connect(self._flag_cache.clear) 
[docs]    def setOptionsModel(self, options_model):
        """
        Set the options model for the model, which reports on various display
        options that the user can set through the GUI.
        Accessing `OptionsModel` attributes is slow enough that it affects
        painting speed, so we instead use an `OptionsModelCache` in this class.
        Note that the `OptionsModelCache` instance is read-only and must not be
        used to change options.
        :param options_model: The widget options.
        :type options_model: schrodinger.application.msv.gui.gui_models.
                             OptionsModel
        """
        self._options_model_cache = OptionsModelCache()
        attrs = ('group_by',)
        if self._options_model is not None:
            # Disconnect old options model
            for attr_name in attrs:
                signal = getattr(self._options_model, f"{attr_name}Changed")
                signal.disconnect()
        self._options_model = options_model
        for attr_name in attrs:
            cur_val = getattr(self._options_model, attr_name)
            setattr(self._options_model_cache, attr_name, cur_val)
            signal = getattr(self._options_model, f"{attr_name}Changed")
            signal.connect(
                partial(setattr, self._options_model_cache, attr_name))
        ss = [
            (self._options_model.group_byChanged, self._updateGroupBy),
            (self._options_model.all_visible_annotationsChanged,
             self._updateVisibleRowTypes),
            (self._options_model.annotation_spacer_enabledChanged,
             self._updateShowSpacer),
        ]
        for signal, slot in ss:
            signal.connect(slot)
            slot() 
[docs]    @table_helper.model_reset_method
    def setSourceModel(self, model):
        og_model = self.sourceModel()
        if og_model is not None:
            for signal, slot in self._getSourceModelSignalsAndSlots(og_model):
                signal.disconnect(slot)
        # See Qt documentation for method documentation
        super().setSourceModel(model)
        for signal, slot in self._getSourceModelSignalsAndSlots(model):
            signal.connect(slot)
        self._fetchAnnotationTypes()
        self._initializeBookkeeping() 
    def _getSourceModelSignalsAndSlots(self, model):
        return [
            (model.modelAboutToBeReset, self.beginResetModel),
            (model.modelReset, self._sourceModelReset),
            (model.layoutAboutToBeChanged, self.layoutAboutToBeChanged),
            (model.layoutChanged, self._sourceLayoutChanged),
            (model.rowsAboutToBeInserted, self._sourceRowsAboutToBeInserted),
            (model.rowsInserted, self._sourceRowsInserted),
            (model.rowsAboutToBeRemoved, self._sourceRowsAboutToBeRemoved),
            (model.rowsRemoved, self._sourceRowsRemoved),
            (model.columnsAboutToBeInserted, self.beginInsertColumns),
            (model.columnsInserted, self.endInsertColumns),
            (model.columnsAboutToBeRemoved, self.beginRemoveColumns),
            (model.columnsRemoved, self.endRemoveColumns),
            (model.dataChanged, self._modelDataChanged),
            (model.alnSetChanged, self._alnSetChanged),
            (model.kinaseFeaturesChanged, self._invalidateFilter),
            (model.kinaseConservationChanged, self._invalidateFilter),
            (model.secondaryStructureChanged, self._invalidateFilter),
            (model.predictionsChanged, self._invalidateFilter),
            (model.sequenceStructureChanged, self._onSequenceStructureChanged),
            (model.domainsChanged, self._invalidateFilter)
        ]  # yapf: disable
    @QtCore.pyqtSlot()
    def _sourceModelReset(self):
        self._fetchAnnotationTypes()
        self._initializeBookkeeping()
        self.endResetModel()
    @QtCore.pyqtSlot()
    def _sourceLayoutChanged(self):
        self._initializeBookkeeping()
        self._invalidateAllPersistentIndices()
        self.layoutChanged.emit()
    def _fetchAnnotationTypes(self):
        """
        When we get a new model or the model is reset, fetch the enums for
        annotation types
        """
        model = self.sourceModel()
        self._all_global_ann, self._all_seq_ann = model.annotationTypes()
        self._all_structureless_seq_ann = [
            ann for ann in self._all_seq_ann if not ann.requires_structure
        ]
    def _initializeBookkeeping(self):
        """
        Clear and re-populate all of the bookkeeping data needed for this proxy.
        """
        # not using _clearMouseOverIndex because it emits signals
        self._mouse_over_index = None
        self._seq_info = []
        self._group_by_ann_info = []
        source_model = self.sourceModel()
        if source_model is None:
            self._column_count = 0
            return
        self._column_count = source_model.columnCount()
        for row in range(source_model.rowCount()):
            index = source_model.index(row, 0)
            has_structure = source_model.data(index, CustomRole.HasStructure)
            self._seq_info.append(SequenceInfo(has_structure))
        aln = self.getAlignment()
        has_sets = aln is not None and aln.hasAlnSets()
        self._setAlnSetShown(has_sets)
        if self._options_model_cache.group_by is GroupBy.Sequence:
            self._initializeGroupBySeqData()
        else:
            self._initializeGroupByAnnData()
    def _initializeGroupBySeqData(self):
        """
        Populate the bookkeeping data needed when we're grouping rows by
        sequence.
        """
        for seq_num, cur_seq_info in enumerate(self._seq_info):
            self._updateSeqInfoAnnotations(seq_num, cur_seq_info)
    def _updateSeqInfoAnnotations(self, seq_num, seq_info):
        shown_anns = self._getAnnList(seq_num, seq_info.has_struc)
        seq_info.anns = shown_anns
    def _getAnnList(self, seq_num, has_struc):
        model = self.sourceModel()
        index = model.index(seq_num, 0)
        all_anns = self._all_seq_ann if has_struc else self._all_structureless_seq_ann
        new_anns = []
        for ann in all_anns:
            if ann not in self._shown_row_types:
                continue
            role = RoleBase.SeqRowHeightScale + ann.value
            scale = model.data(index, role)
            if scale == 0:
                continue
            if ann.multi_value:
                indexes = model.data(index,
                                     RoleBase.SeqAnnotationIndexes + ann.value)
                if indexes is None:
                    continue
                for idx in indexes:
                    new_anns.append((ann, idx))
            else:
                # TODO MSV-3293: allow hiding annotation rows from specific
                # sequences
                ann_idx = 0
                new_anns.append((ann, ann_idx))
        return new_anns
    def _getSeqAnnotationLists(self):
        """
        Generate lists of currently shown sequence annotations for structured
        and structureless sequences.
        :return: A tuple of:
            - A list of sequence annotations for sequence with an associated
            structure.
            - A list of sequence annotations for sequence without an associated
            structure.
        :rtype: tuple
        """
        structured = []
        structureless = []
        for ann in self._all_seq_ann:
            if ann in self._shown_row_types:
                structured.append(ann)
                if not ann.requires_structure:
                    structureless.append(ann)
        return structured, structureless
    def _initializeGroupByAnnData(self):
        """
        Populate the bookkeeping data needed when we're grouping rows by
        annotation.
        """
        self._group_by_ann_seq_nums = self._genSeqNums()
        for ann in self._all_seq_ann:
            if ann not in self._shown_row_types:
                continue
            ann_info = self._createGroupByAnnInfo(ann)
            self._group_by_ann_info.append(ann_info)
    def _createGroupByAnnInfo(self, ann):
        """
        Create and populate an object to track data for a single annotation.
        """
        if ann.requires_structure:
            seq_nums = self._group_by_ann_seq_nums.structured_only
        else:
            seq_nums = self._group_by_ann_seq_nums.all
        if ann.multi_value:
            model = self.sourceModel()
            role = RoleBase.SeqAnnotationIndexes + ann.value
            ann_indexes = []
            for seq_num in seq_nums:
                index = model.index(seq_num, 0)
                indexes = model.data(index, role)
                ann_indexes.append(indexes)
            ann_indexes = tuple(ann_indexes)
        else:
            ann_indexes = None
        return GroupByAnnotationInfo(ann, seq_nums, ann_indexes)
    def _genSeqNums(self):
        """
        Return lists of sequence numbers that should be shown for annotations
        that don't require a structure and annotations that do require a
        structure.
        :rtype: `SequenceNums`
        """
        all_nums = list(range(len(self._seq_info)))
        structured_nums = [
            i for i, cur_seq in enumerate(self._seq_info) if cur_seq.has_struc
        ]
        return SequenceNums(all_nums, structured_nums)
[docs]    def rowCount(self, parent=None):
        # See Qt documentation for method documentation
        seq_count = len(self._seq_info)
        num_global_ann = len(self._shown_global_ann)
        if seq_count == 0:
            return 0
        elif parent is None or not parent.isValid():
            if self._options_model_cache.group_by is GroupBy.Sequence:
                return seq_count + num_global_ann
            else:  # GroupBy.Type
                return num_global_ann + seq_count + len(self._group_by_ann_info)
        elif parent.internalId() != TOP_LEVEL:
            # parent is not a top-level row
            return 0
        row_num = parent.row()
        num_global_ann = len(self._shown_global_ann)
        if row_num < num_global_ann:
            return 0
        elif self._options_model_cache.group_by is GroupBy.Sequence:
            seq_num = row_num - num_global_ann
            num_ann = len(self._seq_info[seq_num].anns)
            last_seq = seq_num == len(self._seq_info) - 1
            # We include a spacer row except:
            # - The last sequence doesn't have a spacer (since there's nothing
            #   after it) unless we're in the process of adding or removing
            #   sequences from the end of the alignment (in which case
            #   self._last_seq_has_spacer will be True).
            # - Spacers are hidden when the user is picking pairwise alignment
            #   constraints.  If we're in the process of enabling or disabling
            #   this mode, then self._show_spacers_before_row will be set as we
            #   add or remove spacer rows one-by-one.
            if (num_ann and (not last_seq or self._last_seq_has_spacer) and
                (self._show_spacers or
                 row_num < self._show_spacers_before_row)):
                num_ann += SPACER
            return num_ann
        else:  # GroupBy.Type
            if row_num < num_global_ann + seq_count:
                return 0
            ann_num = row_num - num_global_ann - seq_count
            return len(self._group_by_ann_info[ann_num]) 
[docs]    def columnCount(self, parent=None):
        # See Qt documentation for method documentation
        return self._column_count 
    def _mapIndexToSource(self, proxy_index):
        """
        Determine what source index and annotation the specified index maps to
        :param proxy_index: The index to map.  Must be a valid index.
        :type proxy_index: `QtCore.QModelIndex`
        :return: A tuple of:
              - The source index that `proxy_index` maps to.  Note that this
              will be an index in the first row for global annotation rows and
              an invalid index for spacer rows.
              - If the `proxy_index` row represents an annotation, returns an
              `AnnotationType` enum representing the annotation type.  If the
              `proxy_index` row represents a sequence or a spacer, returns a
              `RowType` enum representing the row type.
              - If the `proxy_index` row represents an annotation, returns an
              annotation enum.
              - If the `proxy_index` row represents a multi-value annotation,
              returns an int representing the annotation index.
        :rtype: tuple
        """
        source_row, ann_type, ann, ann_index = self._mapRowToSource(
            proxy_index.row(), proxy_index.internalId())
        if source_row is None:
            source_index = QtCore.QModelIndex()
        else:
            source_index = self.sourceModel().index(source_row,
                                                    proxy_index.column())
        return source_index, ann_type, ann, ann_index
    def _mapRowToSource(self, proxy_row, internal_id):
        """
        Determine what source row and annotation the specified row maps to.
        :param proxy_row: The row to map.
        :type proxy_index: int
        :param internal_id: The parent row (or `TOP_LEVEL` for top-level
            rows) of the row to map.
        :type internal_id: int
        :return: A tuple of:
              - The source row that `proxy_index` maps to.  Note that this value
              will be 0 for global annotation rows and will be None for spacer
              rows.
              - If the `proxy_index` row represents an annotation, returns an
              `AnnotationType` enum representing the annotation type.  If the
              `proxy_index` row represents a sequence or a spacer, returns a
              `RowType` enum representing the row type.
              - If the `proxy_index` row represents an annotation, returns an
              annotation enum.
              - If the `proxy_index` row represents a multi-value annotation,
              returns an int representing the annotation index.
        :rtype: tuple
        """
        num_global_ann = len(self._shown_global_ann)
        ann_index = None
        if internal_id == TOP_LEVEL and proxy_row < num_global_ann:
            source_row = 0
            ann_type = AnnotationType.Global
            ann = self._shown_global_ann[proxy_row]
        elif self._options_model_cache.group_by is GroupBy.Sequence:
            if internal_id == TOP_LEVEL:
                ann_type = RowType.Sequence
                ann = None
                source_row = proxy_row - num_global_ann
            else:
                source_row = internal_id - num_global_ann
                seq_anns = self._seq_info[source_row].anns
                if proxy_row == len(seq_anns):
                    ann_type = RowType.Spacer
                    ann = None
                else:
                    ann_type = AnnotationType.Sequence
                    ann, ann_index = seq_anns[proxy_row]
        else:  # GroupBy.Type
            seq_count = len(self._seq_info)
            if internal_id == TOP_LEVEL:
                if proxy_row < num_global_ann + seq_count:
                    ann_type = RowType.Sequence
                    ann = None
                    source_row = proxy_row - num_global_ann
                else:
                    ann_type = RowType.Spacer
                    group_num = proxy_row - num_global_ann - seq_count
                    ann = self._group_by_ann_info[group_num].ann
                    source_row = None
            else:
                ann_type = AnnotationType.Sequence
                group_num = internal_id - num_global_ann - seq_count
                ann_info = self._group_by_ann_info[group_num]
                ann = ann_info.ann
                source_row, ann_index = ann_info.getSourceRowInfo(proxy_row)
        return source_row, ann_type, ann, ann_index
[docs]    def mapFromSource(self, source_index):
        # See Qt documentation for method documentation
        if not source_index.isValid():
            return QtCore.QModelIndex()
        col = source_index.column()
        num_global_ann = len(self._shown_global_ann)
        proxy_row = source_index.row() + num_global_ann
        return self.index(proxy_row, col) 
[docs]    def mapToSource(self, proxy_index):
        # See Qt documentation for method documentation
        if not proxy_index.isValid():
            return QtCore.QModelIndex()
        source_index, *_ = self._mapIndexToSource(proxy_index)
        return source_index 
[docs]    def setData(self, proxy_index, value, role=Qt.EditRole):
        # See Qt documentation for method documentation
        source_index = self.mapToSource(proxy_index)
        if source_index.isValid():
            return self.sourceModel().setData(source_index, value, role)
        else:
            return False 
[docs]    def data(self, proxy_index, role=Qt.DisplayRole, multiple_roles=None):
        # See table_speed_up documentation for method documentation
        if not proxy_index.isValid():
            return {} if role == CustomRole.MultipleRoles else None
        source_index, ann_type, ann, ann_index = self._mapIndexToSource(
            proxy_index)
        source_model = self.sourceModel()
        if role in self.ROLE_MAPPINGS:
            mapped_role = self._mappedRole(ann_type, ann, ann_index,
                                           *self.ROLE_MAPPINGS[role])
            return source_model.data(source_index, mapped_role)
        elif (ann_type is AnnotationType.Global and
              role in self.NO_GLOBAL_DATA_ROLES):
            return None
        elif role not in self._data_methods:
            return source_model.data(source_index, role)
        # any change to these argument lists needs to also be made in rowData
        # and _multipleRolesData
        if role == CustomRole.MultipleRoles:
            data_args = (ann_type, ann, ann_index, source_index, source_model,
                         multiple_roles)
        else:
            # add None as a place holder for the data dictionary that
            # _multipleRolesData would pass to data methods
            data_args = (ann_type, ann, ann_index, source_index, source_model,
                         None, role)
        return self._callDataMethod(role, data_args) 
[docs]    def rowData(self, proxy_row, cols, internal_id, roles):
        """
        Fetch data for multiple roles for multiple indices in the same row. Note
        that this method does minimal sanity checking of its input for
        performance reasons, as it is called during painting.  The arguments are
        assumed to refer to valid indices.  Use `data` instead if more sanity
        checking is required.
        :param proxy_row: The row number to fetch data for.
        :type proxy_row: int
        :param cols: A list of columns to fetch data for.
        :type cols: list(int)
        :param internal_id: The parent row (or `TOP_LEVEL` for top-level
            rows) of the row to fetch data for.
        :type internal_id: int
        :param roles: A list of roles to fetch data for.
        :type roles: list(int)
        :return: {role: data} dictionaries for each requested column.  Note
            that the keys of these dictionaries may not match `roles`.  Data for
            additional roles may be included (e.g. if that data was required to
            calculate data for a requested role).  Data for requested roles may
            not be included if those roles are not applicable to the specified
            row (e.g. spacer rows may not provide data beyond row type and
            row title).
        :rtype: list(dict(int, object))
        """
        source_row, ann_type, ann, ann_index = self._mapRowToSource(
            proxy_row, internal_id)
        (roles, mapped_roles, row_height_scale_requested, row_height_scale_role,
         res_for_consensus, chain_col_requested, provide_chain_col) = \
            
self._mapRolesToSource(roles, ann_type, ann, ann_index)
        if (not roles or source_row is None or
            (ann_type is RowType.Spacer and
             CustomRole.NextRowHidden not in roles)):
            # Spacer rows don't need any data, so there's nothing to fetch from
            # the source model.  The only exception is when we're deciding
            # whether to draw a hidden sequence marker after the spacer row at
            # the end of a sequence's annotations in group-by-sequence mode.
            row_data = [{} for _ in cols]
        else:
            row_data = self._source_model.rowData(source_row, cols, roles)
        roles.discard(Qt.DisplayRole)
        roles.discard(CustomRole.Residue)
        for cur_data in row_data:
            # We use the row number for the source_index argument and None for
            # the source_model argument.  Note that any change to this argument
            # list needs to also be made in data and _multipleRolesData.
            self._fetchMultipleRoles(cur_data, roles, ann_type, ann, ann_index,
                                     source_row, None, cur_data)
        self._transformDataFromSource(row_data, mapped_roles,
                                      row_height_scale_requested,
                                      row_height_scale_role, res_for_consensus,
                                      chain_col_requested, provide_chain_col,
                                      ann_index)
        return row_data 
    @table_helper.data_method(CustomRole.MultipleRoles)
    def _multipleRolesData(self, ann_type, ann, ann_index, source_index,
                           source_model, multiple_roles):
        # See table_speed_up documentation for method documentation.
        # Note that the last argument here is the list of roles to fetch, not a
        # dictionary of source data (as it would be in other methods decorated
        # with @table_helper.data_method).
        # Figure out for which roles we need to fetch data from the source model
        (multiple_roles, mapped_roles, row_height_scale_requested,
         row_height_scale_role, res_for_consensus, chain_col_requested,
         provide_chain_col) = \
            
self._mapRolesToSource(multiple_roles, ann_type, ann, ann_index)
        # Fetch data from the source model
        if multiple_roles:
            if source_index.isValid():
                data = source_model.data(source_index, CustomRole.MultipleRoles,
                                         multiple_roles)
            else:
                data = {}
            multiple_roles.discard(Qt.DisplayRole)
            multiple_roles.discard(CustomRole.Residue)
            # We use the row number for the source_index argument and None for
            # the source_model argument.  Note that any change to this argument
            # list needs to also be made in data and rowData.
            self._fetchMultipleRoles(data, multiple_roles, ann_type, ann,
                                     ann_index, source_index.row(), None, data)
        else:
            data = {}
        self._transformDataFromSource([data], mapped_roles,
                                      row_height_scale_requested,
                                      row_height_scale_role, res_for_consensus,
                                      chain_col_requested, provide_chain_col,
                                      ann_index)
        return data
    def _mapRolesToSource(self, multiple_roles, ann_type, ann, ann_index):
        """
        Given a set of roles to fetch data for, figure out what data we need to
        request from `SequenceAlignmentModel` and how that data should be
        transformed before it's sent to the view.
        :param multiple_roles: A set of roles to fetch data for
        :type multiple_roles: set(int)
        :param ann_type: If the row represents an annotation, an
            `AnnotationType` enum representing the annotation type.  If the
            row represents a sequence or a spacer, a `RowType` enum
            representing the row type.
        :type ann_type: `AnnotationType` or `RowType`
        :param ann: If the row represents an annotation, an annotation enum.
            Ignored otherwise.
        :type ann: `annotation._AnnotationEnum`
        :return: A tuple of
            - The roles to fetch data from `SequenceAlignmentModel` (set)
            - A dictionary of {view role: `SequenceAlignmentModel` role} (dict)
            - Whether `CustomRole.RowHeightScale` data was requested (bool)
            - The `SequenceAlignmentModel` role that was used to fetch
            `CustomRole.RowHeightScale` data (int or NoneType)
            - Whether `CustomRole.Residue` data was requested and this is a
            consensus sequence row (bool)
            - Whether `CustomRole.ChainCol` data was requested (bool)
            - Whether `CustomRole.ChainCol` data is being fetched from
            `SequenceAlignmentModel` using the `SeqInfo.Chain` role (bool)
            Note that these values are intended as input to
            `_transformDataFromSource`.
        :rtype: tuple
        """
        multiple_roles = set(multiple_roles)
        mapped_roles = {}
        for orig_role, args in self.ROLE_MAPPINGS.items():
            if orig_role not in multiple_roles:
                continue
            new_role = self._mappedRole(ann_type, ann, ann_index, *args)
            mapped_roles[orig_role] = new_role
            multiple_roles.discard(orig_role)
            if new_role is not None:
                multiple_roles.add(new_role)
        if ann_type is AnnotationType.Global:
            multiple_roles -= self.NO_GLOBAL_DATA_ROLES
        row_height_scale_requested = CustomRole.RowHeightScale in multiple_roles
        row_height_scale_role = None
        if row_height_scale_requested:
            multiple_roles.remove(CustomRole.RowHeightScale)
            if ann in self.ROW_HEIGHT_SCALE_ANNS:
                row_height_scale_role = RoleBase.SeqRowHeightScale + ann.value
                multiple_roles.add(row_height_scale_role)
        res_for_consensus = (CustomRole.Residue in multiple_roles and
                             ann is ALN_ANNO_TYPES.consensus_seq)
        if res_for_consensus:
            multiple_roles.remove(CustomRole.Residue)
            multiple_roles.add(CustomRole.ConsensusSeq)
        if CustomRole.RowTitle in multiple_roles:
            if (ann_type is RowType.Sequence or
                (self._options_model_cache.group_by is GroupBy.Type and
                 ann_type in AnnotationType)):
                multiple_roles.add(SeqInfo.Name)
                multiple_roles.add(SeqInfo.Title)
            if ann is SEQ_ANNO_TYPES.pfam:
                multiple_roles.add(CustomRole.PfamName)
            elif ann_index is not None:
                if ann is SEQ_ANNO_TYPES.domains:
                    role = RoleBase.DomainName + ann_index
                    multiple_roles.add(role)
                elif ann in (SEQ_ANNO_TYPES.binding_sites,
                             SEQ_ANNO_TYPES.kinase_conservation):
                    role = RoleBase.BindingSiteName + ann_index
                    multiple_roles.add(role)
        chain_col_requested = CustomRole.ChainCol in multiple_roles
        provide_chain_col = None
        if chain_col_requested:
            multiple_roles.remove(CustomRole.ChainCol)
            provide_chain_col = (
                ann_type is RowType.Sequence or
                (self._options_model_cache.group_by is GroupBy.Type and
                 ann_type is AnnotationType.Sequence))
            if provide_chain_col:
                multiple_roles.add(SeqInfo.Chain)
        if (CustomRole.NextRowHidden in multiple_roles and
                self._options_model_cache.group_by is GroupBy.Sequence and
                ann_type is not AnnotationType.Global):
            multiple_roles.add(CustomRole.SeqExpanded)
        if CustomRole.AnnotationSelected in multiple_roles:
            multiple_roles.add(CustomRole.Seq)
        if CustomRole.BindingSiteConstraint in multiple_roles:
            if ann is SEQ_ANNO_TYPES.binding_sites and ann_index is not None:
                role = RoleBase.BindingSiteName + ann_index
                multiple_roles.add(role)
            multiple_roles.add(CustomRole.ReferenceResidue)
        return (multiple_roles, mapped_roles, row_height_scale_requested,
                row_height_scale_role, res_for_consensus, chain_col_requested,
                provide_chain_col)
    def _mappedRole(self, ann_type, ann, ann_index, seq_role, seq_ann_base,
                    global_ann_base):
        """
        Map a Qt role from view roles to `SequenceAlignmentModel` roles.
        Because `SequenceAlignmentModel` doesn't have separate rows for
        annotations, it uses different role numbers to differentiate
        between sequences, sequence annotations, and global annotations.
        :param ann_type: If the row represents an annotation, an
            `AnnotationType` enum representing the annotation type.  If the
            row represents a sequence or a spacer, a `RowType` enum
            representing the row type.
        :type ann_type: `AnnotationType` or `RowType`
        :param ann: If the row represents an annotation, an annotation enum.
            Ignored otherwise.
        :type ann: `annotation._AnnotationEnum`
        :param seq_role: The role to return if the row represents a sequence.
        :type seq_role: int
        :param seq_ann_base: The role base to use if the row represents a
            sequence annotation.  The returned role will be this value plus the
            integer value of the annotation.
        :type seq_ann_base: RoleBase
        :param global_ann_base: The role base to use if the row represents a
            global annotation.  The returned role will be this value plus the
            integer value of the annotation.
        :type global_ann_base: RoleBase
        :return: The appropriate role.
        :rtype: int
        """
        if ann_type is RowType.Sequence:
            return seq_role
        elif ann_type is AnnotationType.Sequence:
            multi_value = ann_index is not None and ann.multi_value
            if multi_value and seq_ann_base == RoleBase.SeqBackground:
                seq_ann_base = self.MULTI_ROW_ANN_BACKGROUND_ROLE_BASE[ann]
                offset = ann_index
            elif multi_value and seq_ann_base == RoleBase.SeqToolTip:
                seq_ann_base = self.MULTI_ROW_ANN_TOOLTIP_ROLE_BASE[ann]
                offset = ann_index
            else:
                offset = ann.value
            return seq_ann_base + offset
        elif ann_type is AnnotationType.Global:
            return global_ann_base + ann.value
        else:
            # spacer rows
            return None
    def _transformDataFromSource(self, row_data, mapped_roles,
                                 row_height_scale_requested,
                                 row_height_scale_role, res_for_consensus,
                                 chain_col_requested, provide_chain_col,
                                 ann_index):
        """
        Transform the data we received from `SequenceAlignmentModel` into data
        that can be interpreted by the view.  This method transforms data for
        multiple cells at once.
        :param row_data: A list of {role: data} dictionaries to be transformed.
            These dictionaries will be transformed in place.
        :type row_data: list(dict(int, object))
        :param mapped_roles: A dictionary of {view role:
            `SequenceAlignmentModel` role}.
        :type mapped_roles: dict(int, int)
        :param row_height_scale_requested: Whether `CustomRole.RowHeightScale`
            data was requested.
        :type row_height_scale_requested: bool
        :param row_height_scale_role: The `SequenceAlignmentModel` role that was
            used to fetch `CustomRole.RowHeightScale` data.  Should be None if
            `CustomRole.RowHeightScale` was not requested.
        :type row_height_scale_role: int or NoneType
        :param res_for_consensus: Whether `CustomRole.Residue` data was
            requested and this is a consensus sequence row.
        :type res_for_consensus: bool
        :param chain_col_requested: Whether `CustomRole.ChainCol` data was
            requested.
        :type chain_col_requested: bool
        :param provide_chain_col: Whether `CustomRole.ChainCol` data was fetched
            from `SequenceAlignmentModel` using the `SeqInfo.Chain` role
        :type provide_chain_col: bool
        """
        for cur_data in row_data:
            for orig_role, new_role in mapped_roles.items():
                if new_role is None:
                    cur_data[orig_role] = None
                elif new_role in cur_data:
                    cur_data[orig_role] = cur_data[new_role]
            if row_height_scale_requested:
                if row_height_scale_role is not None:
                    cur_data[CustomRole.RowHeightScale] = cur_data.get(
                        row_height_scale_role, 1)
                else:
                    cur_data[CustomRole.RowHeightScale] = 1
            if res_for_consensus:
                # For the consensus sequence row, return Residue data if all
                # sequences have the same residue
                seq_data = cur_data[CustomRole.ConsensusSeq]
                res = seq_data[0] if len(seq_data) == 1 else None
                cur_data[CustomRole.Residue] = res
            if chain_col_requested:
                if provide_chain_col:
                    cur_data[CustomRole.ChainCol] = cur_data.get(SeqInfo.Chain)
                else:
                    cur_data[CustomRole.ChainCol] = None
    @table_helper.data_method(CustomRole.RowType)
    def _rowTypeData(self, ann_type, ann):
        """
        Return the type of data contained in the specified row.  The return
        value will be either a `RowType` enum or an annotation enum.
        """
        if isinstance(ann_type, RowType):
            return ann_type
        elif ann_type is AnnotationType.Global:
            return ann
        elif ann_type is AnnotationType.Sequence:
            return ann
    @table_helper.data_method(CustomRole.MultiRowAnnIndex)
    def _annIndexData(self, ann_type, ann, ann_index):
        return ann_index
    @table_helper.data_method(CustomRole.DataRange)
    def _dataRangeData(self, ann_type, ann, ann_index, source_index,
                       source_model):
        """
        Return the range of values contained in the specified row.
        """
        data = self._multipleRolesData(ann_type, ann, ann_index, source_index,
                                       source_model, {CustomRole.DataRange})
        return data[CustomRole.DataRange]
    @table_helper.data_method(Qt.DisplayRole)
    def _displayData(self, ann_type, ann, ann_index, source_index,
                     source_model):
        data = self._multipleRolesData(ann_type, ann, ann_index, source_index,
                                       source_model, {Qt.DisplayRole})
        return data[Qt.DisplayRole]
    @table_helper.data_method(CustomRole.SeqRowEntryID)
    def _seqRowEntryIDData(self, ann_type, ann, ann_index, source_index,
                           source_model):
        if ann_type is RowType.Sequence:
            return source_model.data(source_index, CustomRole.EntryID)
    @table_helper.data_method(CustomRole.Residue)
    def _residueData(self, ann_type, ann, ann_index, source_index,
                     source_model):
        data = self._multipleRolesData(ann_type, ann, ann_index, source_index,
                                       source_model, {CustomRole.Residue})
        return data.get(CustomRole.Residue)
    @table_helper.data_method(CustomRole.RowTitle)
    def _rowTitleData(self, ann_type, ann, ann_index, source_index,
                      source_model, multiple_roles_data):
        def get_seq_name():
            if multiple_roles_data is not None:
                return multiple_roles_data.get(SeqInfo.Name)
            else:
                return source_model.data(source_index, SeqInfo.Name)
        def get_ann_title():
            if ann is SEQ_ANNO_TYPES.pfam:
                title_role = CustomRole.PfamName
            elif ann is SEQ_ANNO_TYPES.domains:
                title_role = RoleBase.DomainName + ann_index
            elif ann in (SEQ_ANNO_TYPES.binding_sites,
                         SEQ_ANNO_TYPES.kinase_conservation):
                title_role = RoleBase.BindingSiteName + ann_index
            else:
                return ann.title
            if multiple_roles_data is not None:
                return multiple_roles_data.get(title_role, "")
            else:
                return source_model.data(source_index, title_role)
        grouped_by_type = self._options_model_cache.group_by is GroupBy.Type
        if ann_type is AnnotationType.Global:
            return ann.title
        elif ann_type is RowType.Sequence:
            return get_seq_name()
        elif ann_type is RowType.Spacer:
            if grouped_by_type:
                return ann.title
            # spacer rows have no title in group by seq
        elif ann_type is AnnotationType.Sequence:
            ann_title = get_ann_title()
            if grouped_by_type:
                return f'{get_seq_name()} {ann_title}'
            else:
                return ann_title
        else:
            assert False
    @table_helper.data_method(CustomRole.ChainCol)
    def _chainColData(self, ann_type, ann, ann_index, source_index,
                      source_model):
        data = self._multipleRolesData(source_model, ann_type, ann,
                                       source_index, {CustomRole.ChainCol})
        return data[CustomRole.ChainCol]
    @table_helper.data_method(CustomRole.RowHeightScale)
    def _rowHeightScaleData(self, ann_type, ann, ann_index, source_index,
                            source_model):
        data = self._multipleRolesData(ann_type, ann, ann_index, source_index,
                                       source_model,
                                       {CustomRole.RowHeightScale})
        return data[CustomRole.RowHeightScale]
    @table_helper.data_method(CustomRole.SeqresOnly)
    def _seqresOnlyData(self, ann_type, ann, ann_index, source_index,
                        source_model, multiple_roles_data):
        if ann_type is AnnotationType.Global:
            return False
        elif multiple_roles_data is not None:
            return multiple_roles_data.get(CustomRole.SeqresOnly)
        else:
            return source_model.data(source_index, CustomRole.SeqresOnly)
    @table_helper.data_method(CustomRole.IncludeInDragImage)
    def _includeInDragImageData(self, ann_type, ann, ann_index, source_index,
                                source_model):
        """
        Whether the specified row should be drawn when we're drawing an image
        to represent the drag-and-dropped sequences.
        """
        if not source_index.isValid() or source_index.row() == 0:
            # don't draw spacer rows or the reference sequence (since we can't
            # drag the reference sequence)
            return False
        elif self._options_model_cache.group_by is GroupBy.Sequence or ann_type is RowType.Sequence:
            # we include sequence annotation rows when in group-by-sequence
            # mode, but not when in group-by-type mode
            return source_index.data(CustomRole.SeqSelected)
        else:
            return False
    @table_helper.data_method(CustomRole.CanDropAbove)
    def _canDropAboveData(self, ann_type, ann, ann_index, source_index,
                          source_model):
        """
        Whether we can drop drag-and-dropped sequences above the specified row.
        """
        return (ann_type is RowType.Sequence and source_index.row() != 0 and
                not source_model.data(source_index, CustomRole.SeqSelected))
    @table_helper.data_method(CustomRole.CanDropBelow)
    def _canDropBelowData(self, ann_type, ann, ann_index, source_index,
                          source_model):
        """
        Whether we can drop drag-and-dropped sequences below the specified row.
        Note that the view won't allow drops below a row with expanded children
        regardless of what this method returns (since there's never a case where
        we want to do that.  It would typically result in a drop between a
        sequence and its annotations.)
        """
        seq_num = source_index.row()
        selected = source_model.data(source_index, CustomRole.SeqSelected)
        if selected:
            return False
        elif ann_type is RowType.Sequence:
            # The view won't allow drops below a row with expanded children, so
            # we don't need to worry about dropping into an annotation group
            # when in group-by-sequence mode
            return True
        elif ann_type is RowType.Spacer:
            # In group by sequence mode, spacers are the last child row of a
            # sequence row.  Dropping after a spacer row therefore drops after
            # the sequence.
            return self._options_model_cache.group_by is GroupBy.Sequence
        elif ann_type is AnnotationType.Sequence:
            # In group by sequence mode, the last sequence row doesn't have a
            # spacer child, so we allow dropping after the last annoation
            return (self._options_model_cache.group_by is GroupBy.Sequence and
                    seq_num == len(self._seq_info) - 1 and
                    ann is self._seq_info[seq_num].anns[-1][0])
    @table_helper.data_method(CustomRole.ResOrColSelected)
    def _resOrColSelectedData(self, ann_type, ann, ann_index, source_index,
                              source_model):
        """
        Whether residues or columns are being selected. Checks column selection
        when ruler row is index is passed, residue selection otherwise.
        """
        if ann is ALN_ANNO_TYPES.indices:
            source_role = CustomRole.ColSelected
        else:
            source_role = CustomRole.ResSelected
        return source_model.data(source_index, source_role)
    @table_helper.data_method(CustomRole.PreviousRowHidden)
    def _prevRowHiddenData(self, ann_type, ann, ann_index, source_index,
                           source_model, multiple_roles_data):
        """
        Was the previous row filtered out by the SequenceProxyFilterModel?
        Controls whether or not the AlignmentInfoView draws a hidden sequence
        marker between this row and the previous one.
        """
        if multiple_roles_data is not None:
            prev_seq_hidden = multiple_roles_data.get(
                CustomRole.PreviousRowHidden)
        else:
            prev_seq_hidden = source_model.data(source_index,
                                                CustomRole.PreviousRowHidden)
        # Sequence rows get a marker in both group-by-sequence and group-by-type
        # modes.  Sequence annotation rows get a marker when in group-by-type
        # mode.
        return prev_seq_hidden and (
            ann_type is RowType.Sequence or
            (ann_type is AnnotationType.Sequence and
             self._options_model_cache.group_by is GroupBy.Type))
    @table_helper.data_method(CustomRole.NextRowHidden)
    def _nextRowHiddenData(self, ann_type, ann, ann_index, source_index,
                           source_model, multiple_roles_data):
        """
        Was the next row filtered out by the SequenceProxyFilterModel?  Controls
        whether or not the AlignmentInfoView draws a hidden sequence marker
        between this row and the next one.
        """
        if multiple_roles_data is not None:
            next_seq_hidden = multiple_roles_data.get(CustomRole.NextRowHidden)
            seq_expanded = multiple_roles_data.get(CustomRole.SeqExpanded)
            # source_index is a row number when this method gets called from
            # _multipleRolesData or rowData
            source_row = source_index
        else:
            next_seq_hidden = source_model.data(source_index,
                                                CustomRole.NextRowHidden)
            seq_expanded = source_model.data(source_index,
                                             CustomRole.SeqExpanded)
            source_row = source_index.row()
        if not next_seq_hidden:
            return False
        elif self._options_model_cache.group_by is GroupBy.Sequence:
            seq_anns = self._seq_info[source_row].anns
            if not seq_expanded or not seq_anns:
                # there are no sequence annotations for this sequence, so draw
                # the marker after the sequence row
                return ann_type is RowType.Sequence
            else:
                # If sequence annotations are present and expanded, then we want
                # to draw the marker after the last annotation row.  For every
                # sequence but the last one, that's the spacer row.  For the
                # last sequence, there's no spacer row so we need to check what
                # the last shown annotation is.
                return (ann_type is RowType.Spacer or
                        (ann_type is AnnotationType.Sequence and
                         source_row == len(self._seq_info) - 1 and
                         ann is seq_anns[-1][0]))
        else:  # GroupBy.Type
            return ann_type is not AnnotationType.Global
    @table_helper.data_method(CustomRole.AnnotationSelected)
    def _seqAnnSelectedData(self, ann_type, ann, ann_index, source_index,
                            source_model, multiple_roles_data):
        if ann_type not in AnnotationType:
            return False
        if ann_type is AnnotationType.Global:
            ann_info = GlobalAnnotationRowInfo(ann)
        else:
            if multiple_roles_data is not None:
                seq = multiple_roles_data.get(CustomRole.Seq)
            else:
                seq = source_model.data(source_index, CustomRole.Seq)
            if seq is None:
                return False
            ann_info = SequenceAnnotationRowInfo(seq, ann, ann_index)
        sel_model = self.getAlignment().ann_selection_model
        return sel_model.isSelected(ann_info)
    @table_helper.data_method(CustomRole.BindingSiteConstraint)
    def _bindingSiteConstraintData(self, ann_type, ann, ann_index, source_index,
                                   source_model, multiple_roles_data):
        if ann is not SEQ_ANNO_TYPES.binding_sites:
            return False
        ligand_name_role = RoleBase.BindingSiteName + ann_index
        if multiple_roles_data is not None:
            ligand_name = multiple_roles_data.get(ligand_name_role, "")
            ref_res = multiple_roles_data.get(CustomRole.ReferenceResidue)
        else:
            ligand_name = source_model.data(source_index, ligand_name_role)
            ref_res = source_model.data(source_index,
                                        CustomRole.ReferenceResidue)
        if ref_res is None or ref_res.is_gap:
            return False
        aln = self.getAlignment()
        return aln.isHMLigandConstraint(ref_res, ligand_name)
    def _clearMouseOverIndex(self):
        # Note: this emits fixedColumnDataChanged so must not be called while
        # the table is changing (e.g. during layoutChanged or between
        # beginRemoveRows and endRemoveRows)
        self.setMouseOverIndex(None)
[docs]    def setMouseOverIndex(self, proxy_index):
        """
        Set the given index as having the mouse over it
        :param proxy_index: The index the mouse is over, or None to clear the
            mouse over index
        :type proxy_index: QtCore.QModelIndex or None
        """
        varname = '_mouse_over_index'
        self._setIndexOver(proxy_index, varname) 
[docs]    def setContextOverIndex(self, proxy_index):
        """
        Set the given index as having the context menu over it
        :param proxy_index: The index the context menu is over
        :type proxy_index: QtCore.QModelIndex
        """
        varname = '_context_over_index'
        self._setIndexOver(proxy_index, varname) 
    def _setIndexOver(self, proxy_index, varname):
        data_changed_rows = []
        prev_value = getattr(self, varname)
        if prev_value is not None:
            data_changed_rows.append(prev_value[0])
        if proxy_index is not None:
            new_value = self._getMouseOverInfoFromProxyIndex(proxy_index)
            data_changed_rows.append(new_value[0])
        else:
            new_value = None
        setattr(self, varname, new_value)
        for changed_row in data_changed_rows:
            self.fixedColumnDataChanged.emit(CustomRole.MouseOver, changed_row)
    def _getMouseOverInfoFromProxyIndex(self, proxy_index):
        proxy_row = proxy_index.row()
        internal_id = proxy_index.internalId()
        if internal_id == TOP_LEVEL:
            top_level_row = proxy_row
            child_row = None
        else:
            top_level_row = internal_id
            child_row = proxy_row
        return (top_level_row, child_row)
[docs]    def isMouseOverIndex(self, proxy_index):
        """
        :return: Whether the given index represents a row that has the mouse
            over it
        """
        if self._mouse_over_index is None and self._context_over_index is None:
            return False
        mouse_over_info = self._getMouseOverInfoFromProxyIndex(proxy_index)
        over_row = (mouse_over_info
                    == self._mouse_over_index) or (mouse_over_info
                                                   == self._context_over_index)
        return over_row 
[docs]    def setAnnSelectionState(self, proxy_index, selected):
        source_index, ann_type, ann, ann_idx = self._mapIndexToSource(
            proxy_index)
        if ann_type is AnnotationType.Global:
            ann_info = GlobalAnnotationRowInfo(ann)
        else:
            seq = source_index.data(CustomRole.Seq)
            ann_info = SequenceAnnotationRowInfo(seq, ann, ann_idx)
        sel_model = self.getAlignment().ann_selection_model
        sel_model.setSelectionState({ann_info}, selected) 
[docs]    def flags(self, index):
        """
        See Qt documentation for additional method documentation
        Everything is selectable except for spacers
        :note: The return values from this function are cached on a per-row
            basis due to PerRowFlagCacheMixin.
        """
        if not index.isValid():
            return Qt.NoItemFlags
        source_index, ann_type, *_ = self._mapIndexToSource(index)
        flags = self.sourceModel().flags(source_index)
        return flags 
    @QtCore.pyqtSlot(QtCore.QModelIndex, int, int)
    def _sourceRowsAboutToBeInserted(self, parent, source_start, source_end):
        """
        Respond to the rowsAboutToBeInserted signal from the source model.  See
        Qt documentation for rowsAboutToBeInserted for argument documentation.
        """
        num_seqs = len(self._seq_info)
        seqs_to_insert = source_end - source_start + 1
        num_global_ann = len(self._shown_global_ann)
        if num_seqs == 0:
            # We're inserting into an empty table
            proxy_end = num_global_ann + seqs_to_insert - ZERO_INDEXED
            if self._options_model_cache.group_by is GroupBy.Type:
                proxy_end += len(self._group_by_ann_info)
            self.beginInsertRows(QtCore.QModelIndex(), 0, proxy_end)
        else:
            if (self._options_model_cache.group_by is GroupBy.Sequence and
                    source_start == num_seqs and
                    self._seq_info[source_start - 1].anns):
                # In group by sequence mode, when adding rows at the end,
                # add spacer row to the previous last row if it has annos
                prev_last_source_row = source_start - 1
                prev_last_proxy_row = num_global_ann + prev_last_source_row
                spacer_parent = self.index(prev_last_proxy_row, 0)
                last_row_seq = self._seq_info[prev_last_source_row]
                spacer_idx = len(last_row_seq.anns)
                self.beginInsertRows(spacer_parent, spacer_idx, spacer_idx)
                self._last_seq_has_spacer = True
                self.endInsertRows()
            proxy_start = num_global_ann + source_start
            proxy_end = num_global_ann + source_end
            self.beginInsertRows(QtCore.QModelIndex(), proxy_start, proxy_end)
            if self._options_model_cache.group_by is GroupBy.Type:
                # all seqs lists in self._group_by_ann_info are pointers to one
                # of the two lists in self._group_by_ann_seq_nums, so this will
                # update self._group_by_ann_info to account for the source model
                # numbering change, but it won't actually insert any of the new
                # sequences. The insertion is handled in
                # self._sourceRowsInserted.
                all_seq_nums = self._group_by_ann_seq_nums.all
                for i in range(source_start, num_seqs):
                    all_seq_nums[i] += seqs_to_insert
                structured_only = self._group_by_ann_seq_nums.structured_only
                structured_only_start = bisect.bisect_left(
                    structured_only, source_start)
                for i in range(structured_only_start, len(structured_only)):
                    structured_only[i] += seqs_to_insert
        # Update self._seq_info with blank entries.  The SequenceInfo objects
        # will be populated with data in _endRowInsert
        new_seq_info = [SequenceInfo() for i in range(seqs_to_insert)]
        self._seq_info[source_start:source_start] = new_seq_info
    @QtCore.pyqtSlot(QtCore.QModelIndex, int, int)
    def _sourceRowsInserted(self, parent, source_start, source_end):
        """
        Respond to the rowsInserted signal from the source model.  See Qt
        documentation for rowsInserted for argument documentation.
        """
        # populate has_struc in self._seq_info
        source_model = self.sourceModel()
        num_structured = 0
        for seq_num in range(source_start, source_end + 1):
            source_index = source_model.index(seq_num, 0)
            has_struc = source_model.data(source_index, CustomRole.HasStructure)
            self._seq_info[seq_num].has_struc = has_struc
            if has_struc:
                num_structured += 1
        if self._options_model_cache.group_by is GroupBy.Sequence:
            # populate anns in self._seq_info
            structured, structureless = self._getSeqAnnotationLists()
            for seq_num in range(source_start, source_end + 1):
                cur_seq_info = self._seq_info[seq_num]
                self._updateSeqInfoAnnotations(seq_num, cur_seq_info)
            self._last_seq_has_spacer = False
            self.endInsertRows()
        else:  # GroupBy.Type
            # finish inserting sequence rows
            self.endInsertRows()
            # insert child rows into the annotation groups
            self._sourceRowsInsertedGroupByType(source_start, source_end,
                                                num_structured)
        self._clearMouseOverIndex()  # must be after endInsertRows
        # Update the global annotations if any are visible (which only happens
        # if we have at least one sequence loaded)
        num_global_ann = len(self._shown_global_ann)
        if num_global_ann and self._seq_info:
            topleft = self.index(0, 0)
            bottomright = self.index(num_global_ann - ZERO_INDEXED,
                                     self.columnCount() - ZERO_INDEXED)
            self.dataChanged.emit(topleft, bottomright)
    def _sourceRowsInsertedGroupByType(self, source_start, source_end,
                                       num_structured):
        """
        In Group by Type mode, insert child rows into each annotation group.
        See Qt documentation for rowsInserted for additional argument
        documentation.
        :param num_structured: Number of structured sequences
        :type num_structured: int
        """
        all_structureless = not num_structured
        seq_nums = self._genSeqNums()
        self._group_by_ann_seq_nums = seq_nums
        num_global_ann = len(self._shown_global_ann)
        if not all_structureless:
            struc_required_start = bisect.bisect_left(seq_nums.structured_only,
                                                      source_start)
            struc_required_end = struc_required_start + num_structured - 1
        # insert rows into each annotation group
        new_seq_count = len(self._seq_info)
        for ann_group_num, ann_info in enumerate(self._group_by_ann_info):
            ann = ann_info.ann
            if ann.requires_structure and all_structureless:
                # we don't need to insert any rows into this annotation, but
                # we still need to update the bookkeeping
                ann_info.seqs = seq_nums.structured_only
                continue
            group_row = num_global_ann + new_seq_count + ann_group_num
            group_index = self.index(group_row, 0)
            if ann.multi_value:
                if ann.requires_structure:
                    my_seq_nums = seq_nums.structured_only
                    my_start = struc_required_start
                else:
                    my_seq_nums = seq_nums.all
                    my_start = source_start
                previous_seq_num = my_seq_nums[my_start - 1]
                adj_start = ann_info.getProxyRowStart(previous_seq_num)
                adj_end = adj_start
                model = self.sourceModel()
                new_ann_indexes = []
                for seq_num in range(source_start, source_end + 1):
                    index = model.index(seq_num, 0)
                    indexes = model.data(
                        index, RoleBase.SeqAnnotationIndexes + ann.value)
                    new_ann_indexes.append(indexes)
                    adj_end += len(indexes)
                adding_new_rows = any(new_ann_indexes)
                if adding_new_rows:
                    self.beginInsertRows(group_index, adj_start, adj_end - 1)
                tmp_ann_indexes = list(ann_info.ann_indexes)
                tmp_ann_indexes[my_start:my_start] = new_ann_indexes
                ann_info.ann_indexes = tuple(tmp_ann_indexes)
                ann_info.seqs = my_seq_nums
                if not adding_new_rows:
                    # Skip endInsertRows because beginInsertRows wasn't called
                    continue
            elif ann.requires_structure:
                self.beginInsertRows(group_index, struc_required_start,
                                     struc_required_end)
                ann_info.seqs = seq_nums.structured_only
            else:
                self.beginInsertRows(group_index, source_start, source_end)
                ann_info.seqs = seq_nums.all
            self.endInsertRows()
    @QtCore.pyqtSlot(QtCore.QModelIndex, int, int)
    def _sourceRowsAboutToBeRemoved(self, parent, source_start, source_end):
        """
        Respond to the rowsAboutToBeRemoved signal from the source model.  If in
        group by annotation mode, note that this method removes all annotation
        rows for the deleted sequences.  Also note that self._group_by_ann_info
        and self._group_by_ann_seq_nums will be out of sync until
        `_sourceRowsRemoved` is run.
        See Qt documentation for rowsAboutToBeRemoved for argument
        documentation.
        """
        num_seqs = len(self._seq_info)
        num_global_ann = len(self._shown_global_ann)
        removing_last_row = source_end == (num_seqs - ZERO_INDEXED)
        if source_start == 0 and removing_last_row:
            # We're clearing the table
            self.beginRemoveRows(QtCore.QModelIndex(), 0, self.rowCount() - 1)
            self._seq_info = []
            if self._options_model_cache.group_by is GroupBy.Type:
                self._group_by_ann_seq_nums.all[:] = []
                self._group_by_ann_seq_nums.structured_only[:] = []
            return
        if self._options_model_cache.group_by is GroupBy.Type:
            self._sourceRowsAboutToBeRemovedGroupByType(source_start,
                                                        source_end)
        proxy_start = num_global_ann + source_start
        proxy_end = num_global_ann + source_end
        self.beginRemoveRows(QtCore.QModelIndex(), proxy_start, proxy_end)
        del self._seq_info[source_start:source_end + 1]
        if ((parent is None or not parent.isValid()) and removing_last_row and
                self._seq_info[source_start - 1].anns and
                self._options_model_cache.group_by is GroupBy.Sequence):
            # The new last sequence's spacer row will be removed after finishing
            # the top level row removal in _sourceRowsRemoved
            self._last_seq_has_spacer = True
    def _sourceRowsAboutToBeRemovedGroupByType(self, source_start, source_end):
        """
        In Group by Type mode, remove annotation rows for sequences about to be
        removed. Note that self._group_by_ann_info and
        self._group_by_ann_seq_nums will be out of sync until
        `_sourceRowsRemoved` is run.
        See Qt documentation for rowsAboutToBeRemoved for argument
        documentation.
        """
        num_seqs = len(self._seq_info)
        num_global_ann = len(self._shown_global_ann)
        new_seq_nums = copy.deepcopy(self._group_by_ann_seq_nums)
        del new_seq_nums.all[source_start:source_end + 1]
        num_to_remove_with_struc = sum(
            1 for seq_info in self._seq_info[source_start:source_end + 1]
            if seq_info.has_struc)
        if num_to_remove_with_struc > 0:
            struc_required_start = bisect.bisect_left(
                new_seq_nums.structured_only, source_start)
            struc_required_end = (struc_required_start +
                                  num_to_remove_with_struc - 1)
            del new_seq_nums.structured_only[
                struc_required_start:struc_required_end + 1]
        # remove rows from each annotation group
        for ann_group_num, ann_info in enumerate(self._group_by_ann_info):
            ann = ann_info.ann
            if (ann.requires_structure and num_to_remove_with_struc == 0):
                # don't remove any rows from structure-required annotations
                # if all sequences being removed are structureless
                ann_info.seqs = new_seq_nums.structured_only
                continue
            group_row = num_global_ann + num_seqs + ann_group_num
            group_index = self.index(group_row, 0)
            if ann.multi_value:
                if ann.requires_structure:
                    my_seq_nums = new_seq_nums.structured_only
                    my_start = struc_required_start
                    my_end = struc_required_end
                else:
                    my_seq_nums = new_seq_nums.all
                    my_start = source_start
                    my_end = source_end
                previous_seq_num = my_seq_nums[my_start - 1]
                adj_start = ann_info.getProxyRowStart(previous_seq_num)
                adj_end = adj_start
                tmp_ann_indexes = list(ann_info.ann_indexes)
                for seq_num in reversed(range(my_start, my_end + 1)):
                    indexes = tmp_ann_indexes.pop(seq_num)
                    adj_end += len(indexes)
                ann_info.ann_indexes = tuple(tmp_ann_indexes)
                removing_rows = adj_end - adj_start > 0
                if removing_rows:
                    self.beginRemoveRows(group_index, adj_start, adj_end - 1)
                ann_info.seqs = my_seq_nums
                if not removing_rows:
                    continue
            elif ann.requires_structure:
                self.beginRemoveRows(group_index, struc_required_start,
                                     struc_required_end)
                ann_info.seqs = new_seq_nums.structured_only
            else:
                self.beginRemoveRows(group_index, source_start, source_end)
                ann_info.seqs = new_seq_nums.all
            self.endRemoveRows()
    @QtCore.pyqtSlot()
    def _sourceRowsRemoved(self):
        """
        Respond to the rowsRemoved signal from the source model.  See
        Qt documentation for rowsRemoved for argument documentation.
        """
        if (not self._seq_info or
                self._options_model_cache.group_by is GroupBy.Sequence):
            self.endRemoveRows()
            if (self._options_model_cache.group_by is GroupBy.Sequence and
                    self._last_seq_has_spacer):
                # In group by sequence mode, remove spacer row from new last row
                new_last_proxy_row = self.rowCount() - 1
                spacer_parent = self.index(new_last_proxy_row, 0)
                last_row_seq = self._seq_info[-1]
                spacer_idx = len(last_row_seq.anns)
                self.beginRemoveRows(spacer_parent, spacer_idx, spacer_idx)
                self._last_seq_has_spacer = False
                self.endRemoveRows()
        else:  # GroupBy.Type
            # update self._group_by_ann_info and self._group_by_ann_seq_nums to
            # reflect the new sequence numbering.  Note that we're just updating
            # the numbering here.  The rows were already removed in
            # _beginRemoveRows.
            seq_nums = self._genSeqNums()
            self._group_by_ann_seq_nums = seq_nums
            for ann_info in self._group_by_ann_info:
                if ann_info.ann.requires_structure:
                    ann_info.seqs = seq_nums.structured_only
                else:
                    ann_info.seqs = seq_nums.all
            # finish removing rows from the structure group
            self.endRemoveRows()
        self._clearMouseOverIndex()  # Must be after endRemoveRows
        # Update the global annotations
        num_global_ann = len(self._shown_global_ann)
        if num_global_ann and self.rowCount():
            topleft = self.index(0, 0)
            bottomright = self.index(num_global_ann - ZERO_INDEXED,
                                     self.columnCount() - ZERO_INDEXED)
            self.dataChanged.emit(topleft, bottomright)
    @QtCore.pyqtSlot(QtCore.QModelIndex, QtCore.QModelIndex)
    def _modelDataChanged(self, source_topleft, source_bottomright):
        """
        Respond to the dataChanged signal from the source model.  See
        Qt documentation for dataChanged for argument documentation.
        """
        if not source_topleft.isValid() and not source_bottomright.isValid():
            self.dataChanged.emit(source_topleft, source_bottomright)
            return
        source_start_row = source_topleft.row()
        source_end_row = source_bottomright.row()
        col_left = source_topleft.column()
        col_right = source_bottomright.column()
        num_global_ann = len(self._shown_global_ann)
        # emit data changed for the sequence rows
        proxy_start_row = num_global_ann + source_start_row
        proxy_end_row = num_global_ann + source_end_row
        proxy_topleft = self.index(proxy_start_row, col_left)
        proxy_bottomright = self.index(proxy_end_row, col_right)
        self.dataChanged.emit(proxy_topleft, proxy_bottomright)
        if self._options_model_cache.group_by is GroupBy.Sequence:
            for seq_num in range(source_start_row, source_end_row + 1):
                anns = self._seq_info[seq_num].anns
                if not anns:
                    continue
                parent = self.index(num_global_ann + seq_num, 0)
                top_left = self.index(0, col_left, parent)
                bottom_right = self.index(
                    len(anns) - ZERO_INDEXED, col_right, parent)
                self.dataChanged.emit(top_left, bottom_right)
        elif self._options_model_cache.group_by is GroupBy.Type:
            num_seqs = len(self._seq_info)
            seq_nums = self._group_by_ann_seq_nums
            struc_required_start = bisect.bisect_left(seq_nums.structured_only,
                                                      source_start_row)
            struc_required_end = bisect.bisect_right(seq_nums.structured_only,
                                                     source_end_row,
                                                     struc_required_start) - 1
            # emit data changed for each annotation group
            for ann_group_num, ann_info in enumerate(self._group_by_ann_info):
                group_row = num_global_ann + num_seqs + ann_group_num
                group_index = self.index(group_row, 0)
                if not ann_info.ann.requires_structure:
                    top_left = self.index(source_start_row, col_left,
                                          group_index)
                    bottom_right = self.index(source_end_row, col_right,
                                              group_index)
                    self.dataChanged.emit(top_left, bottom_right)
                elif struc_required_start <= struc_required_end:
                    top_left = self.index(struc_required_start, col_left,
                                          group_index)
                    bottom_right = self.index(struc_required_end, col_right,
                                              group_index)
                    self.dataChanged.emit(top_left, bottom_right)
        if num_global_ann:
            global_topleft = self.index(0, col_left)
            global_bottomright = self.index(num_global_ann - ZERO_INDEXED,
                                            col_right)
            self.dataChanged.emit(global_topleft, global_bottomright)
    def _seqExpansionChanged(self, source_indices, expanded):
        # See SeqExpansionProxyMixin._seqExpansionChanged for method
        # documentation
        super()._seqExpansionChanged(source_indices, expanded)
        self._clearMouseOverIndex()
    def _updateGroupBy(self):
        """
        Should the rows be grouped by sequence or by annotation type?
        """
        group_by = self._options_model_cache.group_by
        if not isinstance(group_by, GroupBy):
            raise TypeError
        self.beginResetModel()
        try:
            self._initializeBookkeeping()
        finally:
            self.endResetModel()
        self.groupByChanged.emit(group_by)
[docs]    def getGroupBy(self):
        """
        Are the rows currently grouped by sequence or by annotation type?
        :return: The current setting
        :rtype: `GroupBy`
        """
        return self._options_model_cache.group_by 
    @QtCore.pyqtSlot()
    def _alnSetChanged(self):
        has_sets = self.getAlignment().hasAlnSets()
        if has_sets != self._show_alignment_set:
            self._setAlnSetShown(has_sets)
        # always invalidate filtering so that we update whether alignment set
        # rows are shown or not for each sequence
        self._invalidateFilter()
    def _setAlnSetShown(self, show):
        self._show_alignment_set = show
        if show:
            self._shown_row_types.add(SEQ_ANNO_TYPES.alignment_set)
        else:
            self._shown_row_types.discard(SEQ_ANNO_TYPES.alignment_set)
    def _updateVisibleRowTypes(self):
        """
        Update visible row types based on the options model.
        """
        row_types = self._options_model.all_visible_annotations
        self._setShownRowTypes(row_types)
    def _setShownRowTypes(self, row_types):
        """
        Allow only the specified annotations
        .. NOTE::
            Calling code should set shown annotations via
            `schrodinger.gui_models.OptionsModel`
            (residue_propensity_annotations, sequence_annotations, and
            alignment_annotations)
        :param row_types: An iterable containing the annotations to allow
        :type row_types: iter
        """
        self._shown_row_types.clear()
        self._shown_row_types.update(set(row_types))
        if self._show_alignment_set:
            self._shown_row_types.add(SEQ_ANNO_TYPES.alignment_set)
        self._invalidateFilter()
[docs]    def getShownRowTypes(self):
        """
        Return a set of allowed annotation types
        :return: A set of annotation types.  Note that the returned value is a
            copy of the attribute variable, so modifying it will not have any
            effect on this proxy.
        :rtype: set
        """
        return set(self._shown_row_types) 
    def _setVisibilityForRowType(self, row_type, show=True):
        """
        Toggle visibility for the specified annotation
        .. NOTE::
            This method is provided as a convenience for tests. Calling code
            should set shown annotations via
            `schrodinger.gui_models.OptionsModel`
            (residue_propensity_annotations, sequence_annotations, and
            alignment_annotations)
        :param row_type: The annotation to adjust
        :type row_type: `enum.Enum`
        :param show: Whether the annotation should be shown or hidden
        :type show: bool
        """
        if show:
            self._shown_row_types.add(row_type)
        else:
            self._shown_row_types.discard(row_type)
        self._invalidateFilter()
    def _setVisibilityForRowTypes(self, row_types, show=True):
        """
        Toggle visibility for the specified annotations
        .. NOTE::
            This method is provided as a convenience for tests. Calling code
            should set shown annotations via
            `schrodinger.gui_models.OptionsModel`
            (residue_propensity_annotations, sequence_annotations, and
            alignment_annotations)
        :param row_types: An iterable containing the annotations to adjust
        :type row_types: iter
        :param show: Whether the annotations should be shown or hidden
        :type show: bool
        """
        if show:
            self._shown_row_types.update(row_types)
        else:
            self._shown_row_types.difference_update(row_types)
        self._invalidateFilter()
[docs]    def getNumShownGlobalAnnotations(self):
        """
        Return the number of shown global annotations
        """
        return len(self._shown_global_ann) 
    @QtCore.pyqtSlot()
    def _invalidateFilter(self):
        """
        Update which annotations are shown and hidden.  Update
        `self._shown_row_types` before calling this method.
        """
        if not self._seq_info:
            # If the table is empty, we don't need to worry about inserting or
            # removing rows
            self._shown_global_ann = [
                ann for ann in self._all_global_ann
                if ann in self._shown_row_types
            ]
            if self._options_model_cache.group_by is GroupBy.Type:
                self._group_by_ann_info = [
                    GroupByAnnotationInfo(cur_ann, [])
                    for cur_ann in self._all_seq_ann
                    if cur_ann in self._shown_row_types
                ]
            return
        else:
            # update the global annotation rows
            new_anns = list(ann for ann in self._all_global_ann
                            if ann in self._shown_row_types)
            self._updateAnnotationList(self._shown_global_ann, new_anns,
                                       QtCore.QModelIndex())
            # update the sequence annotation rows
            if self._options_model_cache.group_by is GroupBy.Sequence:
                self._updateSeqAnnForGroupBySeq()
            else:
                self._updateSeqAnnForGroupByAnn()
    @QtCore.pyqtSlot()
    def _onSequenceStructureChanged(self):
        """
        Update the sequence info to have the latest has structure.
        Then update which annotations are visible.
        """
        self._updateHasStructure()
        self._invalidateFilter()
    def _updateHasStructure(self):
        """
        Update the sequence information for having structure when sequence
        structure is changed.
        """
        source_model = self.sourceModel()
        for row in range(source_model.rowCount()):
            index = source_model.index(row, 0)
            has_structure = source_model.data(index, CustomRole.HasStructure)
            self._seq_info[row].has_struc = has_structure
    def _updateAnnotationList(self,
                              cur_shown_ann,
                              to_show_ann,
                              parent,
                              account_for_spacer=False):
        """
        Update the current list of shown annotations to match `to_show_ann`.
        Row insertion and removal signals will be emitted for all changes.
        :param cur_shown_ann: The list of shown annotations to update
        :type cur_shown_ann: list
        :param to_show_ann: The list of annotations that `cur_shown_ann`
            should be updated to match. Note that all annotations shared
            between `cur_shown_ann` and `to_show_ann` should be in the same
            order.
        :type to_show_ann: list
        :param parent: The parent index to emit with the row insertion and
            removal signals.
        :type parent: `QtCore.QModelIndex`
        :param account_for_spacer: Whether the `parent` group can have a spacer
            as the last row.  If True, this spacer row will be removed when the
            last annotation is removed and added when adding an annotation to an
            empty group.
        :type account_for_spacer: bool
        """
        # remove all annotations that were shown but are now hidden
        for i, cur_ann in reversed(list(enumerate(cur_shown_ann))):
            if cur_ann not in to_show_ann:
                if account_for_spacer and len(cur_shown_ann) == 1:
                    spacer = 1
                else:
                    spacer = 0
                self.beginRemoveRows(parent, i, i + spacer)
                cur_shown_ann.pop(i)
                self.endRemoveRows()
        # add all annotations that were hidden but are now shown
        i = 0
        for cur_ann in to_show_ann:
            if cur_ann in cur_shown_ann:
                i += 1
            else:
                if account_for_spacer and not cur_shown_ann:
                    spacer = 1
                else:
                    spacer = 0
                self.beginInsertRows(parent, i, i + spacer)
                cur_shown_ann.insert(i, cur_ann)
                self.endInsertRows()
                i += 1
    def _updateSeqAnnForGroupBySeq(self):
        """
        Update which sequence annotations are shown when rows are grouped by
        sequence.
        """
        last_seq_num = len(self._seq_info) - 1
        for seq_num, cur_seq in enumerate(self._seq_info):
            new_anns = self._getAnnList(seq_num, cur_seq.has_struc)
            row = len(self._shown_global_ann) + seq_num
            parent = self.index(row, 0)
            last_seq = seq_num == last_seq_num
            account_for_spacer = self._show_spacers and not last_seq
            self._updateAnnotationList(cur_seq.anns,
                                       new_anns,
                                       parent,
                                       account_for_spacer=account_for_spacer)
    def _updateSeqAnnForGroupByAnn(self):
        """
        Update which sequence annotations are shown when rows are grouped by
        annotation.
        """
        # remove all annotations that were shown but are now hidden
        num_seqs = len(self._seq_info)
        invalid_index = QtCore.QModelIndex()
        for i, cur_ann_info in reversed(list(enumerate(
                self._group_by_ann_info))):
            if cur_ann_info.ann not in self._shown_row_types:
                row_num = len(self._shown_global_ann) + num_seqs + i
                self.beginRemoveRows(invalid_index, row_num, row_num)
                self._group_by_ann_info.pop(i)
                self.endRemoveRows()
        # add all annotations that were hidden but are now shown
        i = 0
        shown_ann = [ann_info.ann for ann_info in self._group_by_ann_info]
        for cur_ann in self._all_seq_ann:
            if cur_ann in shown_ann:
                i += 1
            elif cur_ann in self._shown_row_types:
                row_num = len(self._shown_global_ann) + num_seqs + i
                self.beginInsertRows(invalid_index, row_num, row_num)
                ann_info = self._createGroupByAnnInfo(cur_ann)
                self._group_by_ann_info.insert(i, ann_info)
                self.endInsertRows()
                i += 1
[docs]    @QtCore.pyqtSlot()
    def endInsertColumns(self):
        # See Qt documentation for method documentation
        self._column_count = self.sourceModel().columnCount()
        super().endInsertColumns() 
[docs]    @QtCore.pyqtSlot()
    def endRemoveColumns(self):
        # See Qt documentation for method documentation
        self._column_count = self.sourceModel().columnCount()
        super().endRemoveColumns() 
[docs]    def tableWidthChanged(self, width):
        """
        Emit the `tableWidthChangedSignal` with the specified width.  The
        `RowWrapProxyModel` should receive this signal and adjust the wrapping
        as necessary.
        :param width: The current number of columns in the table
        :type width: int
        """
        self.tableWidthChangedSignal.emit(width) 
[docs]    def rowWrapEnabled(self):
        """
        :return: Whether this model provides row-wrapped data.
        :type: bool
        .. warning:: You probably don't want to use this method. Whenever
            possible, you should pass information to the model and have it
            respond as appropriate (i.e. tell, don't ask). This method should
            only be used for view features that function differently depending
            on whether the model is row-wrapped or not (e.g. drag-and-drop
            auto-scrolling).
        """
        return False 
[docs]    def getPickingMode(self):
        """
        :return: The current picking mode
        .. warning:: This method should only be used for view features that
            function differently depending on the pick mode (e.g. mouse
            clicks).
        """
        return self._options_model.pick_mode 
[docs]    def handlePick(self, index):
        """
        Handle a pick event at the given proxy index.
        Has no effect if the pick mode is not HMBindingSite or HMProximity.
        """
        pick_mode = self.getPickingMode()
        if pick_mode not in (PickMode.HMBindingSite, PickMode.HMProximity):
            return
        source_index, ann_type, ann, ann_idx = self._mapIndexToSource(index)
        if (pick_mode is PickMode.HMBindingSite and
                ann is SEQ_ANNO_TYPES.binding_sites):
            self.sourceModel().handleBindingSitePick(source_index, ann_idx)
        elif (pick_mode is PickMode.HMProximity and
              ann_type is RowType.Sequence):
            self.sourceModel().handleProximityPick(source_index) 
    def _updateShowSpacer(self):
        """
        Respond to a change in OptionsModel.annotation_spacer_enabled by adding
        or removing spacer rows.  Note that this setting only affects
        group-by-sequence mode.
        """
        if self._show_spacers == self._options_model.annotation_spacer_enabled:
            # nothing has actually changed, so we don't need to do anything here
            return
        elif (self._options_model_cache.group_by is GroupBy.Type or
              not self._shown_row_types.intersection(self._all_seq_ann)):
            # this change isn't going to affect any visible spacers (either
            # because we're in group-by-type mode or because no sequence-level
            # annotations are shown), so we don't have to worry about emitting
            # signals for adding or removing spacer rows
            pass
        elif self._options_model.annotation_spacer_enabled:
            # adding spacer rows
            # We set _show_spacers to None since some sequences will have spacer
            # rows and some won't while we're in the process of adding spacers.
            # That way, rowCount will use _show_spacers_before_row instead.
            self._show_spacers = None
            self._show_spacers_before_row = 0
            # we don't need to add a spacer row to the last sequence
            for seq_num, cur_seq in enumerate(self._seq_info[:-1]):
                if not cur_seq.anns:
                    continue
                parent_row_num = len(self._shown_global_ann) + seq_num
                spacer_row_num = len(self._seq_info[seq_num].anns)
                seq_row_index = self.index(parent_row_num, 0)
                self.beginInsertRows(seq_row_index, spacer_row_num,
                                     spacer_row_num)
                self._show_spacers_before_row = parent_row_num + 1
                self.endInsertRows()
        else:
            # removing spacer rows
            # We set _show_spacers to None since some sequences will have spacer
            # rows and some won't while we're in the process of removing
            # spacers.  That way, rowCount will use _show_spacers_before_row
            # instead.
            self._show_spacers = None
            self._show_spacers_before_row = self.rowCount()
            # the last sequence doesn't have a spacer row
            for seq_num, cur_seq in reversed(
                    list(enumerate(self._seq_info[:-1]))):
                if not cur_seq.anns:
                    continue
                parent_row_num = len(self._shown_global_ann) + seq_num
                spacer_row_num = len(self._seq_info[seq_num].anns)
                seq_row_index = self.index(parent_row_num, 0)
                self.beginRemoveRows(seq_row_index, spacer_row_num,
                                     spacer_row_num)
                self._show_spacers_before_row = parent_row_num
                self.endRemoveRows()
        self._show_spacers_before_row = -1
        self._show_spacers = self._options_model.annotation_spacer_enabled 
[docs]class GroupByProxyMixin:
    groupByChanged = QtCore.pyqtSignal(object)
[docs]    def setSourceModel(self, model):
        # See QAbstractItemView documentation for method documentation
        old_model = self.sourceModel()
        signals = {'groupByChanged': self.groupByChanged}
        if old_model is not None:
            table_helper.disconnect_signals(old_model, signals)
        super(GroupByProxyMixin, self).setSourceModel(model)
        table_helper.connect_signals(model, signals) 
[docs]    def getGroupBy(self):
        """
        :return: The current `GroupBy` setting.
        :rtype: viewconstants.GroupBy
        """
        return self.sourceModel().getGroupBy()  
[docs]class PostAnnotationProxyMixin(GroupByProxyMixin, MouseOverPassthroughMixin,
                               AnnotationSelectionPassthroughMixin, ProxyMixin):
    """
    A mixin for all proxies that are used after the
    `AnnotationProxyModel`.
    """
[docs]    def getShownRowTypes(self):
        """
        See `AnnotationProxyModel.getShownRowTypes` for method documentation.
        """
        return self.sourceModel().getShownRowTypes() 
[docs]    def getNumShownGlobalAnnotations(self):
        """
        See `AnnotationProxyModel.getNumShownGlobalAnnotations` for method
        documentation
        """
        return self.sourceModel().getNumShownGlobalAnnotations() 
[docs]    def rowWrapEnabled(self):
        """
        See `AnnotationProxyModel.rowWrapEnabled` for method documentation
        """
        return self.sourceModel().rowWrapEnabled() 
[docs]    def getPickingMode(self):
        """
        See `AnnotationProxyModel.getPickingMode` for method documentation
        """
        return self.sourceModel().getPickingMode()  
[docs]class RowWrapInsertingRows(object):
    """
    An object to store `RowWrapProxyModel` bookkeeping for the insertion of
    top-level rows.
    :ivar old_source_row_count: The number of rows in the source model before
        the insertion.
    :vartype old_source_row_count: int
    :ivar new_source_row_count: The number of rows in the source model after
        the insertion.
    :vartype new_source_row_count: int
    :ivar source_start: The row number of the first row being inserted.
    :vartype source_start: int
    :ivar source_start: The row number of the last row being inserted.
    :vartype source_start: int
    :ivar num_new_rows: The number of rows being inserted.
    :vartype num_new_rows: int
    :ivar wrap: The wrap of `RowWrapProxyModel` that rows were just
        inserted into - see `RowWrapProxyModel._sourceRowsAboutToBeInserted`.
    :vartype wrap: int
    :ivar update_row_num: The row number of `RowWrapProxyModel` that was
        just updated; i.e., that just had child rows inserted into it - see
        `RowWrapProxyModel._sourceRowsInserted`.
    :vartype update_row_num: int
    """
[docs]    def __init__(self, old_source_row_count, new_source_row_count, source_start,
                 source_end):
        self.old_source_row_count = old_source_row_count
        self.new_source_row_count = new_source_row_count
        self.source_start = source_start
        self.source_end = source_end
        self.num_new_rows = source_end - source_start + 1
        self.wrap = -1
        self.update_row_num = -1  
[docs]class RowWrapRemovingRows(object):
    """
    An object to store `RowWrapProxyModel` bookkeeping for the removal of
    top-level rows.
    :ivar old_source_row_count: The number of rows in the source model before
        the removal.
    :vartype old_source_row_count: int
    :ivar new_source_row_count: The number of rows in the source model after
        the removal.
    :vartype new_source_row_count: int
    :ivar source_start: The row number of the first row being removed.
    :vartype source_start: int
    :ivar source_start: The row number of the last row being removed.
    :vartype source_start: int
    :ivar num_rem_rows: The number of rows being removed.
    :type num_new_rows: int
    :ivar wrap: The wrap of `RowWrapProxyModel` that rows were just
        removed from - see `RowWrapProxyModel._sourceRowsAboutToBeRemoved`.
    :vartype wrap: int
    """
[docs]    def __init__(self, old_source_row_count, new_source_row_count, source_start,
                 source_end):
        self.old_source_row_count = old_source_row_count
        self.new_source_row_count = new_source_row_count
        self.source_start = source_start
        self.source_end = source_end
        self.num_rem_rows = source_end - source_start + 1
        self.wrap = -1  
[docs]class RowWrapInsertingChildRows(object):
    """
    An object to store `RowWrapProxyModel` bookkeeping for the insertion of
    child rows.
    :ivar source_parent_row: The row number of the top-level row that the child
        rows are being inserted into.
    :vartype source_parent_row: int
    :ivar source_start: The row number of the first row being inserted.
    :vartype source_start: int
    :ivar source_start: The row number of the last row being inserted.
    :vartype source_start: int
    :ivar num_new_rows: The number of rows being inserted.
    :vartype num_new_rows: int
    :ivar wrap: The wrap of `RowWrapProxyModel` that rows were just
        inserted into - see `RowWrapProxyModel._sourceRowsAboutToBeInserted`.
    :vartype wrap: int
    """
[docs]    def __init__(self, source_parent_row, source_start, source_end):
        self.source_parent_row = source_parent_row
        self.source_start = source_start
        self.source_end = source_end
        self.num_new_rows = source_end - source_start + 1
        self.wrap = -1  
[docs]class RowWrapRemovingChildRows(object):
    """
    An object to store `RowWrapProxyModel` bookkeeping for the removal of
    child rows.
    :ivar source_parent_row: The row number of the top-level row that the child
        rows are being removed from.
    :vartype source_parent_row: int
    :ivar source_start: The row number of the first row being removed.
    :vartype source_start: int
    :ivar source_start: The row number of the last row being removed.
    :vartype source_start: int
    :ivar num_rem_rows: The number of rows being removed.
    :type num_new_rows: int
    :ivar wrap: The wrap of `RowWrapProxyModel` that rows were just
        removed from - see `RowWrapProxyModel._sourceRowsAboutToBeRemoved`.
    :vartype wrap: int
    """
[docs]    def __init__(self, source_parent_row, source_start, source_end):
        self.source_parent_row = source_parent_row
        self.source_start = source_start
        self.source_end = source_end
        self.num_rem_rows = source_end - source_start + 1
        self.wrap = -1  
[docs]class RowWrapProxyModel(PerRowFlagCacheProxyMixin, PostAnnotationProxyMixin,
                        NestedProxy):
    """
    A proxy model that wraps rows to a specified column count.  A blank spacer
    row will be inserted between each wrap.  (Note that there's no spacer row
    before the first wrap or after the last wrap.)
    :ivar _width: The current width of the table view, measured in number of
        columns.  The is the width that this proxy will wrap to.
    :type _width: int
    :ivar _source_column_count: The current number of columns in this proxy's
        source model.
    :vartype _source_column_count: int
    :ivar _column_count: The current number of columns in this proxy.
    :vartype _column_count: int
    :ivar _wrap_count: The number of times the rows are wrapped to fit within
        `_width` columns.
    :vartype _wrap_count: int
    :ivar _top_level_row_count: The current number of top-level row in this
        proxy (i.e. excluding children rows).
    :vartype _top_level_row_count: int
    :ivar _source_child_row_counts: The number of child rows for each top-level
        row of the source model.  Note that the length of this list is always
        equal to the number of top-level rows in the source model.
    :vartype _source_child_row_counts: list
    :ivar _inserting_rows: If we're in the process of inserting top-level rows
        into the model, this will be a `RowWrapInsertingRows` object describing
        the insertion.  Will be None at all other times.
    :vartype _inserting_rows: `RowWrapInsertingRows` or NoneType
    :ivar _updating_inserted_rows: If we're in the process of updating top-level
        rows that were just inserted into the model, this will be a
        `RowWrapInsertingRows` object describing the updating.  Will be None at
        all other times.
    :vartype _updating_inserted_rows: `RowWrapInsertingRows` or NoneType
    :ivar _inserting_child_rows: If we're in the process of inserting child rows
        into the model, this will be a `RowWrapInsertingChildRows` object
        describing the insertion.  Will be None at all other times.
    :vartype _inserting_child_rows: `RowWrapInsertingChildRows` or NoneType
    :ivar _removing_rows: If we're in the process of removing top-level rows
        from the model, this will be a `RowWrapRemovingRows` object describing
        the removal.  Will be None at all other times.
    :vartype _removing_rows: `RowWrapRemovingRows` or NoneType
    :ivar _removing_child_rows: If we're in the process of removing child rows
        from the model, this will be a `RowWrapRemovingChildRows` object
        describing the removal.  Will be None at all other times.
    :vartype _removing_child_rows: `RowWrapRemovingChildRows` or NoneType
    """
[docs]    def __init__(self, parent=None):
        # See Qt documentation for argument documentation
        super().__init__(parent)
        self._width = 100
        self._source_column_count = 0
        self._column_count = 0
        self._wrap_count = 1
        self._top_level_row_count = 0
        self._source_child_row_counts = []
        self._inserting_rows = None
        self._updating_inserted_rows = None
        self._inserting_child_rows = None
        self._removing_rows = None
        self._removing_child_rows = None 
[docs]    def tableWidthChanged(self, width):
        """
        Wrap the table to the specified number of columns.
        :param width: The number of columns to wrap to.
        :type width: int
        """
        if width == 0:
            # this happens when the view is in the process of being shown
            return
        self.beginResetModel()
        self._width = width
        self._wrap_count = self._wrapCount(self._source_column_count)
        source_row_count = len(self._source_child_row_counts)
        self._top_level_row_count = self._wrap_count * (source_row_count +
                                                        SPACER) - SPACER
        self._column_count = min(self._source_column_count, self._width)
        self._flag_cache.clear()
        self.endResetModel() 
[docs]    @table_helper.model_reset_method
    def setSourceModel(self, model):
        # See Qt documentation for argument documentation
        super(RowWrapProxyModel, self).setSourceModel(model)
        model.modelAboutToBeReset.connect(self.beginResetModel)
        model.modelReset.connect(self._sourceModelReset)
        model.layoutAboutToBeChanged.connect(self.layoutAboutToBeChanged)
        model.layoutChanged.connect(self._sourceLayoutChanged)
        model.rowsAboutToBeInserted.connect(self._sourceRowsAboutToBeInserted)
        model.rowsInserted.connect(self._sourceRowsInserted)
        model.columnsAboutToBeInserted.connect(
            self._sourceColumnsAboutToBeInserted)
        model.columnsAboutToBeRemoved.connect(
            self._sourceColumnsAboutToBeRemoved)
        model.columnsInserted.connect(self._sourceColumnsInsertedOrRemoved)
        model.columnsRemoved.connect(self._sourceColumnsInsertedOrRemoved)
        model.dataChanged.connect(self._sourceDataChanged)
        model.rowsAboutToBeRemoved.connect(self._sourceRowsAboutToBeRemoved)
        model.rowsRemoved.connect(self._sourceRowsRemoved)
        model.tableWidthChangedSignal.connect(self.tableWidthChanged)
        self._resetColumnCount()
        self._resetRowCounts() 
    def _resetColumnCount(self):
        """
        Reset all column-related bookkeeping data using the current source model
        column count.
        """
        source_model = self.sourceModel()
        source_col_count = source_model.columnCount()
        self._source_column_count = source_col_count
        self._column_count = min(source_col_count, self._width)
        self._wrap_count = self._wrapCount(source_col_count)
    def _resetRowCounts(self):
        """
        Reset all row-related bookkeeping data using the current source model
        row counts.
        :note: If resetting both column- and row-related bookkeeping data,
            `_resetColumnCount` must be run before `_resetRowCounts`, as this
            function uses `_wrap_count`, which gets reset in {_resetColumnCount}.
        """
        source_model = self.sourceModel()
        new_row_counts = []
        source_row_count = source_model.rowCount()
        self._top_level_row_count = self._wrap_count * (source_row_count +
                                                        SPACER) - SPACER
        for cur_row in range(source_row_count):
            index = source_model.index(cur_row, 0)
            row_count = source_model.rowCount(index)
            new_row_counts.append(row_count)
        self._source_child_row_counts = new_row_counts
    def _wrapCount(self, source_cols):
        """
        Calculate the number of times that a table would need to be wrapped to
        fit into the current width.
        :param source_cols:  The number of columns in the source table to use
            for the wrapping calculation.
        :type source_cols: int
        :return: The number of wrappings
        :rtype: int
        """
        num_wraps = math.ceil(source_cols / float(self._width))
        num_wraps = int(num_wraps)
        return max(num_wraps, 1)
[docs]    def columnCount(self, parent=None):
        # See Qt documentation for method documentation
        return self._column_count 
[docs]    def rowCount(self, parent=None):
        # See Qt documentation for method documentation
        if parent is None or not parent.isValid():
            return self._top_level_row_count
        elif parent.internalId() != TOP_LEVEL:
            # parent is not a top-level row
            return 0
        else:
            source_row, wrap_num, is_spacer = \
                
self._mapTopLevelRowToSource(parent.row())
            if is_spacer or source_row is None:
                # spacer row between wraps or can't map source_row (which means
                # we're in the process of inserting the source row)
                return 0
            proxy_row = parent.row()
            row_count = self._source_child_row_counts[source_row]
            ins = self._updating_inserted_rows
            ins_child = self._inserting_child_rows
            rem_child = self._removing_child_rows
            if (ins is not None and
                    ins.source_start <= source_row <= ins.source_end and
                    proxy_row > ins.update_row_num):
                # We're in the process of inserting top-level rows.  Dummy
                # rows (with no children) were inserted into each wrap and
                # we haven't yet emitted signals to update the dummy rows in
                # this wrap, so we shouldn't report any children yet.
                row_count = 0
            elif (ins_child is not None and
                  source_row == ins_child.source_parent_row and
                  wrap_num > ins_child.wrap):
                # We're in the process of inserting child rows.
                # self._source_child_row_counts has already been updated,
                # but we haven't emitted signals for inserting rows into
                # this wrap yet, so we shouldn't count the new rows.
                row_count -= ins_child.num_new_rows
            elif (rem_child is not None and
                  source_row == rem_child.source_parent_row and
                  wrap_num > rem_child.wrap):
                # We're in the process of removing child rows.
                # self._source_child_row_counts has already been updated,
                # but we haven't emitted signals for removing rows in this
                # wrap yet, so we should still count the removed rows.
                row_count += rem_child.num_rem_rows
            return row_count 
[docs]    def mapFromSource(self, source_index):
        # See Qt documentation for argument documentation
        if not source_index.isValid():
            return QtCore.QModelIndex()
        source_model = self.sourceModel()
        source_parent = source_model.parent(source_index)
        if not source_parent.isValid():
            proxy_index, is_valid, wrap_num = self._mapTopLevelIndexFromSource(
                source_index)
            return proxy_index
        else:
            source_col = source_index.column()
            proxy_parent, is_valid, wrap_num = self._mapTopLevelIndexFromSource(
                source_parent, child_col=source_col)
            if not is_valid:
                # the source index is a child of a row that in the process of
                # being removed
                return proxy_parent
            proxy_row = source_index.row()
            proxy_col = source_col % self._width
            ins = self._inserting_child_rows
            rem = self._removing_child_rows
            if (ins is not None and
                    source_parent.row() == ins.source_parent_row and
                    proxy_row >= ins.source_start and wrap_num <= ins.wrap):
                proxy_row += ins.num_new_rows
            elif (rem is not None and
                  source_parent.row() == rem.source_parent_row and
                  wrap_num <= rem.wrap):
                if proxy_row > rem.source_end:
                    proxy_row -= rem.num_rem_rows
                elif proxy_row >= rem.source_start:
                    return QtCore.QModelIndex()
            return self.index(proxy_row, proxy_col, proxy_parent) 
    def _mapTopLevelIndexFromSource(self, source_index, child_col=None):
        """
        Map a top-level index from the source model.
        :param source_index: The source index to map.
        :type source_index: `QtCore.QModelIndex`
        :param child_col: If `source_index` is the parent of a child index
            that's being mapped, the column of the child index, needed so that this
            method can return a proxy index from the appropriate wrap.  Should be
            None otherwise.
        :type child_col: int or NoneType
        :return: A tuple of:
            - The equivalent proxy index (`QtCore.QModelIndex`)
            - Whether the proxy index is valid (bool)
            - The wrap that the proxy index is in.  Will be None if the proxy
            index is invalid. (int or NoneType)
        :rtype: tuple
        """
        source_row = source_index.row()
        if child_col is None:
            source_col = source_index.column()
            proxy_col = source_col % self._width
        else:
            source_col = child_col
            proxy_col = 0
        wrap_num = source_col // self._width
        ins = self._inserting_rows
        rem = self._removing_rows
        if ins is not None:
            if wrap_num < (ins.wrap + ZERO_INDEXED):
                proxy_row = (wrap_num * (ins.new_source_row_count + SPACER) +
                             source_row)
                if source_row >= ins.source_start:
                    proxy_row += ins.num_new_rows
            else:
                old_wraps = wrap_num - (ins.wrap + ZERO_INDEXED)
                proxy_row = ((ins.wrap + ZERO_INDEXED) *
                             (ins.new_source_row_count + SPACER) + old_wraps *
                             (ins.old_source_row_count + SPACER) + source_row)
        elif rem is not None:
            if wrap_num < (rem.wrap + ZERO_INDEXED):
                proxy_row = (wrap_num * (rem.new_source_row_count + SPACER) +
                             source_row)
                if source_row > rem.source_end:
                    proxy_row -= rem.num_rem_rows
                elif source_row >= rem.source_start:
                    return QtCore.QModelIndex(), False, None
            else:
                old_wraps = wrap_num - (rem.wrap + ZERO_INDEXED)
                proxy_row = ((rem.wrap + ZERO_INDEXED) *
                             (rem.new_source_row_count + SPACER) + old_wraps *
                             (rem.old_source_row_count + SPACER) + source_row)
        else:
            source_row_count = len(self._source_child_row_counts)
            proxy_row = wrap_num * (source_row_count + SPACER) + source_row
        proxy_index = self.index(proxy_row, proxy_col)
        return proxy_index, True, wrap_num
[docs]    def mapToSource(self, proxy_index, *, map_past_end_to_last_col=False):
        """
        Map an index from this proxy to the source model.
        :param proxy_index: The proxy index to map.
        :type proxy_index: QtCore.QModelIndex
        :param map_past_end_to_last_col: If False, indices in the last wrap that
            are past the end of the source model will be mapped to an invalid
            index. If True, these indices will be mapped to the last valid index
            in their row.
        :type map_past_end_to_last_col: bool
        """
        if not self._source_child_row_counts:
            # model is empty
            return QtCore.QModelIndex()
        (source_row, source_parent_row, wrap_num, is_spacer) = \
            
self._mapRowToSource(proxy_index.row(), proxy_index.internalId())
        if source_row is None:
            return QtCore.QModelIndex()
        if source_parent_row is None:
            source_parent = None
        else:
            source_parent = self._source_model.index(source_parent_row, 0)
        source_col = wrap_num * self._width + proxy_index.column()
        if source_col >= self._source_column_count:
            # We're in the final wrap and to the right of the last "real" column
            if map_past_end_to_last_col:
                source_col = self._source_column_count - 1
            else:
                return QtCore.QModelIndex()
        return self._source_model.index(source_row, source_col, source_parent) 
    def _mapRowToSource(self, proxy_row, proxy_internal_id):
        """
        Determine the source model row that corresponds to the specified proxy
        row.
        :param proxy_row: The proxy row to map.
        :type proxy_row: int
        :param proxy_internal_id: The parent row (or `TOP_LEVEL` for
            top-level rows) of the row to map.
        :type proxy_internal_id: int
        :return: A tuple of::
            - The source row, or None if the row doesn't exist in the source
            model (for spacer rows in between wraps or source rows that are
            in the process of being inserted or removed) (int or None)
            - The source parent row.  Will be None for top level rows or if the
            row doesn't exist in the source model. (int or None)
            - The wrap that the proxy index is in. Will be None if no source row
            exists. (int)
            - Whether this row is a spacer row in between wraps. True if the
            proxy row is a spacer row. False if the proxy row corresponds to a
            source row that's in the process of being inserted or removed. This
            value is only applicable when no equivalent source row exists and
            will be None otherwise. (bool or None)
        :rtype: tuple
        """
        if not self._source_child_row_counts:
            # model is empty
            return None, None, None, False
        elif proxy_internal_id == TOP_LEVEL:
            source_row, wrap_num, is_spacer = self._mapTopLevelRowToSource(
                proxy_row)
            return source_row, None, wrap_num, is_spacer
        else:
            source_parent_row, wrap_num, is_spacer = \
                
self._mapTopLevelRowToSource(proxy_internal_id)
            if source_parent_row is None:
                return (None, None, None, False)
            source_row = proxy_row
            ins = self._inserting_child_rows
            rem = self._removing_child_rows
            if (ins is not None and
                    source_parent_row == ins.source_parent_row and
                    wrap_num <= ins.wrap):
                if source_row > ins.source_end:
                    source_row -= ins.num_new_rows
                elif source_row >= ins.source_start:
                    return None, None, None, False
            elif (rem is not None and
                  source_parent_row == rem.source_parent_row and
                  wrap_num <= rem.wrap and source_row >= rem.source_start):
                source_row += rem.num_rem_rows
            return source_row, source_parent_row, wrap_num, is_spacer
    def _mapTopLevelRowToSource(self, proxy_row):
        """
        Determine the source model row that corresponds to the specified top-
        level proxy row.
        :param proxy_row: The proxy row to map
        :type proxy_row: int
        :return: A tuple of::
            - The source row, or None if the row doesn't exist in the source
            model (for spacer rows in between wraps or source rows that are
            in the process of being inserted or removed) (int or None)
            - The wrap that the proxy index is in. Will be None if no source row
            exists. (int)
            - None if a source row exists.  True if the proxy row is a spacer
            row in between wraps.  False if the proxy row corresponds to a
            source row that's in the process of being inserted or removed.
            (bool or None)
        :rtype: tuple
        """
        ins = self._inserting_rows
        rem = self._removing_rows
        if ins is not None:
            new_rows = (ins.new_source_row_count + SPACER) * (ins.wrap +
                                                              ZERO_INDEXED)
            if proxy_row <= new_rows:
                wrap_num = proxy_row // (ins.new_source_row_count + SPACER)
                source_row = proxy_row % (ins.new_source_row_count + SPACER)
                if source_row == ins.new_source_row_count:
                    return None, None, False
                elif source_row > ins.source_end:
                    source_row -= ins.num_new_rows
                elif source_row >= ins.source_start:
                    return None, None, False
            else:
                wrap_num = ((proxy_row - new_rows) //
                            (ins.old_source_row_count + SPACER) + ins.wrap +
                            ZERO_INDEXED)
                source_row = ((proxy_row - new_rows) %
                              (ins.old_source_row_count + SPACER))
                if source_row == ins.old_source_row_count:
                    return None, None, False
        elif rem is not None:
            new_rows = (rem.new_source_row_count + SPACER) * (rem.wrap +
                                                              ZERO_INDEXED)
            if proxy_row <= new_rows:
                wrap_num = proxy_row // (rem.new_source_row_count + SPACER)
                source_row = proxy_row % (rem.new_source_row_count + SPACER)
                if source_row == rem.new_source_row_count:
                    return None, None, False
                elif source_row >= rem.source_start:
                    source_row += rem.num_rem_rows
            else:
                wrap_num = ((proxy_row - new_rows) //
                            (rem.old_source_row_count + SPACER) + rem.wrap +
                            ZERO_INDEXED)
                source_row = ((proxy_row - new_rows) %
                              (rem.old_source_row_count + SPACER))
                if source_row == rem.old_source_row_count:
                    return None, None, False
        else:
            source_row_count = len(self._source_child_row_counts)
            wrap_num = proxy_row // (source_row_count + SPACER)
            source_row = proxy_row % (source_row_count + SPACER)
            if source_row == source_row_count:
                # This row is a spacer in between wraps
                return None, None, True
        return source_row, wrap_num, None
[docs]    def flags(self, index):
        """
        See Qt documentation for method documentation.  We override the default
        behavior here to provide valid flags for indices in the last wrap that
        are past the end of the source model.  This allows selection to work as
        expected when dragging past the end of the last column since it
        preserves the Qt.ItemIsSelectable flag.
        """
        source_index = self.mapToSource(index, map_past_end_to_last_col=True)
        return self.sourceModel().flags(source_index) 
[docs]    def data(self, index, role=Qt.DisplayRole, multiple_roles=None):
        """
        Returns the data stored under the given `role` for the item referred
        to by `index`. If `role` is `CustomRole.MultipleRoles`, then data
        is returned for all roles specified in `multiple_roles`. See Qt
        documentation for additional information.
        :param index: The index to fetch data for.
        :type  index: QModelIndex
        :param role: The role to fetch data for.
        :type  role: int
        :param multiple_roles: A list of roles to fetch data. Only applies if
            `role` is `CustomRole.MultipleRoles`.
        :type  multiple_roles: list(int) or NoneType
        :return: The data specified by `role` for `index`, or a dictionary
            mapping roles to their values if multiple roles are requested.
        :rtype: object or dict(int, object)
        """
        # Note that we must not call rowData here.  That will cause data methods
        # in AnnotationProxyModel to be called without the source_index
        # argument, and that argument is required for the drag-and-drop data
        # methods (_includeInDragImageData, _canDropAboveData, and
        # _canDropBelowData).
        if not index.isValid():
            return {} if role == CustomRole.MultipleRoles else None
        source_row, source_parent_row, wrap_num, is_spacer = \
            
self._mapRowToSource(index.row(), index.internalId())
        if is_spacer:
            if role == CustomRole.RowType:
                return RowType.Spacer
            elif role == CustomRole.MultipleRoles:
                return {CustomRole.RowType: RowType.Spacer}
            else:
                return None
        elif source_row is None:
            # the proxy_row corresponds to a source row that's in the process of
            # being added or removed
            return {} if role == CustomRole.MultipleRoles else None
        else:
            source_col = wrap_num * self._width + index.column()
            if source_col >= self._source_column_count:
                # the index is in the last wrap and past the end of the
                # alignment
                return {} if role == CustomRole.MultipleRoles else None
            if source_parent_row is None:
                source_parent = None
            else:
                source_parent = self._source_model.index(source_parent_row, 0)
            source_index = self._source_model.index(source_row, source_col,
                                                    source_parent)
            return self._source_model.data(source_index, role, multiple_roles) 
[docs]    def rowData(self, proxy_row, proxy_cols, proxy_internal_id, roles):
        """
        Fetch data for multiple roles for multiple indices in the same row. Note
        that this method does minimal sanity checking of its input for
        performance reasons, as it is called during painting.  The arguments are
        assumed to refer to valid indices.  Use `data` instead if more sanity
        checking is required.
        :param row: The row number to fetch data for.
        :type row: int
        :param proxy_cols: A list of columns to fetch data for.  Columns numbers
            must be sorted in ascending order.
        :type proxy_cols: list(int)
        :param proxy_internal_id: The parent row (or `TOP_LEVEL` for top-
            level rows) of the row to fetch data for.
        :type proxy_internal_id: int
        :param roles: A list of roles to fetch data for.
        :type roles: list(int)
        :return: {role: data} dictionaries for each requested column.  Note
            that the keys of these dictionaries may not match `roles`.  Data for
            additional roles may be included (e.g. if that data was required to
            calculate data for a requested role).  Data for requested roles may
            not be included if those roles are not applicable to the specified
            row (i.e. spacer rows do not provide data beyond row type and
            spacer type).
        :rtype: list(dict(int, object))
        """
        source_row, source_parent_row, wrap_num, is_spacer = \
            
self._mapRowToSource(proxy_row, proxy_internal_id)
        if is_spacer:
            if CustomRole.RowType in roles:
                return [{
                    CustomRole.RowType: RowType.Spacer
                } for _ in proxy_cols]
            else:
                return [{} for _ in proxy_cols]
        elif source_row is None:
            # the proxy_row corresponds to a source row that's in the process of
            # being added or removed
            return [{} for _ in proxy_cols]
        source_col_at_wrap_start = wrap_num * self._width
        source_cols = []
        for cur_proxy_col in proxy_cols:
            cur_source_col = source_col_at_wrap_start + cur_proxy_col
            if cur_source_col >= self._source_column_count:
                # the proxy_row is in the last wrap and columns were requested
                # that are past the end of the alignment.
                cols_truncated = True
                break
            source_cols.append(cur_source_col)
        else:
            cols_truncated = False
        if source_parent_row is None:
            source_internal_id = TOP_LEVEL
        else:
            source_internal_id = source_parent_row
        row_data = self._source_model.rowData(source_row, source_cols,
                                              source_internal_id, roles)
        if cols_truncated:
            num_missing_cols = len(proxy_cols) - len(source_cols)
            row_data.extend({} for _ in range(num_missing_cols))
        return row_data 
    def _sourceModelReset(self):
        """
        Respond to the modelChanged signal from the source model.
        """
        self._resetColumnCount()
        self._resetRowCounts()
        self.endResetModel()
    def _sourceLayoutChanged(self):
        """
        Respond to the layoutChanged signal from the source model.
        """
        self._resetRowCounts()
        self._invalidateAllPersistentIndices()
        self.layoutChanged.emit()
    def _sourceRowsAboutToBeInserted(self, parent, source_start, source_end):
        """
        Respond to the rowsAboutToBeInserted signal from the source model.  This
        method will completely insert (i.e. rowsAboutToBeInserted and
        rowsInserted) dummy rows into each wrap of this proxy.  After the source
        model finishes the insertion, the dummy rows will be updated
        (dataChanged and inserting child rows) in `_sourceRowsInserted`.  This
        allows us to avoid nesting the row insertions (i.e. emitting multiple
        rowsAboutToBeInserted signals followed by multiple rowsInserted
        signals), as nested signals are more complicated for higher proxies to
        handle and sometimes lead to errors from QSortFilterProxyModels.
        See `QAbstractItemModel.rowsAboutToBeInserted` signal documentation for
        argument documentation.
        """
        num_to_insert = source_end - source_start + 1
        assert num_to_insert > 0
        if not parent.isValid():
            old_source_row_count = len(self._source_child_row_counts)
            new_source_row_count = old_source_row_count + num_to_insert
            ins = RowWrapInsertingRows(old_source_row_count,
                                       new_source_row_count, source_start,
                                       source_end)
            if old_source_row_count == 0:
                # If the source model was empty then none of our wraps have
                # spacer rows yet, so we need to handle this insertion as a
                # special case.
                if source_start != 0:
                    # The source model is trying to insert new rows after
                    # existing rows, but there aren't any existing rows.
                    err = ("In RowWrapProxyModel, source model reports invalid "
                           "rows to be inserted.")
                    raise RuntimeError(err)
                new_proxy_row_count = self._wrap_count * (new_source_row_count +
                                                          SPACER) - SPACER
                self.beginInsertRows(QtCore.QModelIndex(), 0,
                                     new_proxy_row_count - 1)
                self._top_level_row_count = new_proxy_row_count
                ins.wrap += self._wrap_count
                self.endInsertRows()
            else:
                for wrap_num in range(self._wrap_count):
                    start = (wrap_num * (new_source_row_count + SPACER) +
                             source_start)
                    self.beginInsertRows(QtCore.QModelIndex(), start,
                                         start + num_to_insert - 1)
                    if wrap_num == 0:
                        self._inserting_rows = ins
                    ins.wrap += 1
                    self._top_level_row_count += num_to_insert
                    self.endInsertRows()
            self._inserting_rows = None
            self._updating_inserted_rows = ins
        else:
            source_parent_row = parent.row()
            source_row_count = len(self._source_child_row_counts)
            ins_child = RowWrapInsertingChildRows(source_parent_row,
                                                  source_start, source_end)
            for wrap_num in range(self._wrap_count):
                proxy_parent_row = (wrap_num * (source_row_count + SPACER) +
                                    source_parent_row)
                proxy_parent_index = self.index(proxy_parent_row, 0)
                self.beginInsertRows(proxy_parent_index, source_start,
                                     source_end)
                if wrap_num == 0:
                    self._inserting_child_rows = ins_child
                    self._source_child_row_counts[source_parent_row] += \
                        
num_to_insert
                ins_child.wrap += 1
                self.endInsertRows()
    def _sourceRowsInserted(self, parent, source_start, source_end):
        """
        Respond to the rowsInserted signal from the source model.  Note that new
        rows were already inserted in `_sourceRowsAboutToBeInserted`, so this
        method updates the new rows by emitting `dataChanged` and inserting any
        child rows.
        See `QAbstractItemModel.rowsInserted` signal documentation for argument
        documentation.
        """
        num_to_insert = source_end - source_start + 1
        assert num_to_insert > 0
        last_col = self.columnCount() - 1
        if not parent.isValid():
            ins = self._updating_inserted_rows
            if ins is None:
                err = ("In RowWrapProxyModel, got rowsInserted for top-level "
                       "rows without first receiving rowsAboutToBeInserted for "
                       "top-level rows.")
                raise RuntimeError(err)
            source_model = self.sourceModel()
            rows_per_wrap = source_model.rowCount() + SPACER
            row_counts = []
            for cur_source_row in range(source_start, source_end + 1):
                index = source_model.index(cur_source_row, 0)
                cur_row_count = source_model.rowCount(index)
                row_counts.append(cur_row_count)
            self._source_child_row_counts[source_start:source_start] = \
                
row_counts
            for wrap_num in range(self._wrap_count):
                top_proxy_row = wrap_num * rows_per_wrap + source_start
                bottom_proxy_row = wrap_num * rows_per_wrap + source_end
                proxy_row_nums = range(top_proxy_row, bottom_proxy_row + 1)
                for cur_proxy_row, cur_row_count in zip(proxy_row_nums,
                                                        row_counts):
                    if not cur_row_count:
                        continue
                    index = self.index(cur_proxy_row, 0)
                    self.beginInsertRows(index, 0, cur_row_count - 1)
                    ins.update_row_num = cur_proxy_row
                    self.endInsertRows()
                top_left = self.index(top_proxy_row, 0)
                bottom_right = self.index(bottom_proxy_row, last_col)
                self.dataChanged.emit(top_left, bottom_right)
            self._updating_inserted_rows = None
        else:
            if self._inserting_child_rows is None:
                err = ("In RowWrapProxyModel, got rowsInserted for child rows "
                       "without first receiving rowsAboutToBeInserted for "
                       "child rows.")
                raise RuntimeError(err)
            self._inserting_child_rows = None
            source_parent_row = parent.row()
            rows_per_wrap = len(self._source_child_row_counts) + 1
            for wrap_num in range(self._wrap_count):
                proxy_parent_row = wrap_num * rows_per_wrap + source_parent_row
                proxy_parent_index = self.index(proxy_parent_row, 0)
                top_left = self.index(source_start, 0, proxy_parent_index)
                bottom_right = self.index(source_end, last_col,
                                          proxy_parent_index)
                self.dataChanged.emit(top_left, bottom_right)
    def _sourceRowsAboutToBeRemoved(self, parent, source_start, source_end):
        """
        Respond to the rowsAboutToBeRemoved signal from the source model.  Note
        that this method completely removes (i.e. rowsAboutToBeRemoved and
        rowsRemoved) rows from all wraps of this proxy.
        See `QAbstractItemModel.rowsAboutToBeRemoved` signal documentation for
        argument documentation.
        """
        num_to_remove = source_end - source_start + 1
        assert num_to_remove > 0
        if not parent.isValid():
            old_source_row_count = len(self._source_child_row_counts)
            new_source_row_count = old_source_row_count - num_to_remove
            rem = RowWrapRemovingRows(old_source_row_count,
                                      new_source_row_count, source_start,
                                      source_end)
            for wrap_num in range(self._wrap_count):
                start = (wrap_num * (new_source_row_count + SPACER) +
                         source_start)
                self.beginRemoveRows(QtCore.QModelIndex(), start,
                                     start + num_to_remove - 1)
                if wrap_num == 0:
                    self._removing_rows = rem
                rem.wrap += 1
                self._top_level_row_count -= num_to_remove
                self.endRemoveRows()
        else:
            source_parent_row = parent.row()
            rows_per_wrap = len(self._source_child_row_counts) + SPACER
            rem_child = RowWrapRemovingChildRows(source_parent_row,
                                                 source_start, source_end)
            for wrap_num in range(self._wrap_count):
                proxy_parent_row = wrap_num * rows_per_wrap + source_parent_row
                proxy_parent_index = self.index(proxy_parent_row, 0)
                self.beginRemoveRows(proxy_parent_index, source_start,
                                     source_end)
                if wrap_num == 0:
                    self._removing_child_rows = rem_child
                    self._source_child_row_counts[source_parent_row] -= \
                        
num_to_remove
                self._removing_child_rows.wrap += 1
                self.endRemoveRows()
    def _sourceRowsRemoved(self, parent, source_start, source_end):
        """
        Respond to the rowsRemoved signal from the source model.  Note that the
        rows were already removed in `_sourceRowsAboutToBeRemoved`, so this
        method just clears the row removal bookkeeping.
        See `QAbstractItemModel.rowsRemoved` signal documentation for argument
        documentation.
        """
        num_to_remove = source_end - source_start + 1
        assert num_to_remove > 0
        if not parent.isValid():
            if self._removing_rows is None:
                err = ("In RowWrapProxyModel, got rowsRemoved for top-level "
                       "rows without first receiving rowsAboutToBeRemoved for "
                       "top-level rows.")
                raise RuntimeError(err)
            rem = self._removing_rows
            if rem.source_start != source_start or rem.source_end != source_end:
                err = ("In RowWrapProxyModel, row numbers for top-level "
                       "rowsRemoved signal do not match row numbers from "
                       "rowsAboutToBeRemoved signal.")
                raise RuntimeError(err)
            self._removing_rows = None
            self._source_child_row_counts[source_start:source_end + 1] = []
        else:
            if self._removing_child_rows is None:
                err = ("In RowWrapProxyModel, got rowsRemoved for child rows "
                       "without first receiving rowsAboutToBeRemoved for "
                       "child rows.")
                raise RuntimeError(err)
            self._removing_child_rows = None
    def _sourceColumnsAboutToBeInserted(self, parent, start, end):
        """
        Respond to the columnsAboutToBeInserted signal from the source model.
        See `QAbstractItemModel.columnsAboutToBeInserted` signal documentation
        for argument documentation.
        """
        if parent.isValid():
            # We assume that all groups have the same number of columns, so
            # ignore signals for everything but the top level group
            return
        old_source_cols = self._source_column_count
        source_cols_to_insert = end - start + 1
        new_source_cols = old_source_cols + source_cols_to_insert
        old_wrap_count = self._wrapCount(old_source_cols)
        new_wrap_count = self._wrapCount(new_source_cols)
        wraps_added = new_wrap_count - old_wrap_count
        if old_wrap_count == 1 and old_source_cols < self._width:
            new_proxy_cols = min(new_source_cols, self._width)
            self.beginInsertColumns(QtCore.QModelIndex(), self._column_count,
                                    new_proxy_cols - ZERO_INDEXED)
            self._column_count = new_proxy_cols
            self.endInsertColumns()
        if wraps_added:
            if self._top_level_row_count:
                source_row_count = len(self._source_child_row_counts)
                new_row_count = (self._top_level_row_count + wraps_added *
                                 (source_row_count + SPACER))
                self.beginInsertRows(QtCore.QModelIndex(),
                                     self._top_level_row_count,
                                     new_row_count - ZERO_INDEXED)
                self._top_level_row_count = new_row_count
                self._wrap_count = new_wrap_count
                self.endInsertRows()
            else:
                self._wrap_count = new_wrap_count
    def _sourceColumnsAboutToBeRemoved(self, parent, start, end):
        """
        Respond to the columnsAboutToBeRemoved signal from the source model.
        See `QAbstractItemModel.columnsAboutToBeRemoved` signal documentation
        for argument documentation.
        """
        if parent.isValid():
            # We assume that all groups have the same number of columns, so
            # ignore signals for everything but the top level group
            return
        old_source_cols = self._source_column_count
        source_cols_to_remove = end - start + 1
        new_source_cols = old_source_cols - source_cols_to_remove
        old_wrap_count = self._wrapCount(old_source_cols)
        new_wrap_count = self._wrapCount(new_source_cols)
        wraps_removed = old_wrap_count - new_wrap_count
        if wraps_removed:
            if self._top_level_row_count:
                source_row_count = len(self._source_child_row_counts)
                new_row_count = (self._top_level_row_count - wraps_removed *
                                 (source_row_count + SPACER))
                self.beginRemoveRows(QtCore.QModelIndex(), new_row_count,
                                     self._top_level_row_count - ZERO_INDEXED)
                self._top_level_row_count = new_row_count
                self._wrap_count = new_wrap_count
                self.endRemoveRows()
            else:
                self._wrap_count = new_wrap_count
        if new_wrap_count == 1 and new_source_cols < self._width:
            self.beginRemoveColumns(QtCore.QModelIndex(), new_source_cols,
                                    self._column_count - ZERO_INDEXED)
            self._column_count = new_source_cols
            self.endRemoveColumns()
    def _sourceColumnsInsertedOrRemoved(self, parent, start, end):
        if parent.isValid():
            # We assume that all groups have the same number of columns, so
            # ignore signals for everything but the top level group
            return
        self._source_column_count = self.sourceModel().columnCount()
        # If we wanted to emit dataChanged for all affected cells, we'd have to
        # iterate through all rows with children and emit a separate dataChanged
        # signal for each group.  Doing that will almost certainly take longer
        # than just repainting the entire visible area.
        invalid_index = QtCore.QModelIndex()
        self.dataChanged.emit(invalid_index, invalid_index)
    def _sourceDataChanged(self, source_topleft, source_bottomright):
        """
        Respond to a dataChanged signal from the source model by emitting
        dataChanged for all affected cells. See
        `QAbstractItemModel.dataChanged` signal documentation for argument
        documentation.
        """
        if not source_topleft.isValid() and not source_bottomright.isValid():
            self.dataChanged.emit(source_topleft, source_bottomright)
        source_parent = source_topleft.parent()
        source_row_start = source_topleft.row()
        source_row_end = source_bottomright.row()
        num_source_rows = len(self._source_child_row_counts)
        last_col = self._width - ZERO_INDEXED
        if not source_parent.isValid():
            proxy_topleft, is_topleft_valid, top_wrap = \
                
self._mapTopLevelIndexFromSource(source_topleft)
            proxy_bottomright, is_bottomright_valid, bottom_wrap = \
                
self._mapTopLevelIndexFromSource(source_bottomright)
            if not is_topleft_valid or not is_bottomright_valid:
                err = ("Invalid proxy index in %s._modelDataChanged" %
                       self.__class__.__name__)
                raise RuntimeError(err)
            left_col = proxy_topleft.column()
            right_col = proxy_bottomright.column()
            for cur_wrap in range(top_wrap, bottom_wrap + 1):
                cur_left_col = left_col if cur_wrap == top_wrap else 0
                cur_right_col = (right_col
                                 if cur_wrap == bottom_wrap else last_col)
                cur_wrap_start = cur_wrap * (num_source_rows + SPACER)
                cur_top_row = cur_wrap_start + source_row_start
                cur_bottom_row = cur_wrap_start + source_row_end
                cur_topleft = self.index(cur_top_row, cur_left_col)
                cur_bottomright = self.index(cur_bottom_row, cur_right_col)
                self.dataChanged.emit(cur_topleft, cur_bottomright)
        else:
            proxy_topleft = self.mapFromSource(source_topleft)
            proxy_bottomright = self.mapFromSource(source_bottomright)
            if not (proxy_topleft.isValid() and proxy_bottomright.isValid()):
                err = ("Invalid proxy index in %s._modelDataChanged" %
                       self.__class__.__name__)
                raise RuntimeError(err)
            proxy_top_parent = proxy_topleft.parent()
            proxy_bottom_parent = proxy_bottomright.parent()
            top_parent_row = proxy_top_parent.row()
            bottom_parent_row = proxy_bottom_parent.row()
            top_wrap = top_parent_row // (num_source_rows + 1)
            bottom_wrap = bottom_parent_row // (num_source_rows + 1)
            left_col = proxy_topleft.column()
            right_col = proxy_bottomright.column()
            source_parent_row = source_parent.row()
            for cur_wrap in range(top_wrap, bottom_wrap + 1):
                cur_left_col = left_col if cur_wrap == top_wrap else 0
                cur_right_col = (right_col
                                 if cur_wrap == bottom_wrap else last_col)
                cur_parent_row = (cur_wrap * (num_source_rows + SPACER) +
                                  source_parent_row)
                cur_parent_index = self.index(cur_parent_row, 0)
                cur_topleft = self.index(source_row_start, cur_left_col,
                                         cur_parent_index)
                cur_bottomright = self.index(source_row_end, cur_right_col,
                                             cur_parent_index)
                self.dataChanged.emit(cur_topleft, cur_bottomright)
    def _seqExpansionChanged(self, source_indices, expanded):
        # See SeqExpansionProxyMixin._seqExpansionChanged for method
        # documentation
        proxy_indices = []
        rows_per_wrap = len(self._source_child_row_counts) + SPACER
        for source_index in source_indices:
            source_row = source_index.row()
            for cur_wrap in range(self._wrap_count):
                proxy_row = cur_wrap * rows_per_wrap + source_row
                proxy_index = self.index(proxy_row, 0)
                proxy_indices.append(proxy_index)
        self.seqExpansionChanged.emit(proxy_indices, expanded)
    @QtCore.pyqtSlot(int, int)
    def _sourceFixedColumnDataChanged(self, role, source_row):
        # See method documentation in ProxyMixin
        rows_per_wrap = len(self._source_child_row_counts) + SPACER
        for cur_wrap in range(self._wrap_count):
            proxy_row = cur_wrap * rows_per_wrap + source_row
            self.fixedColumnDataChanged.emit(role, proxy_row)
[docs]    def rowWrapEnabled(self):
        """
        :return: Whether this model provides row-wrapped data.
        :type: bool
        .. warning: You probably don't want to use this method. Whenever
            possible, you should pass information to the model and have it
            respond as appropriate (i.e. tell, don't ask). This method should
            only be used for view features that function differently depending
            on whether the model is row-wrapped or not (e.g. drag-and-drop
            auto-scrolling).
        """
        return True 
[docs]    def setResRangeSelectionState(self,
                                  from_index,
                                  to_index,
                                  selected,
                                  columns,
                                  current=False):
        # See SequenceAlignmentModel.setResRangeSelectionState for method
        # documentation.
        from_source_index = self.mapToSource(from_index,
                                             map_past_end_to_last_col=True)
        to_source_index = self.mapToSource(to_index,
                                           map_past_end_to_last_col=True)
        self.sourceModel().setResRangeSelectionState(from_source_index,
                                                     to_source_index, selected,
                                                     columns, current)  
[docs]class ExportProxyModel(PostAnnotationProxyMixin, QtCore.QIdentityProxyModel):
    """
    A proxy for use when generating a static image of the table.
    """
[docs]    def tableWidthChanged(self, *args, **kwargs):
        """
        Ignore changes in the table size rather than trying to update row
        wrapping.
        """ 
        # This method intentionally left blank
[docs]    def rowData(self, proxy_row, proxy_cols, proxy_internal_id, roles):
        """
        See `AnnotationProxyModel.rowData` for method documentation.
        """
        # This method assumes that QIdentityProxyModel uses the same internal
        # ids as the source model.  That's currently the case, but internal ids
        # are considered implementation details so it's possible (although
        # highly unlikely) that internal ids would change.  If that happens,
        # this method would need to remap the internal id using
        # self.mapToSource.
        return self.sourceModel().rowData(proxy_row, proxy_cols,
                                          proxy_internal_id, roles)  
[docs]class ExportLogoProxyModel(PostAnnotationProxyMixin,
                           QtCore.QSortFilterProxyModel):
    """
    Sort proxy model which only accepts the ruler, spacer, and sequence logo
    rows. Used when exporting an image of just the sequence logo.
    """
[docs]    def filterAcceptsRow(self, source_row, parent_index):
        source_index = self.sourceModel().index(source_row, 0)
        row_type = self.sourceModel().data(source_index, CustomRole.RowType)
        return row_type in (RowType.Spacer, ALN_ANNO_TYPES.sequence_logo,
                            ALN_ANNO_TYPES.indices) 
[docs]    def tableWidthChanged(self, *args, **kwargs):
        """
        Ignore changes in the table size rather than trying to update row
        wrapping.
        """ 
        # This method intentionally left blank
[docs]    def rowData(self, proxy_index, proxy_cols, roles):
        """
        This method differs from typical rowData methods in that it takes a
        proxy index instead of a row number, because it must first map that
        index to source.
        This is because the QSortFilterProxyModel uses a different
        indexing/internal id system than other proxy models in MSV.
        """
        source_index = self.mapToSource(proxy_index)
        return self.sourceModel().rowData(source_index.row(), proxy_cols,
                                          source_index.internalId(), roles)  
[docs]class ViewModel(QtCore.QObject):
    """
    An abstraction layer for the various table models in this module
    :cvar orderChanged: A signal emitted when the sorting or row reodering
        (due to drag-and-drop) has changed.  Emitted with the new seq indices.
    :vartype orderChanged: `QtCore.pyqtSignal`
    :cvar topModelChanged: A signal emitted when row wrapping is toggled.  The
        view must change models whenever this signal is emitted.  Emitted with
        the new model that the view should use.
    :vartype topModelChanged: `QtCore.pyqtSignal`
    """
    orderChanged = QtCore.pyqtSignal(list)
    topModelChanged = QtCore.pyqtSignal(QtCore.QAbstractItemModel)
[docs]    def __init__(self, parent):
        super().__init__(parent)
        self.undo_stack = None
        self._base_model = SequenceAlignmentModel(parent)
        self._base_model.sequencesReordered.connect(self.orderChanged)
        self._base_model.hiddenSeqsChanged.connect(self._setFilter)
        self.setAlignment = self._base_model.setAlignment
        self.getAlignment = self._base_model.getAlignment
        self.getResidueDisplayMode = self._base_model.getResidueDisplayMode
        self.updateColorScheme = self._base_model.updateColorScheme
        self.getSeqColorScheme = self._base_model.getSeqColorScheme
        self.updateResidueColors = self._base_model.updateResidueColors
        self.getResidueColors = self._base_model.getResidueColors
        self.sequenceCount = self._base_model.sequenceCount
        self._filter_proxy = SequenceFilterProxyModel(parent)
        self._filter_proxy.setSourceModel(self._base_model)
        self._annotation_proxy = AnnotationProxyModel(parent)
        # `AnnotationProxyModel.setSourceModel` does bookkeeping that needs to
        # know about options so we set a dummy options model so it can run. The
        # real options model is set later and will update the bookkeeping
        self._annotation_proxy.setOptionsModel(gui_models.OptionsModel())
        self._annotation_proxy.setSourceModel(self._base_model)
        ann_proxy = self._annotation_proxy
        self.getGroupBy = ann_proxy.getGroupBy
        self._setShownRowTypes = ann_proxy._setShownRowTypes
        self.getShownRowTypes = ann_proxy.getShownRowTypes
        self._setVisibilityForRowType = ann_proxy._setVisibilityForRowType
        self._setVisibilityForRowTypes = ann_proxy._setVisibilityForRowTypes
        self._wrap_proxy = RowWrapProxyModel(parent)
        self._wrap_proxy.setSourceModel(self._annotation_proxy)
        self._wrap_enabled = False
        self.top_model = self._annotation_proxy
        self.export_model = ExportProxyModel(parent)
        self.export_model.setSourceModel(self.top_model)
        self.info_model = AlignmentInfoProxyModel(parent)
        self.info_model.setSourceModel(self.top_model)
        self.orderChanged.connect(self.info_model.orderChanged)
        self.metrics_model = AlignmentMetricsProxyModel(parent)
        self.metrics_model.setSourceModel(self.top_model) 
[docs]    def setStructureModel(self, smodel):
        self._base_model.setStructureModel(smodel) 
[docs]    def setLightMode(self, enabled):
        """
        Set light mode on the info and metrics tables and appropriately change
        the color scheme for some annotations
        """
        self._base_model.setLightMode(enabled)
        self.info_model.setLightMode(enabled)
        self.metrics_model.setLightMode(enabled)
        if enabled:
            scheme = color.LightModeTextScheme()
        else:
            scheme = color.DarkModeTextScheme()
        for ann_type in color.TEXT_ANN_TYPES:
            self.updateColorScheme(ann_type, scheme) 
    def _setFilter(self, enabled):
        """
        Enable or disable sequence filtering.
        :param enabled: Whether sequences should be filtered.
        :type enabled: bool
        """
        if enabled:
            annotation_source = self._filter_proxy
        else:
            annotation_source = self._base_model
        self._filter_proxy.setActive(enabled)
        self._annotation_proxy.setSourceModel(annotation_source)
[docs]    def setRowWrap(self, enabled):
        """
        Enable or disable row wrapping.
        :param enabled: Whether rows should be wrapped.
        :type enabled: bool
        """
        if enabled == self._wrap_enabled:
            return
        if enabled:
            self.top_model = self._wrap_proxy
        else:
            self.top_model = self._annotation_proxy
        self.topModelChanged.emit(self.top_model)
        self.export_model.setSourceModel(self.top_model)
        self.info_model.setSourceModel(self.top_model)
        self.metrics_model.setSourceModel(self.top_model)
        self._wrap_enabled = enabled 
[docs]    def rowWrap(self):
        """
        Return the current row wrapping setting.
        :return: True if the rows are wrapped.  False otherwise.
        :rtype: bool
        """
        return self._wrap_enabled 
[docs]    def setPageModel(self, page_model):
        """
        Set the page model, which contains the options model and is also
        responsible for switching between split-chain and combined-chain
        alignments.
        :param page_model: The page model to set
        :type page_model: gui_models.PageModel
        """
        self._base_model.setPageModel(page_model)
        self.metrics_model.setOptionsModel(page_model.options)
        self._annotation_proxy.setOptionsModel(page_model.options)  
[docs]class FixedColumn(table_helper.Column):
    """
    A table column that is fixed on the left or right of the scrollable
    columns.  This object is intended to be used in the
    `table_helper.TableColumns` enum.
    """
[docs]    def __init__(self,
                 title,
                 tooltip=None,
                 role=None,
                 percent=False,
                 all_rows=False,
                 align=Qt.AlignCenter,
                 selectable=False):
        """
        :param title: The title to display in the column header.
        :type title: basestring
        :param tooltip: The tooltip to display when the user hovers over the
            column header.
        :type tooltip: str
        :param role: The role that the column should display data from.
            May be None if no data is to be displayed in the column (e.g. the
            column that contains only group expansion arrows).
        :type role: int or NoneType
        :param percent: Whether to format a value as a percentage (without
                        '%' sign)
        :type percent: bool
        :param all_rows: True if the column contains data for all rows.  False
            if the column only contains data for sequence rows.
        :type all_rows: bool
        :param align: The alignment for cells in the column.  Note that only the
            horizontal component of the alignment is obeyed; cells are always
            vertically aligned to center.
        :type align: int
        :param selectable: Whether this column should be flagged as selectable
            (i.e. Qt.ItemIsSelectable).
        :type selectable: bool
        """
        self.data = {
            "title": title,
            "tooltip": tooltip,
            "role": role,
            "percent": percent,
            "all_rows": all_rows,
            "align": align,
            "selectable": selectable
        }
        self._count = self.__class__._count
        self.__class__._count += 1  
[docs]class SequencePropertyColumn(FixedColumn):
[docs]    def __init__(self, seq_prop, role, *args, **kwargs):
        super().__init__(*args, role=role, **kwargs)
        # During creation of a TableColumns class, the items of `data` get
        # converted to instance attributes.  Since instances of
        # SequencePropertyColumn aren't added to AlignmentMetricsColumns,
        # we manually do the conversion here.
        for key, val in self.data.items():
            setattr(self, key, val)
        self._seq_prop = seq_prop 
[docs]    def getSeqProp(self):
        return self._seq_prop  
[docs]class BaseAdjacentAlignmentProxyModel(
        table_speed_up.MultipleRolesRoleProxyMixin, SeqExpansionProxyMixin,
        GroupByProxyMixin, GetAlignmentProxyMixin, NestedProxy):
    """
    A base proxy model to be subclassed by other proxy models that show data
    related to and synchronized with an alignment but in separate, adjacent
    views.
    :cvar ROLES_FROM_SOURCE: Set of data roles to query from the
        source model.
    :vartype ROLES_FROM_SOURCE: set
    """
    Column = None
    FONT_SCALE = 1.25
    textSizeChanged = QtCore.pyqtSignal()
    rowHeightChanged = QtCore.pyqtSignal()
    ROLES_FROM_SOURCE = {
        CustomRole.RowType, CustomRole.ReferenceSequence, CustomRole.Seq,
        CustomRole.RowHeightScale, CustomRole.SeqSelected
    }
    ROLES_FROM_PROXY = {
        Qt.DisplayRole, Qt.TextAlignmentRole, Qt.FontRole, Qt.ToolTipRole
    }
[docs]    def __init__(self, parent=None):
        super(BaseAdjacentAlignmentProxyModel, self).__init__(parent)
        self.ROLE_TO_COL = {col.role: i for i, col in enumerate(self.Column)}
        self.DEFINED_ROLES = (self.ROLES_FROM_SOURCE | self.ROLES_FROM_PROXY |
                              {CustomRole.MultipleRoles})
        self._regular_font = QtGui.QFont()
        font_size = int(self._regular_font.pointSize() * self.FONT_SCALE)
        self._regular_font.setPointSize(font_size)
        # Using self._cur_columns instead of self.Column allows the same code to
        # work with both AlignmentInfoProxyModel and AlignmentMetricsProxyModel
        self._cur_columns = [col for col in self.Column]
        self._selectable_cols = [
            i for i, col in enumerate(self.Column) if col.selectable
        ] 
[docs]    @table_helper.model_reset_method
    def setSourceModel(self, model):
        # See Qt documentation for method documentation.
        signal_map = {
            "modelAboutToBeReset": self.beginResetModel,
            "modelReset": self.endResetModel,
            "layoutAboutToBeChanged": self.layoutAboutToBeChanged,
            "layoutChanged": self.endLayoutChange,
            "rowsAboutToBeInserted": self._sourceRowsAboutToBeInserted,
            "rowsInserted": self.endInsertRows,
            "rowsAboutToBeRemoved": self._sourceRowsAboutToBeRemoved,
            "rowsRemoved": self.endRemoveRows,
            "textSizeChanged": self._textSizeChanged,
            "fixedColumnDataChanged": self.updateData,
            "rowHeightChanged": self._rowHeightChanged,
            "dataChanged": self._sourceDataChanged
        }
        old_model = self.sourceModel()
        if old_model is not None:
            table_helper.disconnect_signals(old_model, signal_map)
        table_helper.connect_signals(model, signal_map)
        super(BaseAdjacentAlignmentProxyModel, self).setSourceModel(model) 
[docs]    def rowCount(self, parent=None):
        # See Qt documentation for method documentation
        if parent is None or not parent.isValid():
            return self.sourceModel().rowCount()
        else:
            source_parent = self.mapToSource(parent)
            return self.sourceModel().rowCount(source_parent) 
[docs]    def mapToSource(self, proxy_index):
        # See Qt documentation for method documentation
        if not proxy_index.isValid():
            return QtCore.QModelIndex()
        source_model = self.sourceModel()
        internal_id = proxy_index.internalId()
        if internal_id == TOP_LEVEL:
            source_parent = QtCore.QModelIndex()
        else:
            source_parent = source_model.index(internal_id, 0)
        return source_model.index(proxy_index.row(), 0, source_parent) 
[docs]    def mapFromSource(self, source_index):
        # See Qt documentation for method documentation
        if not source_index.isValid() or not self._cur_columns:
            return QtCore.QModelIndex()
        source_model = self.sourceModel()
        source_parent = source_model.parent(source_index)
        internal_id = source_parent.row()
        if internal_id == -1:
            internal_id = TOP_LEVEL
        return self.createIndex(source_index.row(), 0, internal_id) 
[docs]    def data(self, proxy_index, role=Qt.DisplayRole, multiple_roles=None):
        # See table_speed_up.MultipleRolesRoleModelMixin.data for method
        # documentation
        if role not in self.DEFINED_ROLES:
            return None
        source_index = self.mapToSource(proxy_index)
        source_model = self.sourceModel()
        if role not in self._data_methods:
            return source_model.data(source_index, role)
        col_num = proxy_index.column()
        col_obj = self._cur_columns[col_num]
        is_title_row = (proxy_index.row() == 0 and
                        proxy_index.internalId() == TOP_LEVEL)
        if role == CustomRole.MultipleRoles:
            data_args = (col_obj, is_title_row, proxy_index, source_index,
                         source_model, multiple_roles)
        else:
            data_args = (col_obj, is_title_row, proxy_index, source_index,
                         source_model, None, role)
        return self._callDataMethod(role, data_args) 
    @table_helper.data_method(Qt.TextAlignmentRole)
    def _alignmentData(self, col):
        return col.align
    @table_helper.data_method(Qt.FontRole)
    def _fontData(self):
        return self._regular_font
[docs]    def rowData(self, proxy_row, cols, internal_id, roles):
        """
        Fetch data for multiple roles for multiple indices in the same row. Note
        that this method does minimal sanity checking of its input for
        performance reasons, as it is called during painting.  The arguments are
        assumed to refer to valid indices.  Use `data` instead if more sanity
        checking is required.
        :param row: The row number to fetch data for.
        :type row: int
        :param cols: A list of columns to fetch data for.
        :type cols: list(int)
        :param internal_id: The parent row (or `TOP_LEVEL` for top-level
            rows) of the row to fetch data for.
        :type internal_id: int
        :param roles: A list of roles to fetch data for.
        :type roles: list(int)
        :return: {role: data} dictionaries for each requested column.  Note
            that the keys of these dictionaries may not match `roles`.  Data for
            additional roles may be included (e.g. if that data was required to
            calculate data for a requested role).  Data for requested roles may
            not be included if those roles are not applicable to the specified
            row (e.g. many roles are not applicable to spacer rows).
        :rtype: list(dict(int, object))
        """
        is_title_row = internal_id == TOP_LEVEL and proxy_row == 0
        roles = set(roles)
        cols = [self._cur_columns[i] for i in cols]
        # We pack the requested tuple (and unpack it below) since
        # AlignmentInfoProxyModel reimplements _mapRolesToSource and
        # _fetchProxyRowData with different requested roles.  This way,
        # AlignmentInfoProxyModel doesn't have to reimplement this method.
        (roles, proxy_roles,
         *requested) = self._mapRolesToSource(is_title_row, cols, roles)
        source_data = self.sourceModel().rowData(proxy_row, [0], internal_id,
                                                 roles)[0]
        roles |= proxy_roles
        return self._fetchProxyRowData(roles, source_data, cols, is_title_row,
                                       *requested) 
    def _mapRolesToSource(self, is_title_row, cols, roles):
        """
        Given a list of roles and columns to fetch data for, figure out what
        roles we need to request from the source model and what data can be
        calculated in this proxy.
        :param is_title_row: Whether this row is the title row (i.e. the first
            row of the view).
        :type is_title_row: bool
        :param cols: A list of proxy column to fetch data for.
        :type cols: iterable(FixedColumn)
        :param roles: A set of roles to fetch data for.
        :type roles: set(int)
        :return: A tuple of:
          - Roles to fetch from the source model
          - Roles of data that can be calculated in this proxy
          - Whether Qt.DisplayRole data was requested
        :rtype: tuple(set(int), set(int), bool)
        """
        display_data_requested = Qt.DisplayRole in roles
        roles.discard(Qt.DisplayRole)
        proxy_roles = roles & self.ROLES_FROM_PROXY
        roles &= self.ROLES_FROM_SOURCE
        if display_data_requested and not is_title_row:
            roles.add(CustomRole.RowType)
            for cur_col in cols:
                if cur_col.role is not None:
                    roles.add(cur_col.role)
        return roles, proxy_roles, display_data_requested
    def _fetchProxyRowData(self, roles, source_data, cols, is_title_row,
                           display_data_requested):
        """
        After data has been retrieved from the source model, determine data for
        all requested roles for all requested columns.
        :param roles: The roles to fetch data for.
        :type roles: set(int)
        :param source_data: Data for this row that has been retrieved from the
            source model.
        :type source_data: dict(int, object)
        :param cols: The columns to fetch data for.
        :type cols: list(int)
        :param is_title_row: Whether this row is the title row (i.e. the first
            row of the view).
        :type is_title_row: bool
        :param display_data_requested: Whether Qt.DisplayRole data was
            requested.  This role will not be in `roles` since
            `self._displayData` calls `self._multipleRolesData`, which calls
            this method, so fetching this value through the normal mechanisms
            would lead to an infinite loop.
        :type display_data_requested: bool
        """
        row_data = []
        for cur_col in cols:
            cell_data = source_data.copy()
            if roles:
                self._fetchMultipleRoles(cell_data, roles, cur_col,
                                         is_title_row)
            if display_data_requested:
                self._displayDataFromSourceData(cell_data, cur_col,
                                                is_title_row)
            row_data.append(cell_data)
        return row_data
    def _displayDataFromSourceData(self, data, col, is_title_row):
        """
        Determine the Qt.DisplayRole data for the specified column.  The
        requested data will be added to the `data` dictionary.
        :param data: Data from the source model for this row in the form of
            {role: value}.
        :type data: dict(int, object)
        :param col: The column to fetch display data for.
        :type col: FixedColumn
        :param is_title_row: Whether this row is the title row (i.e. the first
            row of the view).
        :type is_title_row: bool
        """
        if is_title_row:
            data[Qt.DisplayRole] = col.title
        elif (col.role is None or
              (not col.all_rows and
               data.get(CustomRole.RowType) is not RowType.Sequence)):
            data[Qt.DisplayRole] = None
        else:
            display_data = data.get(col.role)
            if display_data is None:
                data[Qt.DisplayRole] = display_data
            elif not col.percent:
                data[Qt.DisplayRole] = str(display_data)
            else:
                data[Qt.DisplayRole] = str(round(display_data * 100))
    @table_helper.data_method(CustomRole.MultipleRoles)
    def _multipleRolesData(self, col, is_title_row, proxy_index, source_index,
                           source_model, multiple_roles):
        multiple_roles = set(multiple_roles)
        multiple_roles, proxy_roles, *requested = \
            
self._mapRolesToSource(is_title_row, [col], multiple_roles)
        if multiple_roles:
            data = source_model.data(source_index, CustomRole.MultipleRoles,
                                     multiple_roles)
        else:
            data = {}
        multiple_roles |= proxy_roles
        if multiple_roles:
            self._fetchMultipleRoles(data, multiple_roles, col, is_title_row)
        return self._fetchProxyRowData(multiple_roles, data, [col],
                                       is_title_row, *requested)[0]
    @table_helper.data_method(Qt.DisplayRole)
    def _displayData(self, col, is_title_row, proxy_index, source_index,
                     source_model):
        data = self._multipleRolesData(col, is_title_row, proxy_index,
                                       source_index, source_model,
                                       {Qt.DisplayRole})
        return data[Qt.DisplayRole]
    @table_helper.data_method(Qt.ToolTipRole)
    def _toolTipData(self, col, is_title_row, proxy_index, source_index,
                     source_model):
        """
        Returns the column tooltip (if set) for the first row.
        :param col: Column to get tooltip for
        :type col: int
        :param is_title_row: Whether this is the title row or not
        :type is_title_row: bool
        :param proxy_index: The proxy index for which we need a tooltip
        :type proxy_index: `QtCore.QModelIndex`
        :param source_index: The source index for which we need a tooltip
        :type source_index: `QtCore.QModelIndex`
        :param source_model: The source model to map to
        :type source_model: `QtWidgets.QAbstractItemModel`
        :return: A tooltip for the specified column or None
        :rtype: str or None
        """
        if is_title_row:
            return col.tooltip
        return self._nonHeaderToolTipData(col, proxy_index, source_index,
                                          source_model)
    def _nonHeaderToolTipData(self, col, proxy_index, source_index,
                              source_model):
        """
        Subclasses should override this function to return the tooltip
        appropriate for a given cell, if any.
        """
        return None
[docs]    def columnCount(self, parent=None):
        # See Qt documentation for method documentation
        return len(self._cur_columns) 
[docs]    def endLayoutChange(self):
        self._invalidateAllPersistentIndices()
        self.layoutChanged.emit() 
    def _sourceRowsAboutToBeInserted(self, source_parent, first, last):
        # See QAbstractItemModel.rowsAboutToBeInserted signal documentation for
        # argument documentation
        proxy_parent = self.mapFromSource(source_parent)
        self.beginInsertRows(proxy_parent, first, last)
    def _sourceRowsAboutToBeRemoved(self, source_parent, first, last):
        # See QAbstractItemModel.rowsAboutToBeRemoved signal documentation for
        # argument documentation
        proxy_parent = self.mapFromSource(source_parent)
        self.beginRemoveRows(proxy_parent, first, last)
[docs]    @QtCore.pyqtSlot(int, int)
    def updateData(self, role, source_row):
        """
        Update a specified index when data changes.
        :param role: Role of the data that changed.
        :type role: `enum.Enum`
        :param source_row: Index of the source model index to update.
        :type source_row: int
        """
        row = self.mapFromSource(self.index(source_row, 0)).row()
        col = self.ROLE_TO_COL.get(role)
        if col is None or row < 0:
            return
        index = self.index(row, col)
        self.dataChanged.emit(index, index) 
    def _rowHeightChanged(self):
        """
        Emit dataChanged and rowHeightChanged when we receive a rowHeightChanged
        signal. We emit dataChanged with an invalid index to signal that all
        indices have changed, to force a cache clear and repaint.
        """
        invalid_index = QtCore.QModelIndex()
        self.dataChanged.emit(invalid_index, invalid_index)
        self.rowHeightChanged.emit()
    def _textSizeChanged(self):
        """
        Emit dataChanged and textSizeChanged when we receive a textSizeChanged
        signal. We emit dataChanged with an invalid index to signal that all
        indices have changed, to force a cache clear and repaint.
        """
        invalid_index = QtCore.QModelIndex()
        self.dataChanged.emit(invalid_index, invalid_index)
        self.textSizeChanged.emit()
[docs]    def isWorkspaceAln(self):
        """
        :return: Whether this model represents the workspace alignment.
        :rtype: bool
        """
        return self.sourceModel().isWorkspaceAln() 
[docs]    def selectableColumns(self):
        """
        Return a list of all selectable columns (i.e. columns flagged with
        Qt.ItemIsSelectable).
        :rtype: list(int)
        """
        return self._selectable_cols.copy() 
    def _sourceDataChanged(self, topleft, bottomright, roles=None):
        """
        Respond to data changing in the source model.  Standard Qt model/view
        behavior ignores dataChanged signals with invalid indices, but we use
        them to indicate that everything has changed, so this method passes
        those signals along to the view.
        See QAbstractItemModel.dataChaged signal documentation for argument
        documentation.
        """
        if not topleft.isValid() and not bottomright.isValid():
            self.dataChanged.emit(topleft, bottomright) 
class AlignmentInfoColumns(table_helper.TableColumns):
    """
    An enum for the left-hand fixed columns.
    """
    Expand = FixedColumn("")
    Struct = FixedColumn("", selectable=True)
    Title = FixedColumn("TITLE",
                        tooltip=None,
                        role=CustomRole.RowTitle,
                        all_rows=True,
                        align=Qt.AlignLeft,
                        selectable=True)
    Chain = FixedColumn("CHN",
                        tooltip="Chain",
                        role=CustomRole.ChainCol,
                        all_rows=True,
                        selectable=True)
    # NOTE: Title column expands to fill full width of the table, so is
    # not technically "Fixed"
[docs]class AlignmentInfoProxyModel(DropProxyMixin, MouseOverPassthroughMixin,
                              AnnotationSelectionPassthroughMixin,
                              BaseAdjacentAlignmentProxyModel):
    """
    A proxy model that contains sequence info for an alignment.
    """
    Column = AlignmentInfoColumns
    ROLES_FROM_SOURCE = (BaseAdjacentAlignmentProxyModel.ROLES_FROM_SOURCE | {
        CustomRole.Included,
        CustomRole.CanDropAbove,
        CustomRole.CanDropBelow,
        CustomRole.IncludeInDragImage,
        CustomRole.HomologyStatus,
        CustomRole.PreviousRowHidden,
        CustomRole.NextRowHidden,
        SeqInfo.Title,
        SeqInfo.GaplessLength,
        SeqInfo.GapCount,
        SeqInfo.Chain,
        SeqInfo.Name,
        CustomRole.EntryID,
        CustomRole.PfamName,
    })
    ROLES_FROM_PROXY = (
        BaseAdjacentAlignmentProxyModel.ROLES_FROM_PROXY |
        {Qt.DecorationRole, Qt.ForegroundRole, Qt.BackgroundRole} |
        {CustomRole.InfoColumnType})
    orderChanged = QtCore.pyqtSignal(list)
[docs]    def __init__(self, parent=None):
        super(AlignmentInfoProxyModel, self).__init__(parent=parent)
        self._bold_font = QtGui.QFont(self._regular_font)
        self._bold_font.setBold(True)
        self._mouse_over_struct_col = False
        self._context_over_struct_col = False
        # Add role-to-column mappings to ROLE_TO_COL so that
        # FixedColumnDataChanged signals can be correctly interpreted.  These
        # roles aren't given in AlignmentInfoColumns above since their data are
        # provided via custom roles or Qt.DecorationRole, and data for roles
        # given in AlignmentInfoColumns is assumed to be provided via
        # Qt.DisplayRole.
        self.ROLE_TO_COL[CustomRole.PreviousRowHidden] = self.Column.Expand
        self.ROLE_TO_COL[CustomRole.NextRowHidden] = self.Column.Expand
        self.ROLE_TO_COL[CustomRole.HomologyStatus] = self.Column.Struct
        self.ROLE_TO_COL[CustomRole.Included] = self.Column.Struct
        self.ROLE_TO_COL[CustomRole.AnnotationSelected] = self.Column.Title
        self.setLightMode(False) 
[docs]    def setMouseOverIndex(self, proxy_index):
        """
        Store whether the mouse is over the struct column and the mouse-over
        index
        """
        col = proxy_index.column() if proxy_index is not None else None
        self._mouse_over_struct_col = col == self.Column.Struct
        if col == self.Column.Expand:
            # ignore the mouse when it's over the expansion column since we
            # don't want to highlight the row in that case
            proxy_index = None
        super().setMouseOverIndex(proxy_index) 
[docs]    def setContextOverIndex(self, proxy_index):
        self._context_over_struct_col = (proxy_index.column()
                                         == self.Column.Struct
                                         if proxy_index is not None else False)
        super().setContextOverIndex(proxy_index) 
[docs]    def isMouseOverStructCol(self):
        """
        Return whether the mouse is over the Struct column
        """
        return self._mouse_over_struct_col or self._context_over_struct_col 
[docs]    def setLightMode(self, enabled):
        """
        Set light mode for the info model. This changes the structure
        inclusion/exclusion icons as well as changing text and background color
        """
        if enabled:
            self._ref_background_brush = QtGui.QBrush(
                color.REF_SEQ_BACKGROUND_COLOR_LM)
            self._ref_row_color = color.REF_SEQ_FONT_COLOR_LM
            self._ref_row_sel_color = color.REF_SEQ_FONT_COLOR_LM
            self._sel_color = color.REG_FONT_COLOR_LM
            self._default_color = color.REG_FONT_COLOR_LM
        else:
            self._ref_background_brush = QtGui.QBrush(
                color.REF_SEQ_BACKGROUND_COLOR)
            self._ref_row_color = color.REF_SEQ_FONT_COLOR
            self._ref_row_sel_color = color.REF_SEQ_SEL_FONT_COLOR
            self._sel_color = color.REG_SEL_FONT_COLOR
            self._default_color = color.REG_FONT_COLOR
        self._icon_map = self._createIconMap(enabled)
        invalid_index = QtCore.QModelIndex()
        self.dataChanged.emit(invalid_index, invalid_index)  # Force repaint 
    def _createIconMap(self, light_mode):
        """
        Generate icons for the structure column
        """
        # We show an icon to indicate A) whether the sequence
        # has a structure and B) if it does, whether or not it's included.
        # No distinction is made for visibility.
        # See MSV-1590.
        struct_images = {
            None: None,
            True: self.getStructImage(True, light_mode),
            False: self.getStructImage(False, light_mode),
        }
        homology_colors = {
            None: None,
            HomologyStatus.Target: color.HOMOLOGY_TARGET_COLOR,
            HomologyStatus.Template: color.HOMOLOGY_TEMPLATE_COLOR,
        }
        transparent = QtGui.QColor(0, 0, 0, 0)
        painter = QtGui.QPainter()
        image_size = struct_images[True].size()
        icon_map = {}
        prod = itertools.product(homology_colors.items(), struct_images.items())
        for (homology_key, fill_color), (struct_key, struct_image) in prod:
            if fill_color is None and struct_image is None:
                image = None
            else:
                image = QtGui.QImage(image_size, QtGui.QImage.Format_ARGB32)
                if fill_color is None:
                    fill_color = transparent
                image.fill(fill_color)
                if struct_image is not None:
                    painter.begin(image)
                    painter.drawImage(0, 0, struct_image)
                    painter.end()
            icon_map[(homology_key, struct_key)] = image
        return icon_map
[docs]    def getStructImage(self, included, light_mode):
        """
        Get the image to be in the structure column
        :param included: Whether or not the structure is included in the
            workspace
        :type included: bool
        :param light_mode: Whether or not to use the light mode icons
        :type light_mode: bool
        :return: The structure column image
        :rtype: QtGui.QImage
        """
        path = ":/msv/icons/struct_"
        if included:
            path += "included"
        else:
            path += "excluded"
        if light_mode:
            path += "_light"
        path += ".png"
        return QtGui.QImage(path) 
[docs]    def setSeqSelectionState(self, selection, selected):
        """
        Mark the residues specified by `selection` as either selected or
        deselected.
        :param selection: A selection containing the entries to update.
        :type selection: QtCore.QItemSelection
        :param selected: Whether the entries should be selected (True) or
            deselected (False).
        :type selected: bool
        """
        source_selection = self.mapSelectionToSource(selection)
        self.sourceModel().setSeqSelectionState(source_selection, selected) 
[docs]    def clearSeqSelection(self):
        self.getAlignment().seq_selection_model.clearSelection() 
[docs]    def getMinimumWidth(self) -> int:
        """
        Return the minimum width of the view based on the title and chain title
        lengths.
        :return: The minimum length of the view
        """
        font_metrics = QtGui.QFontMetrics(self._regular_font)
        width = font_metrics.width(self.Column.Title.title +
                                   self.Column.Chain.title)
        return width + 50 
[docs]    def getTitleColumnWidth(self):
        """
        Return the width of the longest title currently shown, including both
        sequence titles and annotation names
        :rtype: int
        :return: The length of the longest title currently shown
        """
        # sequences may have bold font but annotations do not, so we calculate
        # their respective widths separately
        seq_font_metrics = QtGui.QFontMetrics(self._bold_font)
        ann_font_metrics = QtGui.QFontMetrics(self._regular_font)
        default_width = seq_font_metrics.width('W') * 5
        source_model = self.sourceModel()
        if source_model is None:
            return default_width
        aln = source_model.getAlignment()
        if aln is None or len(aln) == 0:
            return default_width
        longest_seq = max((seq.name for seq in aln), key=len)
        seq_title_length = seq_font_metrics.width(longest_seq)
        shown_anns = source_model.getShownRowTypes()
        annotation_names = [ann.title for ann in shown_anns]
        # If ligand contacts are shown, they are in the row title
        if SEQ_ANNO_TYPES.binding_sites in shown_anns:
            for seq in aln:
                annotation_names.extend(seq.annotations.ligands)
        longest_ann = max(annotation_names, key=len) if annotation_names else ""
        ann_title_length = ann_font_metrics.width(longest_ann)
        if (source_model.getGroupBy() is GroupBy.Sequence or
                not annotation_names):
            return max(default_width, seq_title_length, ann_title_length)
        else:
            combined_length = (ann_font_metrics.width(longest_seq + " ") +
                               ann_title_length)
            return max(default_width, seq_title_length, combined_length) 
[docs]    @QtCore.pyqtSlot(int, int)
    def updateData(self, role, source_row):
        """
        Update a specified index when data changes.
        :param role: Role of the data that changed.
        :type role: `enum.Enum`
        :param source_row: Index of the source model index to update.
        :type source_row: int
        """
        if role == CustomRole.MouseOver:
            row = self.mapFromSource(self.index(source_row, 0)).row()
            if row < 0:
                return
            start_col = self._cur_columns[0]
            end_col = self._cur_columns[-1]
            start_index = self.index(row, start_col)
            self.dataChanged.emit(start_index, self.index(row, end_col))
            num_children = self.rowCount(parent=start_index)
            if num_children:
                child_start_index = self.index(0, start_col, start_index)
                child_end_index = self.index(num_children - 1, end_col,
                                             start_index)
                self.dataChanged.emit(child_start_index, child_end_index)
        else:
            super().updateData(role, source_row) 
    def _fetchProxyRowData(self, roles, source_data, cols, is_title_row,
                           display_data_requested, font_requested,
                           decoration_requested, foreground_requested,
                           background_requested, tooltip_requested):
        """
        See parent class for method documentation.  Note that this method takes
        several additional arguments relative to the parent class method, which
        are documented here. (These additional arguments match the additional
        return values from `_mapRolesToSource`.)
        :param font_requested: Whether Qt.FontRole data was
            requested.  This role will not be in `roles` since
            `self._fontData` calls `self._multipleRolesData`, which calls
            this method, so fetching this value through the normal mechanisms
            would lead to an infinite loop.
        :type font_requested: bool
        :param decoration_requested: Whether Qt.DecorationRole data was
            requested.  This role will not be in `roles` since
            `self._decorationData` calls `self._multipleRolesData`, which calls
            this method, so fetching this value through the normal mechanisms
            would lead to an infinite loop.
        :type decoration_requested: bool
        :param foreground_requested: Whether Qt.ForegroundRole data was
            requested.
        :type foreground_requested: bool
        :param background_requested: Whether Qt.BackgroundRole data was
            requested.
        :type background_requested: bool
        :param tooltip_requested: Whether Qt.ToolTipRole data was
                                  requested.
        :type tooltip_requested: bool
        """
        row_data = super()._fetchProxyRowData(roles, source_data, cols,
                                              is_title_row,
                                              display_data_requested)
        for cur_col_num, cell_data in zip(cols, row_data):
            if font_requested:
                self._fontDataFromSourceData(cell_data)
            if decoration_requested:
                self._decorationDataFromSourceData(cell_data, cur_col_num)
            if foreground_requested:
                self._foregroundDataFromSourceData(cell_data)
            if background_requested:
                self._backgroundDataFromSourceData(cell_data)
            if tooltip_requested:
                self._toolTipDataFromSourceData(cell_data)
        return row_data
    def _mapRolesToSource(self, is_title_row, cols, roles):
        """
        See parent class for method documentation.  Note that the return value
        here includes several additional values not included in the parent class
        method's return value.
        :return: A tuple of:
          - Roles to fetch from the source model
          - Roles of data that can be calculated in this proxy
          - Whether Qt.DisplayRole data was requested
          - Whether Qt.FontRole data was requested
          - Whether Qt.DecorationRole data was requested
          - Whether Qt.ForegroundRole data was requested
          - Whether Qt.BackgroundRole data was requested
          - Whether Qt.ToolTipRole data was requested
        :rtype: tuple(set(int), set(int), bool, bool, bool, bool, bool)
        """
        font_requested = Qt.FontRole in roles
        foreground_requested = Qt.ForegroundRole in roles
        background_requested = Qt.BackgroundRole in roles
        tooltip_requested = Qt.ToolTipRole in roles
        roles -= {
            Qt.FontRole, Qt.ForegroundRole, Qt.BackgroundRole, Qt.ToolTipRole
        }
        if font_requested or foreground_requested or background_requested:
            roles.add(CustomRole.ReferenceSequence)
            roles.add(CustomRole.RowType)
        decoration_requested = Qt.DecorationRole in roles
        if decoration_requested:
            roles.discard(Qt.DecorationRole)
            if self.Column.Struct in cols:
                roles.add(CustomRole.HomologyStatus)
                roles.add(CustomRole.Included)
                roles.add(CustomRole.RowType)
        if tooltip_requested:
            roles.add(SeqInfo.Title)
            roles.add(CustomRole.RowType)
            roles.add(SeqInfo.GaplessLength)
            roles.add(SeqInfo.GapCount)
            roles.add(SeqInfo.Chain)
            roles.add(SeqInfo.Name)
            roles.add(CustomRole.EntryID)
            roles.add(CustomRole.PfamName)
        roles, proxy_roles, display_data_requested = super()._mapRolesToSource(
            is_title_row, cols, roles)
        return (roles, proxy_roles, display_data_requested, font_requested,
                decoration_requested, foreground_requested,
                background_requested, tooltip_requested)
    def _fontDataFromSourceData(self, data):
        reference_row = data.get(CustomRole.ReferenceSequence)
        sequence_row = data.get(CustomRole.RowType) is RowType.Sequence
        if reference_row and sequence_row:
            font = self._bold_font
        else:
            font = self._regular_font
        data[Qt.FontRole] = font
    def _decorationDataFromSourceData(self, data, col):
        if data.get(CustomRole.RowType) is not RowType.Sequence:
            return
        if col == self.Column.Struct:
            homology_status = data.get(CustomRole.HomologyStatus)
            included = data.get(CustomRole.Included)
            is_included = included_map.get(included)
            icon = self._icon_map[(homology_status, is_included)]
            data[Qt.DecorationRole] = icon
    def _foregroundDataFromSourceData(self, data):
        """
        Get the foreground role - text color - for the given cell data.
        """
        reference_row = data.get(CustomRole.ReferenceSequence)
        sequence_row = data.get(CustomRole.RowType) is RowType.Sequence
        selected_row = data.get(CustomRole.SeqSelected)
        if reference_row and sequence_row:
            if selected_row:
                font_color = self._ref_row_sel_color
            else:
                font_color = self._ref_row_color
        elif selected_row:
            font_color = self._sel_color
        else:
            font_color = self._default_color
        data[Qt.ForegroundRole] = font_color
    def _backgroundDataFromSourceData(self, data):
        """
        Get the background color brush for the given cell data for reference
        sequence rows only.
        """
        reference_row = data.get(CustomRole.ReferenceSequence)
        sequence_row = data.get(CustomRole.RowType) is RowType.Sequence
        if reference_row and sequence_row:
            data[Qt.BackgroundRole] = self._ref_background_brush
    def _toolTipDataFromSourceData(self, data):
        """
        Get the tooltip text for the given cell data
        """
        row_type = data.get(CustomRole.RowType)
        tooltip_list = [data.get(SeqInfo.Name)]
        if row_type is RowType.Sequence:
            tooltip_list.extend(self._getToolTipSequenceData(data))
        elif row_type is SEQ_ANNO_TYPES.pfam:
            tooltip_list = [data.get(CustomRole.PfamName)]
        elif row_type in SEQ_ANNO_TYPES or row_type in ALN_ANNO_TYPES:
            tooltip_list.extend(self._getToolTipAnnotationData(data))
        if None in tooltip_list:
            return
        tooltip = '<br>'.join(tooltip_list)
        data[Qt.ToolTipRole] = tooltip
    def _getToolTipSequenceData(self, data) -> typing.List[str]:
        """
        Get a list of tooltip data to show for a sequence.
        """
        tooltip = []
        chain_name = data.get(SeqInfo.Chain)
        if chain_name:
            tooltip.append(f'Chain {chain_name}')
        seq_title = data.get(SeqInfo.Title)
        if seq_title:
            if len(seq_title) > 80:
                tooltip.append(f'{seq_title}...')
            else:
                tooltip.append(seq_title)
        res_count = data.get(SeqInfo.GaplessLength)
        res_str = inflect.engine().no('residue', res_count)
        gap_count = data.get(SeqInfo.GapCount)
        full_gap_str = ''
        if gap_count > 0:
            gap_str = inflect.engine().no('gap', gap_count)
            full_gap_str = f', {gap_str}'
        tooltip.append(f'{res_str}{full_gap_str}')
        entry_id = data.get(CustomRole.EntryID)
        if entry_id:
            tooltip.append(f'Entry ID {entry_id}')
        return tooltip
    def _getToolTipAnnotationData(self, data) -> typing.List[str]:
        """
        Get a list of tooltip data to show for a sequence or alignment annotation.
        """
        tooltip = []
        chain_name = data.get(SeqInfo.Chain)
        row_type = data.get(CustomRole.RowType)
        if row_type in PRED_ANNO_TYPES:
            tooltip.append('<i>Prediction</i>')
        elif chain_name:
            tooltip.append(f'Chain {chain_name}')
        if row_type in RES_PROP_ANNO_TYPES or not row_type.tooltip:
            tooltip.append(row_type.title)
        else:
            tooltip.append(row_type.tooltip)
        return tooltip
    @table_helper.data_method(Qt.FontRole)
    def _fontData(self, col, is_title_row, proxy_index, source_index,
                  source_model):
        multiple_roles = {Qt.FontRole}
        data = self._multipleRolesData(col, is_title_row, proxy_index,
                                       source_index, source_model,
                                       multiple_roles)
        return data.get(Qt.FontRole)
    @table_helper.data_method(Qt.DecorationRole)
    def _decorationData(self, col, is_title_row, proxy_index, source_index,
                        source_model):
        data = self._multipleRolesData(col, is_title_row, proxy_index,
                                       source_index, source_model,
                                       {Qt.DecorationRole})
        return data.get(Qt.DecorationRole)
    @table_helper.data_method(Qt.ForegroundRole)
    def _foregroundData(self, col, is_title_row, proxy_index, source_index,
                        source_model):
        multiple_roles = {Qt.ForegroundRole}
        data = self._multipleRolesData(col, is_title_row, proxy_index,
                                       source_index, source_model,
                                       multiple_roles)
        return data.get(Qt.ForegroundRole)
    @table_helper.data_method(Qt.BackgroundRole)
    def _backgroundData(self, col, is_title_row, proxy_index, source_index,
                        source_model):
        multiple_roles = {Qt.BackgroundRole}
        data = self._multipleRolesData(col, is_title_row, proxy_index,
                                       source_index, source_model,
                                       multiple_roles)
        return data.get(Qt.BackgroundRole)
    def _nonHeaderToolTipData(self, col, proxy_index, source_index,
                              source_model):
        """
        Defines tooltip data for non-header rows of the table model.
        :param col: Column for this row
        :type col: int
        :param proxy_index: Proxy index for the cell
        :type proxy_index: `QtCore.QModelIndex`
        :param source_index: Source index for this cell
        :type source_index: `QtCore.QModelIndex`
        :param source_model: Source model for the table
        :type source_model: `QtWidgets.QAbstractItemModel`
        """
        multiple_roles = {Qt.ToolTipRole}
        data = self._multipleRolesData(col, False, proxy_index, source_index,
                                       source_model, multiple_roles)
        return data.get(Qt.ToolTipRole)
[docs]    def getAlignment(self):
        """
        Return the underlying alignment
        :return: The alignment if the source model exists; else None
        :rtype: `schrodinger.protein.alignment.BaseAlignment`
        """
        source_model = self.sourceModel()
        if source_model is not None:
            return source_model.getAlignment() 
[docs]    def flags(self, index):
        # See Qt documentation for additional method documentation
        flags = Qt.ItemIsEnabled
        if index.column() in self._selectable_cols:
            data = self.data(index, CustomRole.MultipleRoles,
                             {CustomRole.RowType, CustomRole.ReferenceSequence})
            row_type = data[CustomRole.RowType]
            if row_type is RowType.Sequence:
                flags |= Qt.ItemIsSelectable
                if not data[CustomRole.ReferenceSequence]:
                    flags |= Qt.ItemIsDragEnabled
            elif row_type in SELECTABLE_ANNO_TYPES:
                flags |= Qt.ItemIsSelectable
        return flags 
[docs]    def mimeData(self, indices):
        """
        Return an object used to represent `indices` during a drag-and-drop
        operation.  Since we only allow selected indices to be dragged, and
        since the selection is stored with the model rather than the view, we
        can safely ignore `indices` and just return an empty object.  The
        selected sequences are then fetched during the drop instead.
        See QAbstractItemModel documentation for additional method
        documentation.
        """
        return QtCore.QMimeData() 
    @table_helper.data_method(CustomRole.InfoColumnType)
    def _infoColumnTypeData(self, col):
        return col 
class AlignmentMetricsColumns(table_helper.TableColumns):
    """
    An enum for the right-hand fixed columns.
    """
    Identity = FixedColumn("ID %",
                           "Identity",
                           CustomRole.AlignmentIdentity,
                           percent=True,
                           align=Qt.AlignRight)
    Similarity = FixedColumn("SIM %",
                             "Similarity",
                             CustomRole.AlignmentSimilarity,
                             percent=True,
                             align=Qt.AlignRight)
    Conservation = FixedColumn("CON %",
                               "Conservation",
                               CustomRole.AlignmentConservation,
                               percent=True,
                               align=Qt.AlignRight)
    Score = FixedColumn("SCORE",
                        "Score",
                        CustomRole.AlignmentScore,
                        percent=False,
                        align=Qt.AlignRight)
[docs]class AlignmentMetricsProxyModel(BaseAdjacentAlignmentProxyModel):
    """
    A proxy model that contains the alignment metrics, such as percentage of
    identity, similarity, homology and score to the reference sequence.  Columns
    can be enabled or disabled via the options model.
    :note: If all columns are disabled, then this model will be completely empty
        (i.e. zero rows in addition to zero columns).  That's because there's no
        way to generate a valid QModelIndex object without any columns, so there
        would be no way to create a parent index when referring to a nested row.
        Instead, this model will ignore all row changes until a column is
        enabled, at which point it will emit modelAboutToBeReset and modelReset
        to resync the view.
    """
    Column = AlignmentMetricsColumns
    ROLES_FROM_SOURCE = BaseAdjacentAlignmentProxyModel.ROLES_FROM_SOURCE | {
        *range(RoleBase.SequenceProperty,
               RoleBase.SequenceProperty + MAX_SEQ_PROPS)
    }
    ROLES_FROM_PROXY = BaseAdjacentAlignmentProxyModel.ROLES_FROM_PROXY | {
        Qt.ForegroundRole, CustomRole.MetricsType
    }
[docs]    def __init__(self, parent):
        """
        :param parent: The Qt parent widget
        :type parent: QtWidgets.QWidget
        """
        super().__init__(parent)
        self.options_model = None
        self._cur_columns = []
        self.setLightMode(False) 
[docs]    def setLightMode(self, enabled):
        """
        Set light mode for the alignment metrics table. This changes the font
        color that is used
        """
        if enabled:
            self.font_color = color.REG_FONT_COLOR_LM
        else:
            self.font_color = color.REG_FONT_COLOR
        invalid_index = QtCore.QModelIndex()
        self.dataChanged.emit(invalid_index, invalid_index)  # Force repaint 
    def _columnSettings(self):
        """
        Get information on what columns are currently enabled in the options
        model.
        :return: A tuple of tuples of (column, enabled)
        :rtype: tuple(tuple(AlignmentMetricsColumn, bool))
        """
        c = self.Column
        options = self.options_model
        cols = [
            (c.Identity, options.show_identity_col),
            (c.Similarity, options.show_similarity_col),
            (c.Conservation, options.show_conservation_col),
            (c.Score, options.show_score_col)
        ]  # yapf: disable
        for idx, item in enumerate(options.sequence_properties):
            role = RoleBase.SequenceProperty + idx
            col = SequencePropertyColumn(item,
                                         role,
                                         title=item.display_name,
                                         tooltip=item.display_name)
            cols.append((col, item.visible))
        return cols  # yapf: disable
[docs]    def setOptionsModel(self, options_model):
        """
        Set the options model for the proxy, which are used for row filtering.
        :param options_model: The widget options.
        :type options_model: `schrodinger.application.msv.gui.gui_models.
                               OptionsModel`
        """
        attrs = ('show_score_col', 'show_similarity_col',
                 'show_conservation_col', 'show_identity_col',
                 'sequence_properties')
        if self.options_model is not None:
            for attr_name in attrs:
                signal = getattr(self.options_model, f"{attr_name}Changed")
                signal.disconnect()
        for attr_name in attrs:
            signal = getattr(options_model, f"{attr_name}Changed")
            signal.connect(self._invalidateFilter)
        self.options_model = options_model
        self._cur_columns = [
            col for col, enabled in self._columnSettings() if enabled
        ] 
[docs]    def rowCount(self, parent=None):
        # See Qt documentation for method documentation
        if not self._cur_columns:
            return 0
        else:
            return super(AlignmentMetricsProxyModel, self).rowCount(parent) 
    def _invalidateFilter(self):
        """
        Update which columns are included and excluded, emitting any signals
        necessary to notify the view of the changes.
        """
        self.beginResetModel()
        self._cur_columns = [
            col for col, enabled in self._columnSettings() if enabled
        ]
        self.endResetModel()
    def _sourceRowsAboutToBeInserted(self, source_parent, first, last):
        # See QAbstractItemModel.rowsAboutToBeInserted signal documentation for
        # argument documentation
        if self._cur_columns:
            super()._sourceRowsAboutToBeInserted(source_parent, first, last)
    def _sourceRowsAboutToBeRemoved(self, source_parent, first, last):
        # See QAbstractItemModel.rowsAboutToBeRemoved signal documentation for
        # argument documentation
        if self._cur_columns:
            super()._sourceRowsAboutToBeRemoved(source_parent, first, last)
[docs]    def endInsertRows(self):
        # See QAbstractItemModel documentation for method documentation
        if self._cur_columns:
            super().endInsertRows() 
[docs]    def endRemoveRows(self):
        # See QAbstractItemModel documentation for method documentation
        if self._cur_columns:
            super().endRemoveRows() 
[docs]    def flags(self, index):
        # See QAbstractItemModel documentation for method documentation
        # We assume that no columns in AlignmentMetricsColumns are defined as
        # selectable.
        return Qt.ItemIsEnabled 
    @table_helper.data_method(CustomRole.MetricsType)
    def _metricsTypeData(self, col):
        return col
    def _fetchProxyRowData(self, roles, source_data, cols, is_title_row,
                           display_data_requested, foreground_requested):
        """
        See parent class for method documentation.  Note that this method takes
        several additional arguments relative to the parent class method, which
        are documented here. (These additional arguments match the additional
        return values from `_mapRolesToSource`.)
        :param foreground_requested: Whether Qt.ForegroundRole data was
            requested.
        :type foreground_requested: bool
        """
        row_data = super()._fetchProxyRowData(roles, source_data, cols,
                                              is_title_row,
                                              display_data_requested)
        if foreground_requested:
            for cell_data in row_data:
                self._foregroundDataFromSourceData(cell_data)
        return row_data
    def _mapRolesToSource(self, is_title_row, cols, roles):
        """
        See parent class for method documentation.  Note that the return value
        here includes several additional values not included in the parent class
        method's return value.
        :return: A tuple of:
          - Roles to fetch from the source model
          - Roles of data that can be calculated in this proxy
          - Whether Qt.DisplayRole data was requested
          - Whether Qt.ForegroundRole data was requested
        :rtype: tuple(set(int), set(int), bool, bool)
        """
        foreground_requested = Qt.ForegroundRole in roles
        roles.discard(Qt.ForegroundRole)
        roles, proxy_roles, display_data_requested = super()._mapRolesToSource(
            is_title_row, cols, roles)
        return (roles, proxy_roles, display_data_requested,
                foreground_requested)
    def _foregroundDataFromSourceData(self, data):
        """
        Get the foreground role - text color - for the given cell data.
        """
        data[Qt.ForegroundRole] = self.font_color
    @table_helper.data_method(Qt.ForegroundRole)
    def _foregroundData(self, col, is_title_row, proxy_index, source_index,
                        source_model):
        multiple_roles = {Qt.ForegroundRole}
        data = self._multipleRolesData(col, is_title_row, proxy_index,
                                       source_index, source_model,
                                       multiple_roles)
        return data.get(Qt.ForegroundRole)
    def _nonHeaderToolTipData(self, col, proxy_index, source_index,
                              source_model):
        metric = source_model.data(source_index, col.role)
        if metric is None:
            return None
        if col.percent:
            return f'{100*metric:.2f}%'
        try:
            return f'{float(metric):.4f}'
        except ValueError:
            return metric