import enum
from contextlib import contextmanager
from functools import partial
import schrodinger
from schrodinger.application.msv import command
from schrodinger.application.msv.gui import dialogs
from schrodinger.application.msv.gui import gui_models
from schrodinger.application.msv.gui import row_delegates
from schrodinger.application.msv.gui import stylesheets
from schrodinger.application.msv.gui import viewconstants
from schrodinger.application.msv.gui import viewmodel
from schrodinger.application.msv.gui.menu import EnabledTargetSpec
from schrodinger.application.msv.gui.viewconstants import TOP_LEVEL
from schrodinger.application.msv.gui.viewconstants import Adjacent
from schrodinger.application.msv.gui.viewconstants import CustomRole
from schrodinger.application.msv.gui.picking import PickMode
from schrodinger.application.msv.gui.viewconstants import ResidueFormat
from schrodinger.application.msv.gui.viewconstants import RowType
from schrodinger.application.msv.gui.viewconstants import SortTypes
from schrodinger.infra import util
from schrodinger.models import mappers
from schrodinger.protein import alignment
from schrodinger.protein import annotation
from schrodinger.protein import constants as protein_constants
from schrodinger.protein import sequence
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.ui.qt import table_speed_up
from schrodinger.ui.qt import utils as qt_utils
from schrodinger.ui.qt import widgetmixins
from schrodinger.ui.qt.appframework2.application import get_application
from schrodinger.utils.scollections import DefaultFactoryDictMixin
maestro = schrodinger.get_maestro()
PROT_ALN_ANN_TYPES = annotation.ProteinAlignmentAnnotations.ANNOTATION_TYPES
PROT_SEQ_ANN_TYPES = annotation.ProteinSequenceAnnotations.ANNOTATION_TYPES
PRED_ANN_TYPES = annotation.ProteinSequenceAnnotations.PRED_ANNOTATION_TYPES
_EditorType = enum.Enum("_EditorType", ("Change", "Replace", "Insert"))
_DragAndDropState = enum.Enum("_DragAndDropState",
                              ("Hover", "MouseDown", "Dragging"))
class _DefaultFactoryDataCache(DefaultFactoryDictMixin,
                               table_speed_up.DataCache):
    pass
[docs]class StructureColumnContextMenu(StyledQMenu):
    """
    Class for context menus in the structure icon column of the alignment info
    view.
    """
    INCLUDE_TEXT = "Include Entry in Workspace"
    EXCLUDE_TEXT = "Exclude Entry from Workspace"
[docs]    def __init__(self, parent=None):
        super().__init__(parent)
        self.entry_ids = set()
        self.included = False
        self.toggle_inclusion = self.addAction(self.INCLUDE_TEXT)
        self.toggle_inclusion.triggered.connect(self.onInclusionChangeRequested,
                                                Qt.QueuedConnection)
        self.addSeparator()
        self.exclude_all_others = self.addAction("Exclude All Others")
        self.exclude_all_others.triggered.connect(self.excludeAllOthers,
                                                  Qt.QueuedConnection)
        if maestro:
            self.proj = maestro.project_table_get() 
[docs]    def setSelectedSeqs(self, sel_seqs):
        """
        :param sel_seqs: The currently selected sequences.
        :type  sel_seqs: iterable(sequence.Sequence)
        """
        self.entry_ids = {
            seq.entry_id for seq in sel_seqs if seq.entry_id not in ("", None)
        } 
[docs]    def setIncluded(self, included):
        """
        :param included: Whether the clicked on sequence has a structure
            included in the workspace
        :type  included: bool
        """
        self.included = included
        if included:
            text = self.EXCLUDE_TEXT
        else:
            text = self.INCLUDE_TEXT
        self.toggle_inclusion.setText(text) 
[docs]    def onInclusionChangeRequested(self):
        if self.included:
            self.excludeEntryInWorkspace()
        else:
            self.includeEntryInWorkspace() 
[docs]    @qt_utils.maestro_required
    def excludeEntryInWorkspace(self):
        currently_included = {row.entry_id for row in self.proj.included_rows}
        new_included = currently_included - self.entry_ids
        if len(new_included) != len(currently_included):
            if len(new_included) == 0:
                self._clearMaestroInclusion()
                return
            self.proj.includeRows(new_included, exclude_others=True) 
    @qt_utils.maestro_required
    def _clearMaestroInclusion(self):
        currently_included = {row.entry_id for row in self.proj.included_rows}
        maestro.command(f"entrywsexclude entry {','.join(currently_included)}")
[docs]    @qt_utils.maestro_required
    def includeEntryInWorkspace(self):
        self.proj.includeRows(self.entry_ids, exclude_others=False) 
[docs]    @qt_utils.maestro_required
    def excludeAllOthers(self):
        currently_included = {row.entry_id for row in self.proj.included_rows}
        new_included = currently_included & self.entry_ids
        if len(new_included) != len(currently_included):
            if len(new_included) == 0:
                self._clearMaestroInclusion()
                return
            self.proj.includeRows(new_included, exclude_others=True)  
[docs]class AbstractSelectionProxyModel(QtCore.QItemSelectionModel):
    """
    A "hollowed out" selection model that simply notifies the viewmodel about
    selection instead of keeping track of selection itself.  Instead, selection
    information is stored with the alignment.  This makes selection much faster
    (since QItemSelectionModels are incredibly slow when there are a lot of
    selection ranges) is makes it easier to keep data synchronized.
    Note that isSelected() will always return False for this selection model.
    Since isSelected() isn't virtual, any reimplementation here won't be called
    from C++, so we avoid reimplementing the method to avoid potential
    confusion.  As a result, selection information should always be fetched via
    `model.data(index, ROLE)` instead of through this selection model.  Note
    that this will cause problems with Ctrl+click behavior.  See
    `CtrlClickToggleMixin` to fix those issues.
    :cvar ROLE: The Qt role used for selection information.  Subclasses must
        redefine this value.
    :vartype ROLE: int
    """
    ROLE = None
[docs]    def select(self, selection, flags):
        # See Qt documentation for method documentation
        # This function is overloaded, so "selection" may actually be an index
        if isinstance(selection, QtCore.QModelIndex):
            index = selection
            if index.isValid():
                selection = QtCore.QItemSelection(index, index)
            else:
                selection = QtCore.QItemSelection()
        if flags & self.Clear:
            self._clearSelection()
        if flags & self.Select:
            selection_state = True
        elif flags & self.Deselect:
            selection_state = False
        elif flags & self.Toggle:
            first_index = selection.indexes()[0]
            selected = self.model().data(first_index, self.ROLE)
            selection_state = not selected
        else:
            return
        self._setSelectionState(selection, selection_state) 
    def _clearSelection(self):
        raise NotImplementedError
    def _setSelectionState(self, selection, selection_state):
        """
        Select or deselect the items corresponding to `selection`.
        :type selection: QtCore.QItemSelection
        :param selection_state: Whether to select or deselect the items
        :type  selection_state: bool
        """
        raise NotImplementedError 
[docs]class ResidueSelectionProxyModel(AbstractSelectionProxyModel):
    """
    Selection model for selecting residues in an alignment.  Intended to be used
    with `AbstractAlignmentView`.  Note that this model only supports selecting
    single residues.  For selecting residue ranges, use
    `AbstractAlignmentView.model().setResRangeSelectionState()` instead.
    Storing residue selection information with the alignment allows us to
    synchronize selection state with the maestro workspace.
    """
    ROLE = CustomRole.ResSelected
[docs]    def __init__(self, model=None, parent=None):
        # See Qt documentation for argument documentation
        self._ignore_next_select_call = False
        if model is not None:
            model.columnsAboutToBeRemoved.connect(self._ignoreNextSelectCall)
        super().__init__(model, parent) 
    def _ignoreNextSelectCall(self):
        """
        Ignore the next call to `select`.  `QItemSelectionModel` deselects any
        residues from columns that are about to be removed.  We don't want that
        behavior, though, since the alignment itself takes care of updating the
        selection as necessary.  As such, this method allows us to ignore the
        `select` call that comes from
        `QItemSelectionModelPrivate::_q_columnsAboutToBeRemoved`.
        """
        self._ignore_next_select_call = True
[docs]    def select(self, selection, flags):
        if self._ignore_next_select_call:
            self._ignore_next_select_call = False
        elif isinstance(selection, QtCore.QItemSelection):
            raise RuntimeError(
                "ResidueSelectionProxyModel only supports selecting single "
                "residues.   Use setResRangeSelectionState() instead.")
        else:
            super().select(selection, flags) 
    def _clearSelection(self):
        self.model().clearResSelection()
    def _setSelectionState(self, selection, selection_state):
        self.model().setResSelectionState(selection, selection_state) 
[docs]class SequenceSelectionProxyModel(AbstractSelectionProxyModel):
    """
    Selection model for selecting entire sequences in an alignment.
    Intended to be used with `AlignmentInfoView`.
    The actual selection model is stored with the alignment, similar to how
    residue selection is stored. This allows us to sync selection across
    wrap rows on top of improving performance over Qt's selection models.
    """
    ROLE = CustomRole.SeqSelected
[docs]    def __init__(self, model, parent, **kwargs):
        super().__init__(model, parent, **kwargs)
        # This prevents unexpected behavior when shift+clicking after
        # reordering the alignment.
        model.orderChanged.connect(self.clearCurrentIndex) 
    def _clearSelection(self):
        self.model().clearSeqSelection()
    def _setSelectionState(self, selection, selection_state):
        self.model().setSeqSelectionState(selection, selection_state) 
[docs]class CtrlClickToggleMixin:
    """
    A mixin for `QAbstractItemViews` that reimplements `selectionCommand` to fix
    broken Ctrl+click behavior caused by using `AbstractSelectionProxyModel`
    selection models.  QAbstractItemView::mousePressEvent replaces toggle
    commands with either select or deselect based on
    selection_model.isSelected(), but AbstractSelectionProxyModel.isSelected()
    always returns False.  To get around this, we replace toggle commands with
    select or deselect here using the correct selection status.
    :cvar SELECTION_ROLE: The Qt role used for selection information.  This
        value must be defined in the view class.
    :vartype SELECTION_ROLE: int
    :ivar _drag_selection_flag: The selection command to apply when dragging the
        mouse.  This mimics the effects of
        QAbstractItemViewPrivate::ctrlDragSelectionFlag and prevents us from
        constantly toggling selection if the mouse is Ctrl+dragged within the
        same cell.
    :vartype _drag_selection_flag: int
    """
    SELECTION_ROLE = None
[docs]    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._drag_selection_flag = QtCore.QItemSelectionModel.Select 
[docs]    def selectionCommand(self, index, event=None):
        # See QAbstractItemView documentation for method documentation
        QISM = QtCore.QItemSelectionModel
        command = super().selectionCommand(index, event)
        if command & QISM.Toggle:
            command &= ~QISM.Toggle
            if event is not None and event.type() == event.MouseMove:
                command |= self._drag_selection_flag
            else:
                if self.model().data(index, self.SELECTION_ROLE):
                    new_command = QISM.Deselect
                else:
                    new_command = QISM.Select
                command |= new_command
                if event is not None and event.type() == event.MouseButtonPress:
                    self._drag_selection_flag = int(new_command)
        return command  
[docs]class SeqExpansionViewMixin:
    """
    A mixin for views that synchronize sequence expansion via the model.
    Must be mixed in with QTreeView.
    """
    # A context manager for ignoring changes in group expansion to avoid
    # infinite loops
    _ignoreExpansion = util.flag_context_manager("_ignore_expansion")
[docs]    def __init__(self, parent=None):
        self._ignore_expansion = False
        self._group_by = None
        super().__init__(parent)
        self.setExpandsOnDoubleClick(False) 
[docs]    def setModel(self, model):
        # See Qt documentation for method documentation
        old_model = self.model()
        if old_model is not None:
            old_model.seqExpansionChanged.disconnect(
                self._seqExpansionChangedInModel)
            old_model.groupByChanged.disconnect(self._groupByChanged)
            old_model.rowsInserted.disconnect(
                self._syncExpansionForInsertedRows)
            old_model.modelReset.disconnect(self._syncExpansion)
            old_model.layoutChanged.disconnect(self._syncExpansion)
        super().setModel(model)
        if model is not None:
            model.seqExpansionChanged.connect(self._seqExpansionChangedInModel)
            model.groupByChanged.connect(self._groupByChanged)
            model.rowsInserted.connect(self._syncExpansionForInsertedRows)
            model.modelReset.connect(self._syncExpansion)
            model.layoutChanged.connect(self._syncExpansion)
            self._group_by = model.getGroupBy()
            self._syncExpansion() 
    def _syncExpansion(self):
        """
        If we are grouping the view by Type, then simply expand the entire view.
        Otherwise, sync the views expansion state to the models expansion state.
        """
        if self._group_by is viewconstants.GroupBy.Type:
            self.expandAll()
        else:
            row_count = self.model().rowCount()
            if row_count > 0:
                last_row = row_count - 1
                self._syncExpansionForRows(0, last_row)
    def _syncExpansionForRows(self, start, end):
        """
        Sync the expand/collapse state for the specified row numbers.  This
        function should only be called when in group-by-sequence mode.
        :param start: The first row to sync state for.
        :type start: int
        :param end: The last row to sync state for.
        :type end: int
        """
        model = self.model()
        aln = model.getAlignment()
        seq_role = viewconstants.CustomRole.Seq
        with self._ignoreExpansion():
            for i in range(start, end + 1):
                index = model.index(i, 0)
                # We get seq from model since aln[i] is not necessarily the
                # same as the sequence this row corresponds to (indexes can be
                # offset by rows like ruler)
                seq = self.model().data(index, role=seq_role)
                if seq is None:
                    continue
                self.setExpanded(index, aln.isSeqExpanded(seq))
    def _syncExpansionForInsertedRows(self, parent, start, end):
        """
        Sync the expand/collapse state of inserted sequences.  See the
        QAbstractItemModel.rowsInserted signal documentation for argument
        documentation.
        """
        if parent.isValid():
            self._syncExpansionForRows(parent.row(), parent.row())
        elif self._group_by is viewconstants.GroupBy.Type:
            self.expandAll()
        else:
            self._syncExpansionForRows(start, end)
[docs]    def expandAll(self):
        if self._group_by is viewconstants.GroupBy.Sequence:
            self._setModelAllExpanded(True)
        super().expandAll() 
[docs]    def collapseAll(self):
        if self._group_by is viewconstants.GroupBy.Sequence:
            self._setModelAllExpanded(False)
        super().collapseAll() 
    def _setModelAllExpanded(self, expanded):
        model = self.model()
        for row_idx in range(model.rowCount()):
            idx = model.index(row_idx, 0)
            model.setData(idx, expanded, viewconstants.CustomRole.SeqExpanded)
[docs]    def expand(self, idx):
        self.setExpanded(idx, True) 
[docs]    def collapse(self, idx):
        self.setExpanded(idx, False) 
[docs]    def setExpanded(self, idx, expanded):
        if (self._group_by is viewconstants.GroupBy.Sequence and
                not self._ignore_expansion):
            self.model().setData(idx, expanded,
                                 viewconstants.CustomRole.SeqExpanded)
        super().setExpanded(idx, expanded) 
    def _seqExpansionChangedInModel(self, indices, expanded):
        """
        Respond to an expansion change coming from the model.
        :param indices: A list of all indices (`QtCore.QModelIndex`) that
            should be expanded or collapsed.
        :type indices: list
        :param expanded: True if the indices was expanded.  False if they were
            collapsed.
        :type expanded: bool
        """
        with self._ignoreExpansion():
            for index in indices:
                self.setExpanded(index, expanded)
    def _groupByChanged(self, group_by):
        """
        Respond to row groups being changed between group-by-sequence and
        group-by-annotation.
        :param group_by: Whether rows are grouped by sequence or annotation
        :type group_by: `viewconstants.GroupBy`
        """
        self._group_by = group_by
        self._syncExpansion() 
[docs]class AbstractAlignmentView(SeqExpansionViewMixin, CtrlClickToggleMixin,
                            widgetmixins.MessageBoxMixin, QtWidgets.QTreeView):
    """
    Class for viewing sequence alignments in a table
    :ivar residueHovered: Signal emitted when a residue cell is hovered
    :ivar residueUnhovered: Signal emitted when a residue cell is unhovered
    :ivar residueMiddleClicked: Signal emitted when middle-button is clicked
        on a residue cell
    :ivar sequenceEditModeRequested: Signal emitted when 'Edit Sequence in Place'
        context menu-item is toggled. Emitted with bool - whether the menu-item
        is toggled on or off.
    :ivar copyToNewTabRequested: Signal emitted to duplicate the sequences
        with only selected residues, to a new tab.
    :ivar _per_row_data_cache: A cache for data that is the same for all cells
        in a row.  Stored as
        {(row, internal_id): (row_type, per_row_data, row_del, per_cell_roles)},
        where per_row_data is {role: data}.  Note that our models ensure that
        all indices in the same row have the same internal ID.
    :vartype _per_row_data_cache: table_speed_up.DataCache
    :ivar _per_cell_data_cache: A cache of data needed for painting cells.
        {(row, column, internal_id): {role: data}}.
    :vartype _per_cell_data_cache: table_speed_up.DataCache
    :ivar _heights_by_row_type: A mapping of {row_type: row height in pixels}.
    :vartype _heights_by_row_type: dict
    :ivar _row_delegates: A list of all row delegates.  Each row delegate
        appears only once on the list even if it applies to more than one row
        type.
    :vartype _row_delegates: list(row_delegates.AbstractDelegate)
    :ivar _row_delegates_by_row_type: A mapping of {row_type: row delegate}.
    :vartype _row_delegates_by_row_type:
        dict(enum.Enum: row_delegates.AbstractDelegate)
    """
    SELECTION_ROLE = CustomRole.ResOrColSelected
    residueHovered = QtCore.pyqtSignal(object)
    residueUnhovered = QtCore.pyqtSignal()
    residueMiddleClicked = QtCore.pyqtSignal(object)
    openColorPanelRequested = QtCore.pyqtSignal()
    sequenceEditModeRequested = QtCore.pyqtSignal(bool)
    copyToNewTabRequested = QtCore.pyqtSignal()
[docs]    def __init__(self, undo_stack, parent=None):
        """
        :param undo_stack: The undo stack.  Used to group commands that happen
            on mouse drag into a single macro.
        :type undo_stack: schrodinger.application.msv.command.UndoStack
        :param parent: The Qt parent
        :type parent: QtWidgets.QWidget or None
        """
        super().__init__(parent)
        self._is_workspace = False
        # Note that the undo stack should only be used to begin and end macros.
        # The commands themselves should be generated in gui_alignment.
        self._undo_stack = undo_stack
        self._drag_possible = False
        self._drag_state = None
        self._drag_anchor_x = 0
        self._drag_failed_moves = 0
        self._sel_prior_to_mouse_click = None
        self._current_selection_mode = False
        self._initContextMenus()
        self._setupHeaders()
        self._num_cols_visible = 0
        self.setSelectionBehavior(self.SelectItems)
        self.setSelectionMode(self.ExtendedSelection)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self._update_cell_size_timer = self._initUpdateCellSizeTimer()
        self._update_row_wrap_timer = self._initUpdateRowWrapTimer()
        self._update_drag_and_drop_possible_timer = self._initUpdateDragAndDropPossibleTimer(
        )
        # Not patching this timer as it's only started in dragMoveEvent
        self._auto_scroll_timer = QtCore.QTimer(self)
        self._auto_scroll_timer.timeout.connect(self._doAutoScroll)
        self._auto_scroll_count = 0
        self.setMouseTracking(True)
        # force editors to be opened manually (i.e. via a toolbar button or a
        # keyboard shortcut we set up in keyPressEvent)
        self.setEditTriggers(self.NoEditTriggers)
        self._hovered_index = None
        self._edit_mode = False
        self.setTextElideMode(Qt.ElideNone)
        self.setRootIsDecorated(False)
        self.setIndentation(0)
        self.header().setStretchLastSection(False)
        # The number of rows in the table can be much, much higher than the
        # number of sequences if annotations are shown and row-wrapping is
        # turned on, so we use DataCache even for the per-row data cache.
        self._per_row_data_cache = _DefaultFactoryDataCache(
            self._populatePerRowCache, 50000)
        self._per_cell_data_cache = table_speed_up.DataCache(75000)
        self._heights_by_row_type = {}
        # we use a Qt delegate for editing (but *not* for painting, which is
        # handled by row delegates for performance reasons)
        self._edit_delegate = EditorDelegate(self)
        self._edit_delegate.moveChangeEditorRequested.connect(
            self._moveChangeEditor)
        self.setItemDelegate(self._edit_delegate)
        # row delegates handle painting
        self._row_delegates = []
        self._row_delegates_by_row_type = {}
        for cls in row_delegates.all_delegates():
            if not cls.ANNOTATION_TYPE:
                continue
            instance = cls()
            self._row_delegates.append(instance)
            ann_types = cls.ANNOTATION_TYPE
            if not isinstance(ann_types, (tuple, list, set)):
                ann_types = [ann_types]
            for row_type in ann_types:
                self._row_delegates_by_row_type[row_type] = instance
        self._col_width = 0
        # In Qt 5.12, the minimum section size is far too large and we don't
        # actually want a minimum anyway, so we just set it to a single pixel.
        self.header().setMinimumSectionSize(1)
        # _cols_to_paint and _col_left_edges are used to pass information from
        # paintEvent to drawRow since QTreeView::paintEvent does the actual
        # calling of drawRow
        self._cols_to_paint = None
        self._col_left_edges = None 
    def _initUpdateCellSizeTimer(self):
        # hook for patching
        timer = QtCore.QTimer(self)
        timer.setSingleShot(True)
        timer.setInterval(0)
        timer.timeout.connect(self._updateCellSize)
        return timer
    def _initUpdateRowWrapTimer(self):
        # hook for patching
        timer = QtCore.QTimer(self)
        timer.setSingleShot(True)
        timer.setInterval(0)
        timer.timeout.connect(self._updateRowWrapping)
        return timer
    def _initUpdateDragAndDropPossibleTimer(self):
        # hook for patching
        timer = QtCore.QTimer(self)
        timer.setSingleShot(True)
        timer.setInterval(0)
        timer.timeout.connect(self._updateDragAndDropPossible)
        return timer
    def __copy__(self):
        copy_view = self.__class__(self._undo_stack, parent=self.parent())
        copy_view.setModel(self.model())
        copy_view.setIsWorkspace(self._is_workspace)
        return copy_view
[docs]    def setLightMode(self, enabled):
        """
        Enable or disable light mode on all delegates
        """
        for delegate in self._row_delegates:
            delegate.setLightMode(enabled) 
[docs]    def setIsWorkspace(self, is_workspace):
        self._is_workspace = is_workspace 
    def _setupHeaders(self):
        """
        Configure the table headers
        """
        header = self.header()
        header.setSectionResizeMode(header.Fixed)
        header.hide()
[docs]    def setModel(self, model):
        """
        Set the model and ensure that changes to the model columns trigger
        updates in the delegates
        See Qt documentation for argument documentation
        """
        cell_size_signals = [
            "modelReset", "rowsInserted", "rowsRemoved", "layoutChanged",
            "residueFormatChanged", "textSizeChanged", "rowHeightChanged"
        ]
        cache_signals = [
            "rowsInserted", "rowsRemoved", "rowsMoved", "columnsInserted",
            "columnsRemoved", "columnsMoved", "modelReset", "layoutChanged",
            "dataChanged"
        ]
        old_model = self.model()
        if old_model is not None:
            for signal_name in cell_size_signals:
                cur_signal = getattr(old_model, signal_name)
                cur_signal.disconnect(self._update_cell_size_timer.start)
            for signal_name in cache_signals:
                cur_signal = getattr(old_model, signal_name)
                cur_signal.disconnect(self._clearCachesOnModelChange)
            old_model.residueSelectionChanged.disconnect(
                self._onResidueSelectionChanged)
            old_model.alnSetChanged.disconnect(self._updateDragAndDropPossible)
        super().setModel(model)
        selection_model = ResidueSelectionProxyModel(model, self)
        self.setSelectionModel(selection_model)
        if old_model is None:
            # If this is the first time we're setting a model, make sure that
            # cell size and row wrapping are set correctly.  We don't need to
            # worry about this on subsequent calls, since AnnotationProxyModel
            # keeps RowWrapProxyModel up to date even when row wrapping is
            # turned off
            self._updateCellSize(update_row_wrap=False)
            # Delay the row wrapping update to make sure that the panel is fully
            # loaded when it happens.  Otherwise, we won't yet know the correct
            # width of this view
            self._update_row_wrap_timer.start()
        for signal_name in cell_size_signals:
            cur_signal = getattr(model, signal_name)
            # We use _update_cell_size_timer to avoid unnecessary repetition of
            # _updateCellSize().
            cur_signal.connect(self._update_cell_size_timer.start)
        for signal_name in cache_signals:
            cur_signal = getattr(model, signal_name)
            cur_signal.connect(self._clearCachesOnModelChange)
        model.residueSelectionChanged.connect(self._onResidueSelectionChanged)
        model.alnSetChanged.connect(self._updateDragAndDropPossible) 
[docs]    def mousePressEvent(self, event):
        model = self.model()
        pos = event.pos()
        index = self.indexAt(pos)
        pick_mode = model.getPickingMode()
        row_type = index.data(CustomRole.RowType)
        if (pick_mode is PickMode.HMBindingSite and
                row_type is PROT_SEQ_ANN_TYPES.binding_sites) or (
                    pick_mode is PickMode.HMProximity and
                    row_type is RowType.Sequence):
            model.handlePick(index)
            return
        aln = model.getAlignment()
        # This value will be checked if the user double-clicks.
        self._sel_prior_to_mouse_click = aln.res_selection_model.getSelection()
        mod = event.modifiers()
        if (self._drag_possible and index.data(CustomRole.ResSelected) and
                mod == Qt.NoModifier):
            self._drag_anchor_x = pos.x()
            self._drag_state = _DragAndDropState.MouseDown
            self.setCursor(Qt.ClosedHandCursor)
            # TODO: draw rectangle (on a delay?)
        elif mod & int(Qt.ControlModifier | Qt.ShiftModifier):
            # If Ctrl or Shift are down, then we want to modify the current
            # selection instead of the main selection while the mouse is down in
            # case this mouse press starts a drag selection.  See
            # viewmodel.SequenceAlignmentModel.setResRangeSelectionState and
            # gui_alignment.ResidueSelectionModel.setCurrentSelectionState
            # for more information about the current selection.
            self._current_selection_mode = True
        elif event.button() == Qt.MidButton:
            residue = self.model().data(index, CustomRole.Residue)
            self.residueMiddleClicked.emit(residue)
        super().mousePressEvent(event) 
[docs]    def mouseReleaseEvent(self, event):
        # See Qt documentation for method documentation
        super().mouseReleaseEvent(event)
        # We must wait until after the super() call to update _drag_state since
        # selectionCommand needs to know if this press started a drag-and-drop
        # or not.
        pos = event.pos()
        index = self.indexAt(pos)
        aln = self.model().getAlignment()
        if self._drag_state is _DragAndDropState.Dragging:
            self._undo_stack.endMacro()
            self._stopAutoScroll()
        if self._drag_state in (_DragAndDropState.MouseDown,
                                _DragAndDropState.Dragging):
            self._drag_anchor_x = None
            self._drag_state = None
            self._drag_failed_moves = 0
            self.unsetCursor()
            index = self.indexAt(event.pos())
            self._updateDragAndDropCursor(index)
        else:
            # Finish the current selection.  If we weren't in current selection
            # mode, then these lines will have no effect.
            self._current_selection_mode = False
            aln.res_selection_model.finishCurrentSelection() 
[docs]    def mouseMoveEvent(self, event):
        # See Qt documentation for method documentation
        pos = event.pos()
        index = self.indexAt(pos)
        self._updateDragAndDropOnMouseMove(event, pos, index)
        if index.isValid():
            index_id = (index.row(), index.column(), index.internalId())
            if index_id != self._hovered_index:
                residue = self.model().data(index, CustomRole.Residue)
                self.residueHovered.emit(residue)
                self._hovered_index = index_id
        else:
            self._unhoverResidue()
        if self._drag_state is None:
            return super().mouseMoveEvent(event) 
[docs]    def mouseDoubleClickEvent(self, event):
        is_shift = event.modifiers() & Qt.ShiftModifier
        if is_shift:
            # Shift+double-click interferes w/ single-click so we ignore it.
            return
        pos = event.pos()
        index = self.indexAt(pos)
        if index.isValid():
            row_type = index.data(CustomRole.RowType)
            res = index.data(CustomRole.Residue)
            modifier = event.modifiers()
            aln = self.model().getAlignment()
            if row_type is PROT_SEQ_ANN_TYPES.secondary_structure:
                if res and res.is_gap:
                    return
                func = aln.setSecondaryStructureSelectionState
            elif row_type is RowType.Sequence:
                func = aln.setRunSelectionState
            elif row_type is PROT_ALN_ANN_TYPES.indices:
                return
            else:
                return
            self._doDoubleClickSelectOrDeselect(res, modifier, func) 
        # Note that we don't call super().mouseDoubleClickEvent(event)
        # because it unintentionally further changes the selection.
    def _doDoubleClickSelectOrDeselect(self, res, modifier, func):
        """
        Select or deselect residues based on a double-click event.
        :param res: Residue that was double clicked on.
        :type res: residue.Residue
        :param modifier: The double click modifier used. Should be one of
                         Qt.NoModifier or Qt.ControlModifier
        :type modifier: int
        :param func: Function to call for updating selection. Should take a
                         residue.Residue as a parameter as well as a 'select'
                         keyword argument that takes a bool value to indicate
                         whether residues should be selected (True) or
                         deselected (False).
        :type func: callable
        """
        is_ctrl = modifier & Qt.ControlModifier
        aln = self.model().getAlignment()
        if is_ctrl:
            # If clicked run is already selected, deselect.
            # otherwise add run to current selection.
            orig_sel = self._sel_prior_to_mouse_click
            self._sel_prior_to_mouse_click = None
            seq = res.sequence
            run_idx = seq.getRun(res)
            run_sel = all(seq[i] in orig_sel for i in run_idx)
            select = not run_sel
        else:
            # clear prior selection and select based on clicked residue
            aln.res_selection_model.clearSelection()
            select = True
        func(res, select=select)
[docs]    def wheelEvent(self, event):
        # See Qt documentation for method documentation
        super().wheelEvent(event)
        index = self.indexAt(event.pos())
        self._updateDragAndDropCursor(index) 
[docs]    def event(self, e):
        # See Qt documentation for method documentation
        if e.type() == QtCore.QEvent.Leave:
            self._unhoverResidue()
        return super().event(e) 
    def _unhoverResidue(self):
        self.residueUnhovered.emit()
        self._hovered_index = None
    def _updateDragAndDropOnMouseMove(self, event, pos, index):
        """
        Handle drag and drop changes required when the user moves the mouse
        cursor.  This method will:
        - update the mouse cursor if the user is over a draggable selection
        - start drag and drop if the user has just moved the cursor after a
        mouse press
        - move residues if the user moves the cursor during a drag and drop
        :param event: The mouse move event
        :type event: QtGui.QMouseEvent
        :param pos: The cursor location in local coordinates.
        :type pos: QtCore.QPoint
        :param index: The index that the mouse cursor is currently over.
        :type index: QtCore.QModelIndex
        """
        if event.buttons() == Qt.NoButton:
            modifiers_pressed = event.modifiers() != Qt.NoModifier
            self._updateDragAndDropCursor(index, modifiers_pressed)
        elif self._drag_state is _DragAndDropState.MouseDown:
            horiz_movement = abs(self._drag_anchor_x - pos.x())
            start_drag_dist = QtWidgets.QApplication.startDragDistance()
            if horiz_movement > start_drag_dist:
                # start drag-and-drop once the cursor has moved far enough
                self._drag_state = _DragAndDropState.Dragging
                self._undo_stack.beginMacro("Drag and drop residues")
                aln = self.model().getAlignment()
                aln.expandSelectionToRectangle()
        if self._drag_state == _DragAndDropState.Dragging:
            x = pos.x()
            self._startAutoScrollIfNeeded(x)
            self._dragResidues(x)
    def _dragResidues(self, mouse_x):
        """
        Potentially move residues in response to the user moving the cursor
        during drag and drop.
        :param mouse_x: The current x-coordinate of the mouse cursor.
        :type mouse_x: int
        """
        if not self.model().rowWrapEnabled():
            # If row wrapping is turned off, then don't allow dragging off the
            # visible portion of the view.  (The user can auto-scroll instead.)
            if mouse_x < 0:
                mouse_x = 0
            elif mouse_x > self.width():
                mouse_x = self.width()
        drag_dist = mouse_x - self._drag_anchor_x
        # We intentionally use an int cast here instead of integer division
        # so that values are rounded towards zero instead of down
        col_movement = int(drag_dist / self._col_width)
        if col_movement == 0:
            # The user hasn't dragged a full column width, so we don't need to
            # do anything
            return
        self._drag_anchor_x += col_movement * self._col_width
        if self._drag_failed_moves:
            # if drag-and-drop can't move residues (because of a lack of gaps or
            # anchoring), then make sure that we don't try to move residues
            # again until the user has moved the mouse cursor back over the
            # dragged residues.
            if (col_movement > 0) == (self._drag_failed_moves > 0):
                # We're moving in the same direction as the last failure, so we
                # know that this move will fail as well.
                self._drag_failed_moves += col_movement
                col_movement = 0
            else:
                # we're moving in a different direction than the last failure
                if abs(self._drag_failed_moves) >= abs(col_movement):
                    # we still haven't moved the mouse back to where we
                    # started failing
                    self._drag_failed_moves += col_movement
                    col_movement = 0
                else:
                    col_movement += self._drag_failed_moves
                    self._drag_failed_moves = 0
        if col_movement == 0:
            return
        aln = self.model().getAlignment()
        if col_movement > 0:
            success_count = aln.moveSelectionToRight(col_movement)
        else:
            success_count = -aln.moveSelectionToLeft(-col_movement)
        failed = col_movement - success_count
        self._drag_failed_moves = failed
    def _updateDragAndDropPossible(self):
        """
        Figure out whether drag and drop is currently possible and update the
        mouse cursor (open hand if it's over a drag-and-droppable selection, or
        a normal pointer otherwise).
        """
        self._drag_possible = (self._edit_mode and
                               not self.model().alnSetResSelected() and
                               self.model().isSingleBlockSelected())
        if QtWidgets.QApplication.mouseButtons() == Qt.NoButton:
            self._updateDragAndDropCursor(None)
    def _updateDragAndDropCursor(self, index, modifiers_pressed=None):
        """
        If the mouse cursor is currently over a drag-and-droppable selection,
        change the cursor to an open hand.  Otherwise, reset it back to a normal
        cursor.
        :param index: The index that the mouse cursor is over, or None.  If
            None, the index will be determined if it is required.  Note that if
            this method is called from a mouse event handler, `event.pos()`
            should be used to fetch an index and that index should be given here
            instead of passing `None`.
        :type index: QtCore.QModelIndex or None
        :param modifiers_pressed: Whether any modifier keys are currently
            pressed on the keyboard.
        :type modifiers_pressed: bool
        """
        if (not modifiers_pressed and self._drag_possible and
                self._drag_state is None):
            if index is None:
                cursor_pos = self.mapFromGlobal(QtGui.QCursor.pos())
                index = self.indexAt(cursor_pos)
            if index.data(CustomRole.ResSelected):
                self._drag_state = _DragAndDropState.Hover
                self.setCursor(Qt.OpenHandCursor)
        elif self._drag_state is _DragAndDropState.Hover:
            self.unsetCursor()
            self._drag_state = None
    def _startAutoScrollIfNeeded(self, mouse_x):
        """
        Start auto-scroll (i.e. scrolling while in drag-and-drop mode when the
        mouse cursor is close to the edges of the view) if the specified
        position is close enough to the view margins.
        :param mouse_x: The x-coordinate of the cursor location in local
            coordinates.
        :type mouse_x: int
        """
        if self.model().rowWrapEnabled():
            # If row wrapping is turned on, then there's nothing to scroll to
            return
        margin = self.autoScrollMargin()
        if mouse_x < margin or mouse_x > self.width() - margin:
            # The 150 value is taken from
            # QAbstractItemViewPrivate::startAutoScroll
            self._auto_scroll_timer.start(150)
            self._auto_scroll_count = 0
    def _stopAutoScroll(self):
        self._auto_scroll_timer.stop()
        self._auto_scroll_count = 0
    def _doAutoScroll(self):
        """
        If the cursor is currently close to the view margins, auto-scroll the
        view.  Activated during drag-and-drop.  Note that this implementation is
        based on QAbstractItemView::doAutoScroll.
        """
        scroll = self.horizontalScrollBar()
        # auto-scroll gradually accelerates up to a max of a page at a time
        if self._auto_scroll_count < scroll.pageStep():
            self._auto_scroll_count += 1
        cursor_pos = QtGui.QCursor.pos()
        viewport_pos = self.viewport().mapFromGlobal(cursor_pos)
        viewport_x = viewport_pos.x()
        margin = self.autoScrollMargin()
        if viewport_x < margin:
            scroll.setValue(scroll.value() - self._auto_scroll_count)
            self._drag_anchor_x += self._auto_scroll_count
        elif viewport_x > self.width() - margin:
            scroll.setValue(scroll.value() + self._auto_scroll_count)
            self._drag_anchor_x -= self._auto_scroll_count
        else:
            self._stopAutoScroll()
            return
        view_pos = self.mapFromGlobal(cursor_pos)
        view_x = view_pos.x()
        self._dragResidues(view_x)
[docs]    def selectionCommand(self, index, event=None):
        """
        Customize selection behavior.
        Filter right button mouse events to prevent selection changes when
        showing context menu on already-selected cells.
        Filter middle button clicks to prevent any selection changes.
        Make clicks on the ruler select the column.
        If a mouse press might potentially start a drag and drop operation, wait
        until mouse release to change the selection.
        See Qt documentation for argument documentation.
        """
        if self.model().getPickingMode() in (PickMode.HMBindingSite,
                                             PickMode.HMProximity):
            return QtCore.QItemSelectionModel.NoUpdate
        if index.isValid():
            row_cache_key = (index.row(), index.internalId())
            row_type, *_ = self._per_row_data_cache[row_cache_key]
        else:
            row_type = None
        is_mouse = isinstance(event, QtGui.QMouseEvent)
        res_selected = self.model().data(index, CustomRole.ResSelected)
        if is_mouse:
            if event.button() == Qt.RightButton:
                # Qt doesn't create context menus for right-clicks with
                # modifiers so we do it ourselves here.
                self._showContextMenu(event.pos())
                sel_model = self.model().getAlignment().res_selection_model
                if not sel_model.hasSelection():
                    return QtCore.QItemSelectionModel.Select
                elif event.modifiers() == Qt.ShiftModifier:
                    return QtCore.QItemSelectionModel.SelectCurrent
                elif event.modifiers() == Qt.ControlModifier:
                    return QtCore.QItemSelectionModel.Select
                elif (row_type is PROT_ALN_ANN_TYPES.indices and
                      event.button() == Qt.RightButton and
                      self.model().data(index, CustomRole.ColSelected)):
                    return QtCore.QItemSelectionModel.NoUpdate
                elif res_selected:
                    return QtCore.QItemSelectionModel.NoUpdate
            elif event.button() == Qt.MidButton:
                return QtCore.QItemSelectionModel.NoUpdate
            elif (res_selected and self._drag_possible and
                  event.type() == event.MouseButtonPress and
                  event.modifiers() == Qt.NoModifier):
                # This click might start a drag and drop
                return QtCore.QItemSelectionModel.NoUpdate
            elif event.type() == event.MouseButtonRelease:
                # We're releasing from a press that could've started a drag and
                # drop.  (selectionCommand will only be called on mouse release
                # if it returned NoUpdate on the press.)
                if self._drag_state is _DragAndDropState.MouseDown:
                    # The press didn't actually start a drag and drop, so update
                    # the selection as if this was a normal click
                    return QtCore.QItemSelectionModel.ClearAndSelect
                elif self._drag_state is _DragAndDropState.Dragging:
                    # The press started a drag and drop, so don't update the
                    # selection at all
                    return QtCore.QItemSelectionModel.NoUpdate
        flags = super().selectionCommand(index, event)
        if is_mouse and row_type is PROT_ALN_ANN_TYPES.indices:
            return flags | QtCore.QItemSelectionModel.Columns
        return flags 
    def _updateCellSize(self, *, update_row_wrap=True):
        """
        Set the appropriate width for all columns based on what row types are
        currently shown.
        :param update_row_wrap: Whether to update row wrapping after updating
            the cell size.  If `False`, `_updateRowWrapping` must be called
            after this method has completed.
        :type update_row_wrap: bool
        """
        col_width, text_height = self._colWidthAndTextHeight()
        self._col_width = col_width
        self._heights_by_row_type = {
            row_type: row_del.rowHeight(text_height)
            for row_type, row_del in self._row_delegates_by_row_type.items()
        }
        self.header().setDefaultSectionSize(self._col_width)
        self._per_cell_data_cache.clear()
        self._per_row_data_cache.clear()
        for row_delegate in self._row_delegates:
            row_delegate.clearCache()
        if update_row_wrap:
            self._updateRowWrapping()
        # items layout is necessary to update row height when the font size
        # changes
        self.scheduleDelayedItemsLayout()
    def _colWidthAndTextHeight(self):
        """
        Determine the appropriate column width and text height for the current
        model settings.
        :return: A tuple of:
            - The column width in pixels
            - The text height in pixels.  This value can be used to determine
            row heights using AbstractDelegate.rowHeight.
        :rtype: tuple(int, int)
        """
        model = self.model()
        if model is None:
            font = self.font()
        else:
            font = model.getFont()
        font_metrics = QtGui.QFontMetrics(font)
        if (model is not None and
                model.getResidueDisplayMode() is ResidueFormat.ThreeLetter):
            # A sample three letter residue name
            text = "WOI"
        else:
            # a sample one-letter residue name.  We intentionally pick a
            # wide letter to make sure that the columns are wide enough.
            text = "W"
        # We use 1.3  and 7 to add a small bit of padding for the letters
        col_width = int(font_metrics.boundingRect(text).width() * 1.3)
        text_height = font_metrics.tightBoundingRect("W").height() + 7
        return col_width, text_height
[docs]    def sizeHintForColumn(self, col):
        # See QTreeView documentation for method documentation
        return self._col_width 
[docs]    def sizeHintForRow(self, row):
        """
        Get the height of a top-level row.
        Note that you probably want to use `indexRowSizeHint` instead of this
        method, as that method can determine the height of any row, not just a
        top-level one.  This is still called by QTreeView::scrollContentsBy,
        though, so we reimplement it here to avoid the delay that would be
        caused by QTreeView::sizeHintForRow, which calls delegate.sizeHint for
        every cell in the row.
        See QAbstractItemView documentation for additional method documentation.
        """
        row_type = self._per_row_data_cache[row, TOP_LEVEL][0]
        return self._heights_by_row_type[row_type] 
[docs]    def indexRowSizeHint(self, index):
        """
        Get the height of a row.
        This method is not normally virtual in QTreeView, but we're using a
        modified version of Qt so that we can override this method.  The default
        implementation calls delegate.sizeHint for every column in the row,
        which leads to major delays while scrolling.
        :note: This method can be called with indices from `self.model()` or
            from a `BaseFixedColumnsView`'s model.  (See
            `BaseFixedColumnsView.indexRowSizeHint`.)  Index row and internal ID
            will be the same regardless of model, but this method must not
            assume that `index.model() == self.model()`.
        See QTreeView documentation for additional method documentation.
        """
        row = index.row()
        internal_id = index.internalId()
        row_type, per_row_data, _, _ = self._per_row_data_cache[row,
                                                                internal_id]
        row_height = self._heights_by_row_type[row_type]
        try:
            row_height_scale = per_row_data[CustomRole.RowHeightScale]
            row_height *= row_height_scale
        except (KeyError, TypeError):
            # If the row doesn't have row height scale data, then leave it
            # unscaled.
            pass
        return row_height 
[docs]    def sizeHintForIndex(self, index):
        # See Qt documentation for method documentation
        height = self.indexRowSizeHint(index)
        return QtCore.QSize(self._col_width, height) 
    def _populatePerRowCache(self, row, internal_id):
        """
        Fetch data for the specified `row` and `internal_id`.  This method
        should only be called by `self._per_row_data_cache.__missing__`.
        Otherwise, row data should be retrieved by using
        `self._per_row_data_cache`.
        :param row: The row number of the row to fetch data for.
        :type row: int
        :param internal_id: The internal ID of the row to fetch data for.  (Our
            models ensure that all indices in the same row have the same
            internal ID.  This is not guaranteed to be generally true to all
            `QAbstractItemModel`.)
        :type internal_id: int
        :return: A tuple of:
            - The row type of the specified row.  (For sequence or spacer rows,
            this is a `RowType` enum.  For annotation rows this is an
            `AnnotationType` enum.)
            - The per-row data fetched from the model.
            - The row delegate for the row type.
            - The per-cell roles for the row type.
        :rtype: tuple(enum.Enum, dict(int, object),
            row_delegates.AbstractDelegate, set(int))
        """
        model = self.model()
        index = model.createIndex(row, 0, internal_id)
        per_row_data = model.data(index, CustomRole.MultipleRoles,
                                  row_delegates.PER_ROW_PAINT_ROLES)
        # If the dictionary is empty, then it means that we're trying to get the
        # size hint for a row that's in the process of being inserted or
        # removed.  In that case, just treat it as a spacer for now and
        # everything will be updated once the insertion/removal is complete.
        row_type = per_row_data.get(CustomRole.RowType,
                                    viewconstants.RowType.Spacer)
        row_del = self._row_delegates_by_row_type[row_type]
        per_cell_roles = row_del.PER_CELL_PAINT_ROLES
        # get rid of data for any per_cell_roles
        for role in (row_delegates.POSSIBLE_PER_ROW_PAINT_ROLES &
                     per_cell_roles):
            per_row_data.pop(role, None)
        return row_type, per_row_data, row_del, per_cell_roles
    def _initContextMenus(self):
        """
        Set up residue and gap context menus
        """
        self._res_context_menu = ResidueContextMenu(self)
        self._anno_context_menu = AnnoResContextMenu(self)
        for menu in (self._res_context_menu, self._anno_context_menu):
            res_ss = (
                (menu.expandBetweenGapsRequested, self.expandSelectionBetweenGaps),
                (menu.expandAlongColumnsRequested, self.expandSelectionAlongColumns),
                (menu.expandFromReferenceRequested, self.expandSelectionFromReference),
                (menu.expandToFullChainRequested, self.expandSelectionToFullChain),
                (menu.invertSelectionRequested, self.invertResSelection),
                (menu.copyToNewTabRequested, self.copyToNewTabRequested),
                (menu.searchForMatchesRequested, self.searchForMatches),
                (menu.copyRequested, self.copySelection),
                (menu.deleteRequested, self.deleteSelection),
                (menu.deleteGapsOnlyRequested, self.deleteSelectedGaps),
                (menu.replaceWithGapsRequested, self.replaceSelectionWithGaps),
                (menu.highlightSelectionRequested, self.openColorPanelRequested),
                (menu.removeHighlightsRequested, self.clearSelectedHighlights),
                # TODO: MSV-1994 (menu.hideColumnsRequested, self.hideColumns),
                (menu.anchorRequested, self.anchorSelection),
                (menu.unanchorRequested, self.unanchorSelection),
                (menu.editSeqInPlaceRequested, self.sequenceEditModeRequested),
            )  # yapf: disable
            for signal, slot in res_ss:
                signal.connect(slot)
        self._anno_context_menu.expandToAnnoValsRequested.connect(
            self.expandSelectionToAnnoVals)
        menu = GapContextMenu(self)
        gap_ss = (
            (menu.expandAlongGapsRequested, self.expandSelectionAlongGaps),
            (menu.deselectGapsRequested, self.deselectGaps),
            (menu.expandAlongColumnsRequested, self.expandSelectionAlongColumns),
            (menu.expandFromReferenceRequested, self.expandSelectionFromReference),
            (menu.expandToFullChainRequested, self.expandSelectionToFullChain),
            (menu.invertSelectionRequested, self.invertResSelection),
            (menu.copyToNewTabRequested, self.copyToNewTabRequested),
            (menu.deleteGapsOnlyRequested, self.deleteSelectedGaps),
            (menu.copyRequested, self.copySelection),
            (menu.highlightSelectionRequested, self.openColorPanelRequested),
            (menu.removeHighlightsRequested, self.clearSelectedHighlights),
            # TODO: MSV-1994 (menu.hideColumnsRequested, self.hideColumns),
            (menu.anchorRequested, self.anchorSelection),
            (menu.unanchorRequested, self.unanchorSelection),
            (menu.editSeqInPlaceRequested, self.sequenceEditModeRequested),
        )  # yapf: disable
        for signal, slot in gap_ss:
            signal.connect(slot)
        self._gap_context_menu = menu
    def _showContextMenu(self, pos):
        clicked_index = self.indexAt(pos)
        clicked_residue = clicked_index.data(CustomRole.Residue)
        if clicked_residue is None:  # terminal gap
            return
        row_type = clicked_index.data(CustomRole.RowType)
        if clicked_residue.is_gap:
            context_menu = self._gap_context_menu
        elif row_type in PROT_SEQ_ANN_TYPES:
            context_menu = self._anno_context_menu
            ann_index = clicked_index.data(CustomRole.MultiRowAnnIndex)
            context_menu.setClickedAnno(row_type, ann_index)
        else:
            context_menu = self._res_context_menu
        aln = self.model().getAlignment()
        enable_expand_from_ref = self._shouldEnableExpandFromReference()
        context_menu.setExpandFromReferenceEnabled(enable_expand_from_ref)
        clicked_residue_is_anchored = clicked_index.data(CustomRole.ResAnchored)
        context_menu.setAnchorEnabled(clicked_residue_is_anchored or
                                      aln.anchorResidueValid(clicked_residue))
        context_menu.setAnchorMode(not clicked_residue_is_anchored)
        context_menu.popup(self.mapToGlobal(pos))
    def _shouldEnableExpandFromReference(self):
        """
        The "Expand From Reference" menu item should be enabled if a reference
        residue is included in the selection and its column isn't already
        selected.
        """
        aln = self.model().getAlignment()
        ref_seq = aln.getReferenceSeq()
        res_selection_model = aln.res_selection_model
        sel_residues = res_selection_model.getSelection()
        for selected_residue in sel_residues:
            if selected_residue.sequence == ref_seq:
                column = aln.getColumn(selected_residue.idx_in_seq)
                if not all(res in sel_residues for res in column):
                    return True
        else:
            return False
[docs]    def expandSelectionBetweenGaps(self):
        """
        Expand selected residues between gaps within each sequence.
        """
        self.model().getAlignment().expandSelectionAlongSequences(True) 
[docs]    def expandSelectionAlongGaps(self):
        """
        Expand selected gaps along gaps within each sequence.
        """
        self.model().getAlignment().expandSelectionAlongSequences(False) 
[docs]    def deselectGaps(self):
        """
        Deselect selected gaps
        """
        self.model().getAlignment().deselectGaps() 
[docs]    def expandSelectionToAnnoVals(self, anno, ann_index):
        self.model().expandSelectionToAnnotationValues(anno, ann_index) 
[docs]    def expandSelectionAlongColumns(self):
        """
        Expand selection along the columns of selected elements.
        """
        self.model().getAlignment().expandSelectionAlongColumns() 
[docs]    def expandSelectionFromReference(self):
        """
        Expand selection along columns of selected reference elements
        """
        self.model().getAlignment().expandSelectionFromReference() 
[docs]    def expandSelectionToFullChain(self):
        self.model().getAlignment().expandSelectionToFullChain() 
[docs]    def invertResSelection(self):
        """
        Invert selection of residues in the alignment
        """
        self.model().getAlignment().invertResSelection() 
[docs]    def replaceSelectionWithGaps(self):
        """
        Replace selected with gaps.
        """
        aln = self.model().getAlignment()
        selection = aln.res_selection_model.getSelection()
        if len(selection.intersection(aln.getAnchoredResiduesWithRef())) > 0:
            response = QtWidgets.QMessageBox.question(
                self, "Some anchors will be removed",
                "Some anchors will be removed. Continue anyway?")
            if response != QtWidgets.QMessageBox.Yes:
                return
        undo_desc = f'Replace {len(selection)} Residues with Gaps'
        with command.compress_command(aln.undo_stack, undo_desc):
            aln.removeAnchors(selection)
            aln.replaceResiduesWithGaps(selection) 
[docs]    def clearSelectedHighlights(self):
        """
        Clear the highlights of the selection.
        """
        self.model().getAlignment().setSelectedResColor(None) 
[docs]    def anchorSelection(self):
        """
        Respond to an "Anchor" event selected through the residue context menu.
        Anchor selected residues using the following per-column protocol:
            1) If only reference residues are selected, anchor
                all non-reference residues aligned to those reference
                residues.
            2) If any non-reference residues (or a mix of reference and
                non-reference residues) are selected, anchor all selected
                non-reference residues.
            3) Any selected elements that can't be anchored (due to being gaps
                or being in columns with reference gaps) will be ignored.
        """
        aln = self.model().getAlignment()
        ref_seq = aln.getReferenceSeq()
        sel_residues = aln.res_selection_model.getSelection()
        processed_sel_residues = []
        for res in sel_residues:
            beyond_ref_seq = (res.idx_in_seq >= len(ref_seq))
            if beyond_ref_seq or ref_seq[res.idx_in_seq].is_gap:
                # Ignore gaps and elements in columns with reference gaps
                continue
            processed_sel_residues.append(res)
        aln.anchorResidues(processed_sel_residues) 
[docs]    def unanchorSelection(self):
        """
        Test that the view has correct behavior for residue unanchoring:
            1) If only non-reference residues are selected, unanchor
                them.
            2) If any reference residues (or a mix of reference and
                non-reference residues) are selected, unanchor all non-reference
                residues aligned to the selected reference residues.
        """
        aln = self.model().getAlignment()
        sel_residues = aln.res_selection_model.getSelection()
        aln.removeAnchors(sel_residues) 
[docs]    def resizeEvent(self, event):
        """
        Update the row wrapping whenever the table is resized
        See Qt documentation for argument documentation
        """
        super().resizeEvent(event)
        self._update_row_wrap_timer.start() 
    def _updateRowWrapping(self, force=False):
        """
        Alert the model if the number of visible columns has changed, as the
        model will need to update row wrapping.
        :param force: If False, the current number of columns will only be sent
            to the model if the number has changed since the last update.  If
            True, the current number of columns will be sent no matter what.
        :type force: bool
        """
        if self._col_width == 0:
            # this may happen when the table is in the process of being
            # displayed
            return 0
        table_width = self.viewport().width()
        num_cols = table_width // self._col_width
        if force or num_cols != self._num_cols_visible:
            model = self.model()
            model.tableWidthChanged(num_cols)
            self._num_cols_visible = num_cols
[docs]    def paintEvent(self, event):
        """
        We override this method to speed up painting.  We calculate what columns
        need to be painted and the x-coordinates of their left edges, which get
        used in `drawRow` below.
        See Qt documentation for additional method documentation.
        """
        # calculate values to be used in drawRow
        header = self.header()
        rect = event.region().boundingRect()
        # visualIndexAt returns -1 if the coordinate is not over a column
        first_visual_col = header.visualIndexAt(rect.left())
        last_visual_col = header.visualIndexAt(rect.right())
        if first_visual_col < 0:
            first_visual_col = 0
        if last_visual_col < 0:
            last_visual_col = header.count() - 1
        self._cols_to_paint = list(range(first_visual_col, last_visual_col + 1))
        self._col_left_edges = list(
            map(self.columnViewportPosition, self._cols_to_paint))
        if self._cols_to_paint:
            super().paintEvent(event)
        self._cols_to_paint = None
        self._col_left_edges = None 
[docs]    def drawRow(self, painter, option, index):
        """
        This view paints entire rows at once instead of painting each cell
        separately.  This gives us a ~6x improvement in scrolling frame rate
        (assuming data is already cached).  This view also fetches data for
        entire rows at once, which gives us an additional ~2x improvement in
        scrolling frame rate when data is uncached.
        See Qt documentation for additional method documentation.
        """
        row_rect = option.rect
        row_height = row_rect.height()
        if row_height <= 1:
            # If the row_rect is only one pixel tall, then there's nothing to
            # paint. For example, this can happen when disulfide bonds are
            # enabled but one sequence has no bonds. We don't use zero pixels
            # since QTreeView sometimes keeps stale QModelIndex objects around
            # when they refer to zero height rows, which can lead to tracebacks.
            return
        # make sure that we don't modify the passed in option object
        model = self.model()
        row = index.row()
        # Our models use the same internal ID for all columns in a row
        internal_id = index.internalId()
        # load all data we'll need into cache
        per_cell_data = []
        row_type, per_row_data, row_del, per_cell_roles = \
            
self._per_row_data_cache[row, internal_id]
        # uncached_cols contains the column numbers for cells that weren't found
        # in the cache
        uncached_cols = []
        # uncached_i contains the per_cell_data indices for cells that weren't
        # found in the cache
        uncached_i = []
        for i, col in enumerate(self._cols_to_paint):
            try:
                data = self._per_cell_data_cache[row, col, internal_id]
            except KeyError:
                data = None
                uncached_cols.append(col)
                uncached_i.append(i)
            per_cell_data.append(data)
        if uncached_cols:
            uncached_data = model.rowData(row, uncached_cols, internal_id,
                                          per_cell_roles)
            for i, col, cur_data in zip(uncached_i, uncached_cols,
                                        uncached_data):
                self._per_cell_data_cache[row, col, internal_id] = cur_data
                per_cell_data[i] = cur_data
        row_del.paintRow(painter, per_cell_data,
                         per_row_data, row_rect, self._col_left_edges,
                         row_rect.top(), self._col_width, row_height) 
[docs]    def setSequenceExpansionState(self, sequences, expand=True):
        """
        Set the expansion state for the given sequences.
        :param sequences: Sequences to expand or collapse.
        :type sequences: list(Sequence)
        :param expand: Whether to expand the sequences
        :type expand: bool
        """
        model = self.model()
        aln = model.getAlignment()
        offset = model.getNumShownGlobalAnnotations()
        new_expanded = [aln.index(seq) + offset for seq in sequences]
        for i in new_expanded:
            index = model.index(i, 0)
            self.setExpanded(index, expand) 
[docs]    def setConstraintsShown(self, enable):
        """
        Enable or disable constraint display
        :param enable: Whether to display constraints
        :type enable: bool
        """
        # tell the sequence row to start painting constraints
        self._row_delegates_by_row_type[RowType.Sequence].setConstraintsShown(
            enable)
        self.update() 
[docs]    def setLigandConstraintsShown(self, enable):
        self._row_delegates_by_row_type[
            PROT_SEQ_ANN_TYPES.binding_sites].setConstraintsShown(enable)
        self.update() 
[docs]    def setChimeraShown(self, enable):
        self._row_delegates_by_row_type[RowType.Sequence].setChimeraShown(
            enable)
        self.update() 
[docs]    def setResOutlinesShown(self, enable):
        # tell the sequence row to start painting res outlines
        self._row_delegates_by_row_type[RowType.Sequence].setResOutlinesShown(
            enable)
        self.update() 
[docs]    def setEditMode(self, enable):
        """
        Enable or disable edit mode.
        :param enable: Whether to enable edit mode.
        :type enable: bool
        """
        self._edit_mode = enable
        # tell the sequence editor to start painting I-bars
        self._row_delegates_by_row_type[RowType.Sequence].setEditMode(enable)
        # clear the cache so I-bar locations will be fetched
        self._per_cell_data_cache.clear()
        self._per_row_data_cache.clear()
        self._updateDragAndDropPossible()
        self._res_context_menu.edit_seq_in_place.setChecked(enable)
        self._gap_context_menu.edit_seq_in_place.setChecked(enable)
        # force a redraw so that I-bars can be drawn
        self.update() 
[docs]    def editAndMaybeClearAnchors(self, edit_func):
        """
        Try to perform an edit action. If it can't proceed because it would
        break anchors, ask the user whether they want to cancel (keeping
        anchors) or break anchors and proceed.
        :param edit_func: the function that may break anchors
        :type  edit_func: callable
        """
        try:
            edit_func()
        except alignment.AnchoredResidueError as exc:
            anchors_to_remove = exc.blocking_anchors
            remove_all_anchors = exc.blocking_anchors is exc.ALL_ANCHORS
            mbox = dialogs.EditClearAnchorsMessageBox(
                parent=self, remove_all=remove_all_anchors)
            response = mbox.exec()
            if response is True:
                desc = "Clear Anchors and Edit"
                with command.compress_command(self._undo_stack, desc):
                    aln = self.model().getAlignment()
                    if remove_all_anchors:
                        aln.clearAnchors()
                    else:
                        aln.removeAnchors(anchors_to_remove)
                    edit_func() 
[docs]    def keyPressEvent(self, event):
        # See Qt documentation for method documentation
        if event.key() == Qt.Key_A and event.modifiers() & Qt.ControlModifier:
            self.model().getAlignment().res_selection_model.selectAll()
        elif event.key() == Qt.Key_C and event.modifiers() & Qt.ControlModifier:
            self.copySelection()
        elif self._edit_mode and self.state() != self.EditingState:
            # EditingState means there's currently an editor open
            if event.key() == Qt.Key_Space:
                self.insertGapsToLeftOfSelection()
            elif event.key() == Qt.Key_Delete:
                self.deleteSelectedGaps()
            elif event.key() in (Qt.Key_Enter, Qt.Key_Return):
                self.editCurrentSelection()
            else:
                super().keyPressEvent(event)
        else:
            super().keyPressEvent(event) 
[docs]    def insertGapsToLeftOfSelection(self):
        """
        Insert one gap to the left of every selected block of residues/gaps.
        Editing is not allowed if alignment set residues are selected.
        """
        if self.model().alnSetResSelected():
            return
        edit_func = self.model().getAlignment().insertGapsToLeftOfSelection
        self.editAndMaybeClearAnchors(edit_func) 
[docs]    def deleteSelectedGaps(self):
        """
        Delete all selected gaps.  Selected residues will remain unchanged.
        Editing is not allowed if alignment set residues are selected.
        """
        if self.model().alnSetResSelected():
            return
        self.model().getAlignment().deleteSelectedGaps() 
[docs]    def copySelection(self):
        """
        Copies the selected residues and gaps as a string onto the clipboard.
        :raises ValueError: if the selection is not a single block.
        """
        res_selection_model = self.model().getAlignment().res_selection_model
        if not res_selection_model.hasSelection():
            return
        if not res_selection_model.isSingleBlockSingleSeqSelected():
            self.warning(
                'Copy action is limited to a single block from a single sequence.',
                title='Selection Not Copied',
            )
            return
        selected_res = res_selection_model.getSelection()
        text = "".join(
            str(res)
            for res in sorted(selected_res, key=lambda r: r.idx_in_seq))
        clipboard = get_application().clipboard()
        clipboard.setText(text) 
[docs]    def deleteSelection(self):
        """
        Delete all selected residues and gaps.  If more than one block of
        residues/gaps is selected, the user will be prompted for confirmation
        before the deletion occurs.
        """
        aln = self.model().getAlignment()
        if not aln.res_selection_model.isSingleBlockSelected():
            response = QtWidgets.QMessageBox.question(
                self, "Delete multiple selection blocks?",
                "Multiple blocks are selected.  Do you wish to delete all "
                "selected residues?")
            if response != QtWidgets.QMessageBox.Yes:
                return
        self._deleteSelection() 
[docs]    def deleteUnselectedResidues(self):
        """
        Delete all the unselected residues.
        """
        self.invertResSelection()
        self._deleteSelection() 
    def _deleteSelection(self):
        aln = self.model().getAlignment()
        edit_func = aln.deleteSelection
        self.editAndMaybeClearAnchors(edit_func)
[docs]    def editCurrentSelection(self):
        """
        If a single residue (or gap) is selected, open an editor for replacing
        one residue at a time.  If a single block of residues/gaps in a single
        sequence is selected, open an editor for overwriting the current
        selection.  Otherwise, do nothing.  Note that editing is disallowed for
        structured residues, on the workspace tab, or in alignment sets, so
        no editor will be opened in those cases.
        """
        model = self.model()
        sel_model = model.getAlignment().res_selection_model
        if (self._is_workspace or model.alnSetResSelected() or
                sel_model.anyStructuredResiduesSelected()):
            pass
        elif len(sel_model.getSelection()) == 1:
            self.changeResidues()
        elif sel_model.isSingleBlockSingleSeqSelected():
            self.replaceSelection() 
[docs]    def changeResidues(self):
        """
        Open an editor that allows the user to replace one residue or gap at a
        time.  This method should only be called when there is exactly one
        residue or gap selected.
        """
        indices = self.model().getSelectedResIndices()
        if len(indices) != 1:
            raise RuntimeError(
                "Cannot change residues with more than one residue selected")
        index = indices[0]
        self.setCurrentIndex(index)
        self.scrollTo(index)
        self._edit_delegate.setEditorType(_EditorType.Change)
        self.edit(index) 
[docs]    def insertResidues(self):
        """
        Open an editor that allows the user to insert residues before the
        current selection.  This method should only be called when there is
        exactly one block of residues or gaps from a single sequence selected.
        """
        first_sel_index, _ = self._getIndicesForSelectedBlock()
        self.scrollTo(first_sel_index)
        self._edit_delegate.setEditorType(_EditorType.Insert)
        self.edit(first_sel_index) 
[docs]    def replaceSelection(self):
        """
        Open an editor that allows the user to overwrite the current selection.
        This method should only be called when there is exactly one block of
        residues or gaps from a single sequence selected.
        Note that if row wrapping is turned on, it's possible that one
        contiguous group of residues may be split over multiple rows of the
        table.  The editor will only cover indices in the first row, but the
        newly entered sequence will replace all selected residues (and the
        editor will start with the entire sequence to be replaced).
        """
        first_sel_index, indices = self._getIndicesForSelectedBlock()
        row = first_sel_index.row()
        indices = [index for index in indices if index.row() == row]
        num_indices = len(indices)
        last_index = max(indices, key=lambda index: index.column())
        # scroll so as much of the selection as possible is visible
        self.scrollTo(last_index)
        self.scrollTo(first_sel_index)
        self._edit_delegate.setEditorType(_EditorType.Replace, num_indices)
        self.edit(first_sel_index) 
    def _getIndicesForSelectedBlock(self):
        """
        Return indices for the currently selected block.
        :raise RuntimeError: If no indices are selected.
        :return: A tuple of:
          - The first selected index (top-most row, left-most column)
          - A list of all selected indices
        :rtype: tuple(QtCore.QModelIndex, list[QtCore.QModelIndex])
        """
        indices = self.model().getSelectedResIndices()
        if not indices:
            raise RuntimeError("No residues selected")
        first_sel_index = min(indices,
                              key=lambda index: (index.row(), index.column()))
        return first_sel_index, indices
    def _moveChangeEditor(self, direction):
        """
        Move the change residue editor one index in the specified direction.
        :param direction: The direction to move the editor.
        :type direction: Adjacent
        """
        cur_index = self.currentIndex()
        new_index = self.model().getAdjacentIndexForEditing(
            cur_index, direction)
        if new_index.isValid():
            sel_model = self.selectionModel()
            self.setCurrentIndex(new_index)
            sel_model.select(new_index, sel_model.ClearAndSelect)
            self.scrollTo(new_index)
            # without the timer, the new editor opens without its contents
            # selected
            QtCore.QTimer.singleShot(0, partial(self.edit, new_index))
    def _clearCachesOnModelChange(self):
        self._per_row_data_cache.clear()
        self._per_cell_data_cache.clear()
        self._update_drag_and_drop_possible_timer.start()
    def _onResidueSelectionChanged(self):
        self._per_cell_data_cache.clear()
        self._updateDragAndDropPossible()
        # since residueSelectionChanged doesn't trigger dataChanged, we have to
        # manually trigger a repaint
        if self.isVisible():
            self.viewport().update()
[docs]    def setSelection(self, rect, flags):
        # See Qt documentation for method documentation
        # QAbstractItemView::mouseMoveEvent calls this method with a non-
        # normalized QRect, so rect.topLeft() isn't necessarily the top-left
        # corner of the rect.  Instead, rect.topLeft() is always where the mouse
        # press happened and rect.bottomRight() is always the current mouse
        # location.
        # We force a selection update whenever we change the selection
        # (`.forceSelectionUpdate`).  Without doing this, the
        # structure model will be unable to keep selection synchronized when
        # two or more sequences in the same tab are linked to the same chain.
        from_index = self.indexAt(rect.topLeft())
        to_index = self.indexAt(rect.bottomRight())
        QISM = QtCore.QItemSelectionModel
        aln = self.model().getAlignment()
        rsm = aln.res_selection_model
        if flags & QISM.Clear:
            self.model().clearResSelection()
            rsm.forceSelectionUpdate()
        if flags & QISM.Select:
            selection_state = True
        elif flags & QISM.Deselect:
            selection_state = False
        elif flags & QISM.Toggle:
            selected = self.model().data(from_index, CustomRole.ResSelected)
            selection_state = not selected
        else:
            return
        columns = flags & QISM.Columns
        self.model().setResRangeSelectionState(
            from_index,
            to_index,
            selection_state,
            columns,
            current=self._current_selection_mode)
        rsm.forceSelectionUpdate()  
[docs]class ProteinAlignmentView(AbstractAlignmentView):
    pass 
[docs]class LogoAlignmentView(NoScrollAlignmentView):
    """
    Alignment view meant to draw ExportLogoProxyModels.
    """
[docs]    def drawRow(self, painter, option, index):
        """
        Draw the row corresponding to the given index. This logic is based
        off of AbstractAlignmentView.drawRow. However, because the view is
        static, no caching is used.
        """
        row_rect = option.rect
        row_height = row_rect.height()
        if row_height <= 1:
            return
        row_type, per_row_data, row_del, per_cell_roles = \
            
self._per_row_data_cache[index.row(), index.internalId()]
        per_cell_data = self.model().rowData(index, self._cols_to_paint,
                                             per_cell_roles)
        row_del.paintRow(painter, per_cell_data,
                         per_row_data, row_rect, self._col_left_edges,
                         row_rect.top(), self._col_width, row_height)  
[docs]class BaseFixedColumnsView(SeqExpansionViewMixin, CtrlClickToggleMixin,
                           QtWidgets.QTreeView):
    """
    Class for fixed column views to be shown alongside an alignment view.
    :cvar CACHE_SIGNALS: Model signals for caching callbacks
    :vartype CACHE_SIGNALS: list(str)
    """
    SELECTION_ROLE = CustomRole.SeqSelected
    CACHE_SIGNALS = [
        "rowsInserted", "rowsRemoved", "rowsMoved", "columnsInserted",
        "columnsRemoved", "columnsMoved", "modelReset", "layoutChanged",
        "dataChanged", "textSizeChanged", "rowHeightChanged"
    ]
[docs]    def __init__(self, alignment_view, parent=None):
        """
        :param alignment_view: The main alignment view
        :type alignment_view: `ProteinAlignmentView`
        """
        super(BaseFixedColumnsView, self).__init__(parent)
        self.alignment_view = alignment_view
        self.setFrameShape(QtWidgets.QFrame.NoFrame)
        self.setSizePolicy(QtWidgets.QSizePolicy.Fixed,
                           QtWidgets.QSizePolicy.Preferred)
        self.setFocusPolicy(Qt.NoFocus)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.verticalScrollBar().hide()
        self.alignment_view.verticalScrollBar().valueChanged.connect(
            self.verticalScrollBar().setValue)
        self.verticalScrollBar().valueChanged.connect(
            self.alignment_view.verticalScrollBar().setValue)
        header = self.header()
        header.setSectionResizeMode(QtWidgets.QHeaderView.Fixed)
        header.setStretchLastSection(False)
        header.setMinimumSectionSize(1)
        header.hide()
        self._row_del = row_delegates.FixedColumnsDelegate()
        self._data_cache = table_speed_up.DataCache(5000)
        # The following variables are used to pass information from paintEvent
        # to drawRow since TreeView::paintEvent does the actual calling of
        # drawRow
        self._cols_to_paint = None
        self._col_left_edges = None
        self._col_widths = None
        self._selectable_cols = None
        self._selection_rect = QtCore.QRect()
        self._drop_indicator_y = None
        self._selection_xs = None 
[docs]    def sizeHint(self):
        # See Qt documentation for method documentation.
        view_width = self.getSizeHint()
        hint = super(BaseFixedColumnsView, self).sizeHint()
        hint.setWidth(view_width)
        return hint 
[docs]    def minimumSizeHint(self):
        return self.sizeHint() 
[docs]    def setModel(self, model):
        """
        Connects signals from model with the view.
        See Qt documentation for argument documentation
        """
        old_model = self.model()
        if old_model is not None:
            old_model.columnsInserted.disconnect(self.updateGeometry)
            old_model.columnsRemoved.disconnect(self.updateGeometry)
            old_model.modelReset.disconnect(self.updateGeometry)
            old_model.textSizeChanged.disconnect(
                self.scheduleDelayedItemsLayout)
            old_model.rowHeightChanged.disconnect(
                self.scheduleDelayedItemsLayout)
            for signal_name in self.CACHE_SIGNALS:
                getattr(old_model, signal_name).disconnect(self.clearCache)
        super(BaseFixedColumnsView, self).setModel(model)
        if model is not None:
            model.columnsInserted.connect(self.updateGeometry)
            model.columnsRemoved.connect(self.updateGeometry)
            model.modelReset.connect(self.updateGeometry)
            model.textSizeChanged.connect(self.scheduleDelayedItemsLayout)
            model.rowHeightChanged.connect(self.scheduleDelayedItemsLayout)
            for signal_name in self.CACHE_SIGNALS:
                getattr(model, signal_name).connect(self.clearCache)
            selectable_cols = model.selectableColumns()
            if not selectable_cols:
                selectable_cols = None
            self._selectable_cols = selectable_cols 
[docs]    def setLightMode(self, enabled):
        """
        Sets light mode on the delegate
        """
        self._row_del.setLightMode(enabled) 
[docs]    def indexRowSizeHint(self, index):
        """
        See `AbstractAlignmentView.indexRowSizeHint` documentation for method
        documentation.
        """
        return self.alignment_view.indexRowSizeHint(index) 
[docs]    def sizeHintForRow(self, row):
        """
        See `AbstractAlignmentView.sizeHintForRow` documentation for method
        documentation.
        """
        return self.alignment_view.sizeHintForRow(row) 
    def __copy__(self):
        copy_view = self.__class__(self.alignment_view, parent=self.parent())
        copy_view.setModel(self.model())
        return copy_view
    @contextmanager
    def _paintingColumns(self, first_visual_col, last_visual_col):
        """
        A context manager for painting.
        :param first_visual_col: The left-most column to paint
        :type first_visual_col: int
        :param last_visual_col: The right-most column to paint
        :type last_visual_col: int
        """
        try:
            self._cols_to_paint = list(
                range(first_visual_col, last_visual_col + 1))
            self._col_left_edges = list(
                map(self.columnViewportPosition, self._cols_to_paint))
            self._col_widths = list(map(self.columnWidth, self._cols_to_paint))
            if self._selectable_cols:
                # If this view paints a selection, set up the left and right
                # edges of self._selection_rect so we can use it to highlight
                # selected rows
                sel_left = self.columnViewportPosition(self._selectable_cols[0])
                sel_right_col = self._selectable_cols[-1]
                sel_right = (self.columnViewportPosition(sel_right_col) +
                             self.columnWidth(sel_right_col))
                self._selection_rect.setLeft(sel_left)
                self._selection_rect.setRight(sel_right)
                # selection_xs are used when drawing the drag-and-drop drop
                # indicator
                self._selection_xs = (sel_left, sel_right)
            yield
        finally:
            self._cols_to_paint = None
            self._col_left_edges = None
            self._col_widths = None
            # we intentionally don't destroy self._selection_rect so it can be
            # reused for the next paint
            self._selection_xs = None
[docs]    def paintEvent(self, event):
        """
        We override this method to speed up painting.  We calculate what columns
        need to be painted and various coordinates, which get used in `drawRow`
        below.
        See Qt documentation for additional method documentation.
        """
        # calculate values to be used in drawRow
        header = self.header()
        rect = event.region().boundingRect()
        # visualIndexAt returns -1 if the coordinate is not over a column
        first_visual_col = header.visualIndexAt(rect.left())
        last_visual_col = header.visualIndexAt(rect.right())
        if first_visual_col < 0:
            first_visual_col = 0
        if last_visual_col < 0:
            last_visual_col = header.count() - 1
        if last_visual_col >= first_visual_col:
            with self._paintingColumns(first_visual_col, last_visual_col):
                super(BaseFixedColumnsView, self).paintEvent(event)
                if self._drop_indicator_y is not None:
                    self._paintDropIndicator() 
[docs]    def drawRow(self, painter, option, index):
        """
        This view fetches data and paints entire rows at once instead of
        fetching data for and painting each cell separately.  This gives us a
        dramatic improvement in scrolling frame rate.
        See Qt documentation for additional method documentation.
        """
        row_rect = option.rect
        row_height = row_rect.height()
        if row_height <= 1:
            # If the row_rect is only one pixel tall, then there's nothing to
            # paint. For example, this can happen when disulfide bonds are
            # enabled but one sequence has no bonds. We don't use zero pixels
            # since QTreeView sometimes keeps stale QModelIndex objects around
            # when they refer to zero height rows, which can lead to tracebacks.
            return
        # make sure that we don't modify the passed in option object
        model = self.model()
        row = index.row()
        # Our models use the same internal ID for all columns in a row
        internal_id = index.internalId()
        roles = self._row_del.PAINT_ROLES
        # even if row wrap is on, we only want the title background painted for
        # the first wrap
        is_title_row = row == 0 and internal_id == TOP_LEVEL
        mouse_over_row, mouse_over_struct_col = self._isMouseOverIndex(index)
        # all_data contains all of the data for the entire row, once {role:
        # value} dictionary per column
        all_data = []
        # uncached_cols contains the column numbers for cells that weren't found
        # in the cache
        uncached_cols = []
        # uncached_i contains the all_data indices for cells that weren't found
        # in the cache
        uncached_i = []
        for i, col in enumerate(self._cols_to_paint):
            try:
                data = self._data_cache[row, col, internal_id]
            except KeyError:
                data = None
                uncached_cols.append(col)
                uncached_i.append(i)
            all_data.append(data)
        if uncached_cols:
            uncached_data = model.rowData(row, uncached_cols, internal_id,
                                          roles)
            for i, col, cur_data in zip(uncached_i, uncached_cols,
                                        uncached_data):
                self._data_cache[row, col, internal_id] = cur_data
                all_data[i] = cur_data
        row_top = row_rect.top()
        paint_expansion_column = self._cols_to_paint[0] == 0
        self._row_del.paintRow(painter, all_data, is_title_row,
                               self._selection_rect, row_rect,
                               self._col_left_edges, self._col_widths, row_top,
                               row_height, mouse_over_row,
                               mouse_over_struct_col, paint_expansion_column)
        # paint the expand/collapse arrow
        if internal_id == TOP_LEVEL and paint_expansion_column:
            cell_rect = QtCore.QRect(self._col_left_edges[0], row_top,
                                     self._col_widths[0], row_height)
            self.drawBranches(painter, cell_rect, index) 
    def _isMouseOverIndex(self, index):
        # Additional args for paintRow for mouse-over highlighting, which is
        # only implemented in AlignmentInfoView so we just return Falses here
        return False, False
[docs]    def mousePressEvent(self, event):
        """
        Manually handle mouse press events because the internal code that
        expands and collapses groups was not correctly detecting the click on
        Windows.
        :param event: the mouse event that occured
        :type event: QtGui.QMouseEvent
        """
        index = self.indexAt(event.pos())
        if self.isGroupIndicatorIndex(index):
            # index is of branch indicator
            self.setExpanded(index, not self.isExpanded(index))
        else:
            super().mousePressEvent(event) 
[docs]    def mouseReleaseEvent(self, event):
        """
        Ignore event if release is over group expansion arrow so it doesnt
        negate the effect of mousePressEvent.
        Typically, whether expansion happens on mouse press or release is
        platform dependent, so by doing this we are normalizing it to only
        happen on mouse press.
        """
        index = self.indexAt(event.pos())
        if not self.isGroupIndicatorIndex(index):
            super().mouseReleaseEvent(event) 
[docs]    def isGroupIndicatorIndex(self, index):
        """
        Get whether or not the given index corresponds to the cell used for
        the group expansion indicator
        """
        return index.internalId() == TOP_LEVEL and index.column() == 0 
    def _paintDropIndicator(self):
        """
        Paint the drag-and-drop drop indicator (the horizontal line showing
        where the dragged rows would be placed if they were dropped).  Note that
        this method should only be called in a `_paintingColumns` context and
        when `self._drop_indicator_y` is not None.
        """
        painter = QtGui.QPainter(self.viewport())
        painter.setPen(Qt.white)
        left, right = self._selection_xs
        y = self._drop_indicator_y
        painter.drawLine(left, y, right, y)
[docs]    def clearCache(self):
        self._data_cache.clear()
        self._row_del.clearCache()  
[docs]class AlignmentMetricsView(BaseFixedColumnsView):
    """
    View for the fixed columns to the right of the alignment that includes
    sequence identity and homology.
    """
    sortRequested = QtCore.pyqtSignal(object, bool)
[docs]    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.context_menu = None
        self.setRootIsDecorated(False)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        width = self.fontMetrics().width('W') * 5
        self.header().setDefaultSectionSize(int(width))
        self.context_menu = AlignmentMetricsContextMenu(parent=self)
        self.context_menu.sortRequested.connect(self.sortRequested) 
[docs]    def setModel(self, model):
        """
        Connects signals from model with the view.
        See Qt documentation for argument documentation
        """
        old_model = self.model()
        if old_model is not None:
            old_model.rowsInserted.disconnect(self._onRowsInserted)
            old_model.rowsRemoved.disconnect(self._onRowsRemoved)
        super().setModel(model)
        if model is not None:
            model.rowsInserted.connect(self._onRowsInserted)
            model.rowsRemoved.connect(self._onRowsRemoved) 
    def _onRowsInserted(self, parent_index, first, last):
        if parent_index.isValid():
            # rows inserted were not top level
            return
        number_of_added_rows = last - first + 1
        if number_of_added_rows == self.model().rowCount():
            # rows were added to empty alignment so restore view to full width
            self.updateGeometry()
    def _onRowsRemoved(self, parent_index, first, last):
        if parent_index.isValid():
            # rows removed were not top level
            return
        if not self.model().rowCount():
            # the final rows were removed so hide this view
            self.updateGeometry()
[docs]    def getSizeHint(self):
        if self.model().rowCount() == 0:
            # If there are no rows, no need to show alignment metrics
            return 0
        return sum(
            self.columnWidth(i) for i in range(self.model().columnCount()))  
[docs]class AlignmentInfoView(BaseFixedColumnsView):
    """
    View for the fixed columns to the left of the alignment that includes the
    structure title.
    This view allows the user to drag-and-drop selected rows to rearrange the
    order of sequences in the alignment.  However, the standard QTreeView
    drag-and-drop implementation isn't configurable enough for our needs, so
    this class reimplements drag-and-drop.  See the note in the `_canDrop`
    docstring for additional information.
    :cvar ADJUST_TIMER_SIGNALS: list of model signals to be
        connected to start the column width adjustment timer.
    :vartype ADJUST_TIMER_SIGNALS: list(str)
    :cvar DRAG_IMG_SCALE: The scale for the drag-and-drop image.  A value of 1
        means that the dragged rows will be drawn the same size that they appear
        in the table.
    :vartype DRAG_IMG_SCALE: float
    :cvar DRAG_IMG_OPACITY: The opacity for the drag-and-drop image.
    :vartype DRAG_IMG_OPACITY: float
    :ivar setAsReferenceSeq: Signal emitted to indicate that the selected
        sequence should be set as the reference sequence
    :ivar renameSeqClicked: Signal emitted to indicate that the selected
        sequence should be renamed
    :ivar findHomologsClicked: Signal emitted to indicate that the selected
        sequence should be used for a BLAST query
    :ivar findInListRequested: Signal to request find sequence in list
    :ivar mvSeqsClicked: Signal emitted to indicate that the selected
        sequences should be moved. Emits a list of the selected
        `sequence.Sequence` objects and the `viewconstants.Direction`. We use
        `object` as the param type because of a known issue with `enum_speedup`.
    :ivar rmSeqsClicked: Signal emitted to indicate that the selected
        sequence(s) should be deleted
    :ivar clearAnnotationRequested: Signal emitted to turn off the selected
        annotations
    :ivar deleteFromAllRequested: Signal emitted to delete the selected
        non-toggleable annotations (e.g. pfam, predictions)
    :ivar clearConstraintsRequested: Signal emitted to turn off pairwise
        constraints
    :ivar selectRowResidues: Signal emitted to indicate that all residues of
        all selected sequences should be selected in the alignment.
    :ivar sortRequested: Signal emitted to sort the sequences in ascending or
        descending order by a given metric.
        Emits an object as the metric to sort by
        'viewconstants.SortTypes'
        Emits a boolean for whether to sort in reverse or not
        'bool'
    :ivar getPdbClicked: Signal emitted to indicate that the GetPDB dialog should open
    :ivar deselectResiduesClicked: Signal emitted to indicate that the selection should
        be cleared
    """
    ADJUST_TIMER_SIGNALS = [
        "modelReset", "rowsInserted", "rowsRemoved", "layoutChanged",
        "textSizeChanged", "rowHeightChanged"
    ]
    SELECTION_ROLE = CustomRole.SeqSelected
    DRAG_IMG_SCALE = 0.75
    DRAG_IMG_OPACITY = 0.5
    clearAnnotationRequested = QtCore.pyqtSignal()
    clearConstraintsRequested = QtCore.pyqtSignal()
    deleteFromAllRequested = QtCore.pyqtSignal()
    deselectResiduesClicked = QtCore.pyqtSignal()
    duplicateAsRefSeqRequested = QtCore.pyqtSignal()
    duplicateAtBottomRequested = QtCore.pyqtSignal()
    duplicateAtTopRequested = QtCore.pyqtSignal()
    duplicateInPlaceRequested = QtCore.pyqtSignal()
    duplicateIntoNewTabRequested = QtCore.pyqtSignal()
    findHomologsClicked = QtCore.pyqtSignal()
    findInListRequested = QtCore.pyqtSignal()
    mvSeqsClicked = QtCore.pyqtSignal(object)
    renameSeqClicked = QtCore.pyqtSignal()
    renameAlnSetClicked = QtCore.pyqtSignal(str)
    selectAlnSetClicked = QtCore.pyqtSignal(str)
    deselectAlnSetClicked = QtCore.pyqtSignal(str)
    dissolveAlnSetClicked = QtCore.pyqtSignal(str)
    gatherAlnSetsClicked = QtCore.pyqtSignal()
    rmSeqsClicked = QtCore.pyqtSignal()
    exportSequencesRequested = QtCore.pyqtSignal()
    hideSeqsRequested = QtCore.pyqtSignal()
    selectRowResidues = QtCore.pyqtSignal()
    setAsReferenceSeq = QtCore.pyqtSignal()
    sortRequested = QtCore.pyqtSignal(object, bool)
    getPdbStClicked = QtCore.pyqtSignal()
    unlinkFromEntryRequested = QtCore.pyqtSignal()
    linkToEntryRequested = QtCore.pyqtSignal()
    alnSetCreateRequested = QtCore.pyqtSignal()
    alnSetAddRequested = QtCore.pyqtSignal(str)
    alnSetRemoveRequested = QtCore.pyqtSignal()
    translateDnaRnaRequested = QtCore.pyqtSignal()
[docs]    def __init__(self, alignment_view, parent=None):
        # See parent class for argument documentation.
        super(AlignmentInfoView, self).__init__(alignment_view, parent=parent)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setSelectionBehavior(self.SelectRows)
        self.setSelectionMode(self.ExtendedSelection)
        self.setDragDropMode(self.InternalMove)
        self.setDefaultDropAction(Qt.MoveAction)
        self.setMouseTracking(True)
        self.entered.connect(self._setMouseOverIndex)
        self._drag_allowed = False
        self._drag_started = False
        self._mouse_press_pos = None
        self._auto_scroll_timer = QtCore.QTimer(self)
        self._auto_scroll_timer.timeout.connect(self._doAutoScroll)
        self._auto_scroll_count = 0
        self._adjust_title_column_timer = self._initAdjustTitleColumnTimer()
        self._initContextMenus() 
    def _initAdjustTitleColumnTimer(self):
        # hook for patching
        timer = QtCore.QTimer(self)
        timer.setSingleShot(True)
        timer.setInterval(0)
        timer.timeout.connect(self._adjustTitleColumn)
        return timer
[docs]    @QtCore.pyqtSlot(QtCore.QAbstractItemModel)
    def setModel(self, model):
        # See Qt documentation for method documentation
        old_model = self.model()
        if old_model is not None:
            for signal_name in self.ADJUST_TIMER_SIGNALS:
                getattr(old_model, signal_name).disconnect(
                    self._adjust_title_column_timer.start)
        super(AlignmentInfoView, self).setModel(model)
        if model is not None:
            # The expand columns display an icon with a fixed width.
            self.setColumnWidth(model.Column.Expand, 20)
            self.setColumnWidth(model.Column.Struct, 20)
            char_width = self.fontMetrics().width('W')
            self.setColumnWidth(model.Column.Chain, char_width * 3)
            # Stretch the title column to fill width of the table, so that
            # when user moves the slider, this column adjusts its width.
            self.header().setSectionResizeMode(model.Column.Title,
                                               QtWidgets.QHeaderView.Stretch)
            selection_model = SequenceSelectionProxyModel(model, self)
            self.setSelectionModel(selection_model)
            # We use _update_cell_size_timer to avoid unnecessary repetition of
            # _adjustTitleColumn.
            for signal_name in self.ADJUST_TIMER_SIGNALS:
                getattr(model, signal_name).connect(
                    self._adjust_title_column_timer.start)
            # Even though we can click on the structure column to select,
            # we don't actually want to paint it as selected even when it is
            self._selectable_cols.remove(model.Column.Struct) 
    def _initContextMenus(self):
        """
        Set up context menus for the view
        """
        title_menu = SeqTitleContextMenu(parent=self)
        title_menu.sortRequested.connect(self.sortRequested)
        title_menu.findInListRequested.connect(self.findInListRequested)
        self._title_context_menu = title_menu
        chain_menu = SeqChainContextMenu(parent=self)
        chain_menu.sortRequested.connect(self.sortRequested)
        self._chain_context_menu = chain_menu
        seq_menu = AlignmentInfoContextMenu(parent=self)
        # yapf: disable
        seq_menu.deleteRequested.connect(self.rmSeqsClicked)
        seq_menu.exportSeqsRequested.connect(self.exportSequencesRequested)
        seq_menu.hideRequested.connect(self.hideSeqsRequested)
        seq_menu.deselectAllResiduesRequested.connect(self.deselectResiduesClicked)
        seq_menu.duplicateAsRefSeqRequested.connect(self.duplicateAsRefSeqRequested)
        seq_menu.duplicateAtBottomRequested.connect(self.duplicateAtBottomRequested)
        seq_menu.duplicateAtTopRequested.connect(self.duplicateAtTopRequested)
        seq_menu.duplicateInPlaceRequested.connect(self.duplicateInPlaceRequested)
        seq_menu.duplicateIntoNewTabRequested.connect(self.duplicateIntoNewTabRequested)
        seq_menu.moveSeqRequested.connect(self.mvSeqsClicked)
        seq_menu.getStructurePdbRequested.connect(self.getPdbStClicked)
        seq_menu.unlinkFromEntryRequested.connect(self.unlinkFromEntryRequested)
        seq_menu.linkToEntryRequested.connect(self.linkToEntryRequested)
        seq_menu.renameRequested.connect(self.renameSeqClicked)
        seq_menu.selectAllResiduesRequested.connect(self.selectRowResidues)
        seq_menu.setAsReferenceSeqRequested.connect(self.setAsReferenceSeq)
        seq_menu.sortRequested.connect(self.sortRequested)
        seq_menu.alnSetCreateRequested.connect(self.alnSetCreateRequested)
        seq_menu.alnSetAddRequested.connect(self.alnSetAddRequested)
        seq_menu.alnSetRemoveRequested.connect(self.alnSetRemoveRequested)
        seq_menu.translateDnaRnaRequested.connect(self.translateDnaRnaRequested)
        # yapf: enable
        self.seq_context_menu = seq_menu
        aln_set_menu = AlignmentSetContextMenu(parent=self)
        aln_set_menu.renameRequested.connect(self.renameAlnSetClicked)
        aln_set_menu.selectRequested.connect(self.selectAlnSetClicked)
        aln_set_menu.deselectRequested.connect(self.deselectAlnSetClicked)
        aln_set_menu.dissolveRequested.connect(self.dissolveAlnSetClicked)
        aln_set_menu.gatherRequested.connect(self.gatherAlnSetsClicked)
        self._aln_set_context_menu = aln_set_menu
        anno_menu = AnnotationContextMenu(parent=self)
        anno_menu.clearRequested.connect(self.clearAnnotationRequested)
        anno_menu.clearConstraintsRequested.connect(
            self.clearConstraintsRequested)
        anno_menu.deleteFromAllRequested.connect(self.deleteFromAllRequested)
        anno_menu.selectAssociatedResiduesRequested.connect(
            self.selectBindingSiteResidues)
        self._annotation_context_menu = anno_menu
        structure_menu = StructureColumnContextMenu(parent=self)
        self._structure_context_menu = structure_menu
        # Any menu shown over a row with a hover effect (all rows except title,
        # ruler, and spacer) should be in this tuple
        hover_effect_menus = (self.seq_context_menu, self._aln_set_context_menu,
                              self._annotation_context_menu,
                              self._structure_context_menu)
        for menu in hover_effect_menus:
            menu.aboutToHide.connect(self._clearContextMenuIndex)
    def _getSeqAndLig(self):
        """
        Get the sequence and the ligand name on which the right menu is showed
        up.
        :return: Sequence and the ligand name
        :rtype: tuple(protein.sequence.ProteinSequence, str)
        """
        menu_pos = self.mapFromGlobal(self._annotation_context_menu.pos())
        index = self.indexAt(menu_pos)
        ligand = index.data(Qt.DisplayRole)
        current_sequence = index.data(CustomRole.Seq)
        return current_sequence, ligand
[docs]    def selectBindingSiteResidues(self):
        """
        Select all residues that are in contact with the ligand.
        """
        aln = self.model().getAlignment()
        current_sequence, ligand = self._getSeqAndLig()
        seq_eid = current_sequence.entry_id
        if not ligand or not seq_eid:
            return
        aln.selectBindingSitesForLigand(ligand, seq_eid) 
    def _adjustTitleColumn(self):
        """
        Slot that resizes the title column in response to model changes
        """
        model = self.model()
        width = model.getTitleColumnWidth()
        self.setColumnWidth(model.Column.Title, width)
        self.updateGeometry()
    def _groupByChanged(self, group_by):
        # See SeqExpansionViewMixin for documentation
        super(AlignmentInfoView, self)._groupByChanged(group_by)
        allow_expansion = group_by is viewconstants.GroupBy.Sequence
        self.setRootIsDecorated(allow_expansion)
[docs]    def selectionCommand(self, index, event=None):
        """
        See Qt documentation for method documentation.  We modify standard
        behavior here in two cases:
        - Prevent selection changes on right mouse button events on already
          selected rows since they show the context menu.
        - If the user clicks on an already selected row, we don't know if they
          want to start a drag-and-drop operation or select-only the clicked
          row.  As such, we return NoUpdate. QAbstractItemModel handles NoUpdate
          on mouse press as a special case and calls this method again to
          potentially change selection on mouse release.  When that happens, we
          select-only the row iff drag-and-drop wasn't started.
        """
        if isinstance(event, QtGui.QMouseEvent):
            if index.internalId() != TOP_LEVEL:
                return QtCore.QItemSelectionModel.NoUpdate
            if event.button() == Qt.RightButton:
                # Qt doesn't create context menus for right-clicks with
                # modifiers so we do it ourselves here.
                self._showContextMenu(event.pos())
                sel_model = self.model().getAlignment().seq_selection_model
                if not sel_model.hasSelection():
                    return QtCore.QItemSelectionModel.ClearAndSelect
                elif event.modifiers() == Qt.ShiftModifier:
                    return QtCore.QItemSelectionModel.SelectCurrent
                elif event.modifiers() == Qt.ControlModifier:
                    return QtCore.QItemSelectionModel.Select
                elif index.data(CustomRole.SeqSelected):
                    return QtCore.QItemSelectionModel.NoUpdate
            elif self._drag_allowed:
                # _drag_allowed is set in mousePressEvent when the user
                # left-clicks on a draggable row
                if event.type() == event.MouseButtonPress:
                    if index.data(CustomRole.SeqSelected):
                        return QtCore.QItemSelectionModel.NoUpdate
                    else:
                        return QtCore.QItemSelectionModel.ClearAndSelect
                elif event.type() == event.MouseButtonRelease:
                    if self._drag_started:
                        return QtCore.QItemSelectionModel.NoUpdate
                    else:
                        return QtCore.QItemSelectionModel.ClearAndSelect
        return super().selectionCommand(index, event) 
    def _showContextMenu(self, pos):
        model = self.model()
        index = self.indexAt(pos)
        column = index.column()
        if column == model.Column.Expand:
            # There are no context menus for the expansion column
            return
        if index.row() == 0 and index.internalId() == TOP_LEVEL:
            if column == model.Column.Title:
                self._title_context_menu.popup(self.mapToGlobal(pos))
            elif column == model.Column.Chain:
                self._chain_context_menu.popup(self.mapToGlobal(pos))
            # There are no context menus for the struct header column
            return
        menu = None
        row_type = index.data(CustomRole.RowType)
        if row_type is PROT_SEQ_ANN_TYPES.alignment_set:
            menu = self._getAlnSetContextMenu(index)
        elif self._clickedAnnotation(index):
            menu = self._getAnnotationContextMenu(index)
        elif row_type is RowType.Sequence:
            clicked_seq = index.data(CustomRole.Seq)
            if (column == model.Column.Struct and clicked_seq and
                    clicked_seq.hasStructure()):
                aln = model.getAlignment()
                sel_seqs = aln.getSelectedSequences()
                menu = self._getStructureColumnContextMenu(
                    clicked_seq, sel_seqs)
            else:
                menu = self._getSeqContextMenu(clicked_seq)
        if menu is not None:
            model.setMouseOverIndex(None)
            model.setContextOverIndex(index)
            menu.popup(self.mapToGlobal(pos))
    @QtCore.pyqtSlot()
    def _clearContextMenuIndex(self):
        """
        Slot to clear the index the context menu is over. All context menus
        shown in `_showContextMenu` (except title row menus) must connect
        `aboutToHide` to this slot.
        """
        self.model().setContextOverIndex(None)
    def _clickedAnnotation(self, index):
        row_type = index.data(CustomRole.RowType)
        return row_type is not None and row_type != PROT_ALN_ANN_TYPES.indices and (
            row_type in PROT_SEQ_ANN_TYPES or row_type in PROT_ALN_ANN_TYPES)
    def _getStructureColumnContextMenu(self, clicked_seq, sel_seqs):
        if not maestro:
            return
        context_menu = self._structure_context_menu
        context_menu.setSelectedSeqs(sel_seqs)
        is_included = (clicked_seq.visibility
                       is not viewconstants.Inclusion.Excluded)
        context_menu.setIncluded(is_included)
        return context_menu
    def _getAlnSetContextMenu(self, index):
        context_menu = self._aln_set_context_menu
        aln = self.model().getAlignment()
        seq = index.data(CustomRole.Seq)
        set_name = aln.alnSetForSeq(seq).name
        context_menu.set_name = set_name
        return context_menu
    def _getAnnotationContextMenu(self, index):
        context_menu = self._annotation_context_menu
        context_menu.annotation = index.data(CustomRole.RowType)
        aln = self.model().getAlignment()
        sel_anns = aln.ann_selection_model.getSelection()
        context_menu.setSelectedAnnotations(sel_anns)
        return context_menu
    def _getSeqContextMenu(self, clicked_seq):
        context_menu = self.seq_context_menu
        if clicked_seq is not None:
            context_menu.updateUnlinkedFromSequences(clicked_seq.hasStructure())
            can_translate = isinstance(clicked_seq,
                                       sequence.NucleicAcidSequence)
            context_menu.updateCanTranslate(can_translate)
            aln = self.model().getAlignment()
            seq_set = aln.alnSetForSeq(clicked_seq)
            context_menu.createAlnSetMenu(in_set=bool(seq_set))
            set_name = seq_set.name if seq_set else None
            context_menu = self._disableCurrentSetName(context_menu, set_name)
        return context_menu
    def _disableCurrentSetName(self, context_menu, set_name):
        """
        Disable the set in the "Add to Set" sub-menu, if the clicked sequence
        belong to the set and return the menu.
        :param context_menu: Menu from which the QAction needs to be disabled.
        :type context_menu: AlignmentInfoContextMenu
        :param set_name: Name of the set to be disabled or None if clicked seq
            doesn't belong to any set.
        :type set_name: str or None
        :return: AlignmentInfoContextMenu
        """
        aln = self.model().getAlignment()
        selected_seqs = aln.seq_selection_model.getSelection()
        unique_selected_seq_set = set()
        for seq in selected_seqs:
            seq_set = aln.alnSetForSeq(seq)
            if seq_set is not None:
                unique_selected_seq_set.add(seq_set.name)
        aln_has_single_set = len(unique_selected_seq_set) == 1
        aln_set_menu = context_menu.aln_set.menu()
        # 'Add to Set' menu-item is at index 2.
        aln_set_add_action = aln_set_menu.actions()[2]
        aln_set_add_menu = aln_set_add_action.menu()
        for action in aln_set_add_menu.actions():
            disable = aln_has_single_set and action.text() == set_name
            action.setDisabled(disable)
        return context_menu
    @QtCore.pyqtSlot(QtCore.QModelIndex)
    def _setMouseOverIndex(self, index):
        """
        Set the given index as having the mouse over it
        """
        self.model().setMouseOverIndex(index)
    def _isMouseOverIndex(self, index):
        """
        Check whether the mouse is over the specified row
        :param index: An index representing the row to check
        :type index: QtCore.QModelIndex
        :return: A tuple of:
            - Whether the specified row has the mouse over it.  Note that, for
            the purposes of this check, mousing over the expansion column does
            *not* count as mousing over the row.
            - Whether the mouse is over the structure column of *any* row.
        :rtype: tuple(bool, bool)
        """
        model = self.model()
        mouse_over_row = model.isMouseOverIndex(index)
        mouse_over_struct_col = model.isMouseOverStructCol()
        return mouse_over_row, mouse_over_struct_col
[docs]    def mousePressEvent(self, event):
        """
        See Qt documentation for method documentation. We modify standard
        behavior here in two cases:
        - On right-clicks, we show the appropriate context menu.
        - On left-clicks, we check to see if drag-and-drop is possible and, if
          so, update self._drag_allowed and self._mouse_press_pos.
        """
        index = self.indexAt(event.pos())
        ann_handled = self._handleMousePressOnAnn(index, event)
        if ann_handled:
            return
        if (event.button() == Qt.LeftButton and
                event.modifiers() == Qt.NoModifier and
                index.flags() & Qt.ItemIsDragEnabled):
            self._drag_allowed = True
            self._mouse_press_pos = event.pos()
        super().mousePressEvent(event) 
[docs]    def mouseMoveEvent(self, event):
        """
        See Qt documentation for method documentation.  Since our selection
        model doesn't store the selection (see `AbstractSelectionProxyModel`),
        `QAbstractItemModel::mouseMoveEvent` always thinks that there's no
        selection and will never start drag-and-drop.  To get around this, we
        manually set the state to `DraggingState` when appropriate.  (Note that
        DraggingState doesn't actually start a drag-and-drop operation. Instead,
        drag-and-drop starts when::
          - state is already set to `DraggingState`
          - the mouse has moved at least `QApplication::startDragDistance()`
          pixels
        """
        if (event.buttons() and self._drag_allowed and
                self.state() == self.NoState):
            self.setState(self.DraggingState)
        super().mouseMoveEvent(event) 
[docs]    def mouseReleaseEvent(self, event):
        # See Qt documentation for method documentation
        super().mouseReleaseEvent(event)
        self._resetDragAndDropState() 
[docs]    def mouseDoubleClickEvent(self, event):
        # See Qt documentation for method documentation
        super().mouseDoubleClickEvent(event)
        index = self.indexAt(event.pos())
        if (index.data(CustomRole.RowType) is RowType.Sequence and
                index.column() == self.model().Column.Title):
            self.selectionModel().select(
                index, QtCore.QItemSelectionModel.ClearAndSelect)
            self.renameSeqClicked.emit() 
[docs]    def leaveEvent(self, event):
        # See Qt documentation for method documentation
        self.model().setMouseOverIndex(None)
        super().leaveEvent(event) 
    def _handleMousePressOnAnn(self, index, event):
        """
        :return: Whether the event was handled and the caller should return early
        """
        ann_sel_model = self.model().getAlignment().ann_selection_model
        row_type = index.data(CustomRole.RowType)
        if not index.flags() & Qt.ItemIsSelectable:
            # For non-selectable rows, clear selection but don't handle event
            ann_sel_model.clearSelection()
            return False
        if row_type not in viewmodel.SELECTABLE_ANNO_TYPES:
            # For non-annotation rows, don't handle event
            return False
        selection_state = None
        if ann_sel_model.hasSelection():
            ctrl_pressed = event.modifiers() == Qt.ControlModifier
            if index.data(CustomRole.AnnotationSelected):
                if event.button() == Qt.RightButton:
                    # Prevent selection event when right-clicking on
                    # already-selected row
                    return True
                elif ctrl_pressed:
                    # Deselect
                    selection_state = False
            elif ctrl_pressed or event.modifiers() == Qt.ShiftModifier:
                # TODO MSV-3289 implement range selection for shift
                # Select-also
                selection_state = True
        if selection_state is None:
            # If behavior was not customized by a modifier, clear and select
            ann_sel_model.clearSelection()
            selection_state = True
        self.model().setAnnSelectionState(index, selection_state)
        return True
    def _resetDragAndDropState(self):
        """
        Reset any internal variables used to keep track of drag-and-drop state.
        """
        self._drag_allowed = False
        self._drag_started = False
        self._mouse_press_pos = None
[docs]    def startDrag(self, supported_actions):
        # See Qt documentation for method documentation
        self._drag_started = True
        model = self.model()
        drag = QtGui.QDrag(self)
        # the model already knows what the selection is, so we don't have to
        # pass a list of indices to mimeData
        mime_data = model.mimeData([])
        try:
            pixmap, rect = self._getDragImageAndRect()
        except NoDragRowsError:
            # This should never happen, but it sometimes does when the user
            # clicks and drags from the reference sequence.  (That row isn't
            # flagged as draggable, so the view should never enter
            # DraggingState.)  See also MAE-37388, which is the equivalent
            # problem occurring in the HPT.
            return
        hotspot = self.DRAG_IMG_SCALE * \
            
(self._mouse_press_pos - rect.topLeft())
        drag.setMimeData(mime_data)
        drag.setPixmap(pixmap)
        drag.setHotSpot(hotspot)
        default_drop_option = self.defaultDropAction()
        drag.exec_(supported_actions, default_drop_option)
        # mouseReleaseEvent may or may not get called after the drop.  (See
        # https://bugreports.qt.io/browse/QTBUG-40733.)  Since drag.exec_ blocks
        # until the drop happens, we reset the drag-and-drop variables here in
        # case mouseReleaseEvent isn't called.
        self._resetDragAndDropState() 
    def _getDragImageAndRect(self):
        """
        Create an image for the rows that are about to be dragged.
        :return: A tuple of
            - The image of all selected rows that are currently visible.
            - A rectangle of the area represented by the image
        :rtype: tuple(QtGui.QPixmap, QtCore.QRect)
        """
        selected = self._getDragRows()
        if not selected:
            # This means that the user has started drag-and-drop without any
            # selected rows visible, which shouldn't happen
            raise NoDragRowsError
        model = self.model()
        first_col = model.Column.Struct
        last_col = model.Column.Chain
        rect = self._getDragRect(selected, first_col, last_col)
        pixmap = self._getDragImage(selected, rect, first_col, last_col)
        return pixmap, rect
    def _getDragRows(self):
        """
        Figure out which rows we should draw for the drag-and-drop image.  We
        draw any currently visible rows that are selected other than the
        reference sequence.  We include annotation rows when in
        group-by-sequence mode (since annotations are adjacent to the selected
        sequence), but not when in group-by-type mode.
        :return: A list of indices, each representing a row to draw.
        :rtype: list[QtCore.QModelIndex]
        """
        rect = self.viewport().rect()
        bottom_index = self.indexAt(rect.bottomLeft())
        cur_index = self.indexAt(QtCore.QPoint(1, 1))
        selected = []
        while cur_index.isValid():
            if cur_index.data(CustomRole.IncludeInDragImage):
                selected.append(cur_index)
            if cur_index == bottom_index:
                break
            cur_index = self.indexBelow(cur_index)
        return selected
    def _getDragRect(self, selected, first_col, last_col):
        """
        Create a QRect for the drag-and-drop image.
        :param selected: A list of indices for each row to draw
        :type selected: list[QtCore.QModelIndex]
        :param first_col: The left-most column to be drawn
        :type first_col: int
        :param last_col: The right-most column to be drawn
        :type last_col: int
        :return: The requested QRect
        :rtype: QtCore.QRect
        """
        top_index = selected[0]
        top_index = top_index.sibling(top_index.row(), first_col)
        topleft = self.visualRect(top_index).topLeft()
        bottom_index = selected[-1]
        bottom_index = bottom_index.sibling(bottom_index.row(), last_col)
        bottomright = self.visualRect(bottom_index).bottomRight()
        return QtCore.QRect(topleft, bottomright)
    def _getDragImage(self, selected, img_rect, first_col, last_col):
        """
        Draw the drag-and-drop image.
        :param selected: A list of indices for each row to draw
        :type selected: list[QtCore.QModelIndex]
        :param img_rect:
        :type img_rect: QtCore.QRect
        :param first_col: The left-most column to draw
        :type first_col: int
        :param last_col: The right-most column to draw
        :type last_col: int
        :return: The drag-and-drop image
        :rtype: QtGui.QPixmap
        """
        pixmap = QtGui.QPixmap(img_rect.size())
        pixmap.fill(Qt.transparent)
        painter = QtGui.QPainter(pixmap)
        painter.setOpacity(self.DRAG_IMG_OPACITY)
        painter.scale(self.DRAG_IMG_SCALE, self.DRAG_IMG_SCALE)
        option = self.viewOptions()
        row_rect = QtCore.QRect(img_rect)
        row_rect.moveLeft(img_rect.left())
        img_rect_top = img_rect.top()
        for cur_index in selected:
            with self._paintingColumns(first_col, last_col):
                index_rect = self.visualRect(cur_index)
                row_rect.setTop(index_rect.top() - img_rect_top)
                row_rect.setBottom(index_rect.bottom() - img_rect_top)
                option.rect = row_rect
                self.drawRow(painter, option, cur_index)
        return pixmap
[docs]    def dragEnterEvent(self, event):
        # See Qt documentation for method documentation
        # This implementation is based on the QAbstractItemView::dragEnterEvent
        # implementation
        if event.source() is self:
            self.setState(self.DraggingState)
            event.accept()
        else:
            event.ignore() 
[docs]    def dragMoveEvent(self, event):
        # See Qt documentation for method documentation
        # This implementation is based on the QAbstractItemView::dragMoveEvent
        # implementation
        if event.source() is not self:
            event.ignore()
        pos = event.pos()
        _, drop_y = self._canDrop(pos)
        if drop_y != self._drop_indicator_y:
            self.viewport().update()
            self._drop_indicator_y = drop_y
        if drop_y is None:
            event.ignore()
        else:
            event.acceptProposedAction()
        self._startAutoScrollIfNeeded(pos) 
[docs]    def dragLeaveEvent(self, event):
        # See Qt documentation for method documentation
        # This implementation is based on the QAbstractItemView::dragLeaveEvent
        # implementation
        self._stopAutoScroll()
        self.setState(self.NoState)
        if self._drop_indicator_y is not None:
            self._drop_indicator_y = None
            self.viewport().update() 
[docs]    def dropEvent(self, event):
        # See Qt documentation for method documentation
        # This implementation is based on the QAbstractItemView::dropEvent
        # implementation
        self._stopAutoScroll()
        self.setState(self.NoState)
        if self._drop_indicator_y is not None:
            self._drop_indicator_y = None
            self.viewport().update()
        index, _ = self._canDrop(event.pos())
        if index is not None:
            event.acceptProposedAction()
            self.model().moveSelectionBelow(index)
        self._syncExpansion() 
    def _canDrop(self, pos):
        """
        Determine if we can drop drag-and-dropped rows at the specified point.
        :note: The standard Qt model/view method for determining where rows can
            be dropped is to check flags(index) & Qt.ItemIsDragEnabled.
            However, due to the way this is implemented, if we can drop between
            two rows, then 1) we must be able to drop between any two rows in
            that same group 2) we must be able to drop on the parent index of
            that group Those restrictions are unworkable here, since we need to
            be able to drop in between sequence rows but not on sequence rows
            and not in between global annotation (which are also top level
            rows). As a result, this method relies on two custom roles:
            CanDropAbove and CanDropBelow.
        :param pos: The location to check
        :type pos: QtCore.QPoint
        :return: A tuple of::
          - If we can drop, the index that the drop would occur below.  None
          otherwise.
          - If we can drop, the y-coordinate to draw the drop indicator at.
          None otherwise.
        :rtype: tuple(QtCore.QModelIndex or None, int or None)
        """
        index = self.indexAt(pos)
        if not index.isValid():
            # We're below the bottom of the last row, so see if we can drop
            # below the last row
            model = self.model()
            last_index = model.index(model.rowCount() - 1, 0)
            child_row_count = model.rowCount(last_index)
            if child_row_count and self.isExpanded(last_index):
                last_index = model.index(child_row_count - 1, 0, last_index)
            if last_index.data(CustomRole.CanDropBelow):
                rect = self.visualRect(last_index)
                return last_index, rect.bottom() + 1
            return None, None
        rect = self.visualRect(index)
        if pos.y() < rect.center().y():
            # The cursor is over the top half of the row
            prev_index = self.indexAbove(index)
            if (index.data(CustomRole.CanDropAbove) or
                    self._canDropBelow(prev_index)):
                # We can drop above this row
                return prev_index, rect.top()
            elif index.data(CustomRole.RowType) is RowType.Spacer:
                # If this row is a spacer and we can't drop above it, see if we
                # can drop below it.
                next_index = self.indexBelow(index)
                if (self._canDropBelow(index) or
                        next_index.data(CustomRole.CanDropAbove)):
                    return index, rect.bottom() + 1
            elif prev_index.data(CustomRole.RowType) is RowType.Spacer:
                # If the row above this one is a spacer, see if we can drop
                # above that row.
                prev_prev_index = self.indexAbove(prev_index)
                if (prev_index.data(CustomRole.CanDropAbove) or
                        self._canDropBelow(prev_prev_index)):
                    prev_rect = self.visualRect(prev_index)
                    return prev_prev_index, prev_rect.top()
        else:
            # The cursor is over the bottom half of the row
            if self._canDropBelow(index):
                return index, rect.bottom() + 1
            next_index = self.indexBelow(index)
            if next_index.data(CustomRole.CanDropAbove):
                return index, rect.bottom() + 1
            elif index.data(CustomRole.RowType) is RowType.Spacer:
                # If this row is a spacer and we can't drop below it, see if we
                # can drop above it.
                prev_index = self.indexAbove(index)
                if (index.data(CustomRole.CanDropAbove) or
                        self._canDropBelow(prev_index)):
                    return prev_index, rect.top()
            elif next_index.data(CustomRole.RowType) is RowType.Spacer:
                # If the row below this one is a spacer, see if we can drop
                # below that row.
                next_next_index = self.indexBelow(next_index)
                if (self._canDropBelow(next_index) or
                        next_next_index.data(CustomRole.CanDropAbove)):
                    next_rect = self.visualRect(next_index)
                    return next_index, next_rect.bottom() + 1
        # We can't drop anywhere nearby
        return None, None
    def _canDropBelow(self, index):
        """
        See if we can drop drag-and-dropped rows below the specified row.  Note
        that we can never drop below an expanded group, since that would mean
        that we're dropping in between a sequence and its annotations.
        :param index: An index in the row to check.
        :type index: QtCore.QModelIndex
        :return: Whether below the row is a valid drop target.
        :rtype: bool
        """
        model = self.model()
        # isExpanded only works for column 0 indices:
        index = index.sibling(index.row(), 0)
        if model.hasChildren(index) and self.isExpanded(index):
            return False
        return model.data(index, CustomRole.CanDropBelow)
    def _startAutoScrollIfNeeded(self, pos):
        """
        Start auto-scroll (i.e. scrolling while in drag-and-drop mode when the
        mouse cursor is close to the edges of the view) if the specified
        position is close enough to the view margins.
        :param pos: The cursor location.
        :type pos: QtCore.QPoint
        """
        y = pos.y()
        margin = self.autoScrollMargin()
        if y < margin or y > self.height() - margin:
            # The 150 value is taken from
            # QAbstractItemViewPrivate::startAutoScroll
            self._auto_scroll_timer.start(150)
            self._auto_scroll_count = 0
    def _stopAutoScroll(self):
        self._auto_scroll_timer.stop()
        self._auto_scroll_count = 0
    def _doAutoScroll(self):
        """
        If the cursor is currently close to the view margins, auto-scroll the
        view.  Activated during drag-and-drop.  Note that this implementation is
        based on QAbstractItemView::doAutoScroll.
        """
        scroll = self.verticalScrollBar()
        # auto-scroll gradually accelerates up to a max of a page at a time
        if self._auto_scroll_count < scroll.pageStep():
            self._auto_scroll_count += 1
        cursor_pos = QtGui.QCursor.pos()
        pos = self.viewport().mapFromGlobal(cursor_pos)
        y = pos.y()
        margin = self.autoScrollMargin()
        if y < margin:
            scroll.setValue(scroll.value() - self._auto_scroll_count)
        elif y > self.height() - margin:
            scroll.setValue(scroll.value() + self._auto_scroll_count)
        else:
            self._stopAutoScroll()
            return
        # don't show the drop indicator while scrolling
        if self._drop_indicator_y is not None:
            self._drop_indicator_y = None
            self.viewport().update()
[docs]    def getSizeHint(self):
        # Enough to show title and chain header. User can then
        # use the splitter to expose more of the title as desired.
        return self.model().getMinimumWidth() 
[docs]    def resizeEvent(self, event):
        """
        Clear the text cache when the view is resized (will re-calculate
        eliding).
        """
        self._row_del.clearCache()
        super().resizeEvent(event)  
[docs]class NoDragRowsError(Exception):
    """
    An exception raised when the user tries to drag-and-drop but there are no
    selected rows.  This should never happen, but it does.  See
    `AlignmentInfoView.startDrag`.
    """ 
    # This class intentionally left blank
[docs]class EditorDelegate(QtWidgets.QStyledItemDelegate):
    """
    A delegate for editing residues.  This delegate is *not* involved in
    painting, as all painting is handled by the `row_delegates` module.
    :cvar GAP: The gap character to pass to the model.
    :vartype GAP: str
    :cvar INSERTION_EDITOR_WIDTH: How wide (in number of cells) we should make
        the insertion editor.
    :vartype INSERTION_EDITOR_WIDTH: int or float
    :cvar KEY_TO_ADJACENT: A mapping of Qt key constants to `Adjacent`
        constants.
    :vartype KEY_TO_ADJACENT: dict(Qt.Key, Adjacent)
    :cvar moveChangeEditorRequested: A signal emitted when the user has
        requested that the change residue editor be moved to a new cell. Emitted
        with an `Adjacent` value corresponding to the direction to move the
        editor.
    :vartype moveChangeEditorRequested: QtCore.pyqtSignal
    """
    GAP = protein_constants.GAP_CHARS[0]
    INSERTION_EDITOR_WIDTH = 5
    KEY_TO_ADJACENT = {
        Qt.Key_Up: Adjacent.Up,
        Qt.Key_Down: Adjacent.Down,
        Qt.Key_Left: Adjacent.Left,
        Qt.Key_Right: Adjacent.Right
    }
    moveChangeEditorRequested = QtCore.pyqtSignal(Adjacent)
[docs]    def __init__(self, parent):
        """
        :param parent: Parent view. Must be `AbstractAlignmentView` because
            setModelData calls parent methods.
        :type parent: AbstractAlignmentView
        """
        super().__init__(parent)
        self._editor_type = _EditorType.Change
        self._num_replacement_res = None 
[docs]    def setEditorType(self, mode, num_replacement_res=None):
        """
        Specify what type of editor should be created the next time we edit a
        cell.
        :param mode: What type of editor to create.
        :type mode: _EditorType
        :param num_replacement_res: If `mode` is `_EditorType.Replacement`, how
            many cells wide the editor should be.  Otherwise, should be `None`.
        :type num_replacement_res: int or None
        """
        self._editor_type = mode
        self._num_replacement_res = num_replacement_res 
[docs]    def createEditor(self, parent, option, index):
        # See Qt documentation for method documentation
        editor = QtWidgets.QLineEdit(parent)
        if self._editor_type is _EditorType.Change:
            editor.setStyleSheet("border: 1px solid blue")
            editor.setMaxLength(1)
            editor.setPlaceholderText(viewconstants.DEFAULT_GAP)
        # create a copy of the font so we don't modify the model's font
        font = QtGui.QFont(index.data(Qt.FontRole))
        # make the font slightly smaller so it fits in the editor better
        font.setPointSize(font.pointSize() - 2)
        editor.setFont(font)
        # raise the text a pixel higher to make up for the fact that we shrank
        # it
        editor.setTextMargins(0, 0, 0, 1)
        editor.textEdited.connect(self._convertSpacesToGaps)
        return editor 
[docs]    def updateEditorGeometry(self, editor, option, index):
        # See Qt documentation for method documentation
        if self._editor_type is _EditorType.Change:
            rect = QtCore.QRect(option.rect)
        elif self._editor_type is _EditorType.Replace:
            rect = QtCore.QRect(option.rect)
            rect.setWidth(rect.width() * self._num_replacement_res)
        elif self._editor_type is _EditorType.Insert:
            width = option.rect.width() * self.INSERTION_EDITOR_WIDTH
            height = option.rect.height()
            x = option.rect.left() - width // 2
            if x < 0:
                # if the editor would extend off the left edge of the view, move
                # it to the right
                x = 0
            elif x + width > editor.parent().width():
                # if the editor would extend off the right edge of the view,
                # move it to the left
                x = editor.parent().width() - width
            y = option.rect.top() - height
            if y < 0:
                # if the editor starts above the top of the view, move it below
                # the row being edited
                y += 2 * height
            rect = QtCore.QRect(x, y, width, height)
        # make the editor a little larger than the cell so it's more visible
        rect += QtCore.QMargins(2, 2, 2, 2)
        editor.setGeometry(rect) 
    def _convertSpacesToGaps(self, text):
        """
        Convert all spaces in the editor to mid-dots as the user types.  May
        only be called as a slot.
        :param text: The new contents of the editor.
        :type text: str
        """
        if " " in text:
            editor = self.sender()
            cursor_position = editor.cursorPosition()
            new_text = text.replace(" ", viewconstants.DEFAULT_GAP)
            editor.setText(new_text)
            editor.setCursorPosition(cursor_position)
[docs]    def setEditorData(self, editor, index):
        # See Qt documentation for method documentation
        if self._editor_type is _EditorType.Change:
            res = index.data(Qt.EditRole)
            editor.setText(res)
        elif self._editor_type is _EditorType.Replace:
            text = index.model().getSingleLetterCodeForSelectedResidues()
            text = text.replace(" ", viewconstants.DEFAULT_GAP)
            editor.setText(text) 
        # don't load any data for insertion editors
[docs]    def setModelData(self, editor, model, index):
        # See Qt documentation for method documentation
        text = editor.text()
        text = text.replace(viewconstants.DEFAULT_GAP, self.GAP)
        if self._editor_type is _EditorType.Change:
            if not text:
                text = self.GAP
            replacement = viewmodel.SeqSliceReplacement(text)
            edit_func = partial(model.setData, index, replacement,
                                CustomRole.ReplacementEdit)
        elif self._editor_type is _EditorType.Replace:
            replacement = viewmodel.SeqSliceReplacement(
                text, self._num_replacement_res)
            edit_func = partial(model.setData, index, replacement,
                                CustomRole.ReplacementEdit)
        elif self._editor_type is _EditorType.Insert:
            edit_func = partial(model.setData, index, text,
                                CustomRole.InsertionEdit)
        else:
            raise RuntimeError(f"_editor_type {self._editor_type} is invalid")
        self.parent().editAndMaybeClearAnchors(edit_func) 
[docs]    def eventFilter(self, editor, event):
        """
        Handle up, down, left, and right keypresses in the editor.  See Qt
        documentation for additional method documentation.
        Note that QAbstractItemView automatically installs the delegate as an
        event filter for the editor, which will cause this function to be called
        when required.
        """
        if (event.type() == event.KeyPress and
                event.modifiers() == Qt.NoModifier):
            key = event.key()
            if (self._editor_type is _EditorType.Change and
                    key in self.KEY_TO_ADJACENT):
                direction = self.KEY_TO_ADJACENT[key]
                self.moveChangeEditorRequested.emit(direction)
                return True
            elif key in (Qt.Key_Up, Qt.Key_Down):
                # Prevent the current index from moving up or down a row while a
                # replacement or insertion editor is open.  Left and right key
                # presses will be accepted by the line edit (to move the
                # cursor), so we don't need to filter those out.
                return True
        return super().eventFilter(editor, event)