import contextlib
import copy
import inspect
import itertools
import os
import shutil
import traceback
import weakref
import collections
import inflect
from typing import List
from typing import Tuple
import schrodinger
from schrodinger import project
from schrodinger.application.msv import command
from schrodinger.application.msv import seqio
from schrodinger.application.msv import structure_model
from schrodinger.application.msv.gui import dialogs
from schrodinger.application.msv.gui import gui_models
from schrodinger.application.msv.gui import history
from schrodinger.application.msv.gui import menu
from schrodinger.application.msv.gui import msv_widget
from schrodinger.application.msv.gui import picking
from schrodinger.application.msv.gui import stylesheets
from schrodinger.application.msv.gui import tab_widget
from schrodinger.application.msv.gui import toolbar
from schrodinger.application.msv.gui import viewconstants
from schrodinger.application.msv.gui.dendrogram_viewer import DendrogramViewer
from schrodinger.application.msv.gui.homology_modeling import homology_panel
from schrodinger.application.msv.gui.homology_modeling.hm_models import \
    VIEWNAME as HM_VIEWNAME
from schrodinger.application.msv.gui.viewconstants import ResidueFormat
from schrodinger.infra import jobhub
from schrodinger.infra import util
from schrodinger.models import json
from schrodinger.models import mappers
from schrodinger.models import parameters
from schrodinger.protein import annotation
from schrodinger.protein import sequence
from schrodinger.protein.tasks import blast
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtWidgets
from schrodinger.tasks.tasks import AbstractTask
from schrodinger.tasks.tasks import Status as TaskStatus
from schrodinger.ui import maestro_ui
from schrodinger.ui.qt import filedialog
from schrodinger.ui.qt import messagebox
from schrodinger.ui.qt import utils as qt_utils
from schrodinger.ui.qt import widgetmixins
from schrodinger.ui.qt.appframework2 import af2
from schrodinger.ui.qt.appframework2 import settings
from schrodinger.utils import documentation
from schrodinger.utils import fileutils
# The msv_rc import loads the MSV icons into Qt
from . import msv_rc  # flake8: noqa # pylint: disable=unused-import
maestro = schrodinger.get_maestro()
SEQ_ANNO_TYPES = annotation.ProteinSequenceAnnotations.ANNOTATION_TYPES
ONE_MINUTE = 60000  # Milliseconds
OPTIMAL_SEQ_COUNT = 100
OPTIMAL_RES_COUNT = 50000
[docs]class NewerProjectException(ValueError):
[docs]    def __init__(self, version, *args, **kwargs):
        self.version = version
        super().__init__(*args, **kwargs)  
[docs]class MSVPanel(widgetmixins.PanelMixin, widgetmixins.BaseMixinCollection,
               QtWidgets.QMainWindow):
    model_class = gui_models.MsvGuiModel
    _checkingForRenamedSeqs = util.flag_context_manager(
        "_checking_for_renamed_seqs")
    APPLY_LEGACY_STYLESHEET = False
[docs]    def initSetOptions(self):
        # InitMixin
        super().initSetOptions()
        self._save_file_name = None
        self.dendrogram_viewer = None
        self._last_sort_reversed = None
        self._rename_sequence_dialog = None
        self._paste_sequence_dialog = None
        self._homology_pane = None
        self._renamed_seqs = {}
        self._checking_for_renamed_seqs = False
        self._should_show_domain_download_error_local_only = True
        self._should_show_domain_download_error = True
        self.setFocusPolicy(QtCore.Qt.StrongFocus)
        self.setObjectName("MSVMainGui")
        self.setWindowTitle("Multiple Sequence Viewer/Editor")
        self._prev_maestro_working_dir = self._last_imported_dir = os.getcwd() 
[docs]    def initSetUp(self):
        # InitMixin
        super().initSetUp()
        self.get_sequences_dialog = dialogs.GetSequencesDialog(self)
        self.get_sequences_dialog.sequencesRetrieved.connect(self.importFiles)
        self.pdb_dialog = dialogs.PDBDialogWithRemoteConfirmation()
        self.menu = self._makeMenu()
        self._setUpMenu()
        self.undo_stack = command.UndoStack(self)
        self.undo_stack.canRedoChanged.connect(self.menu.canRedoChanged)
        self.undo_stack.canUndoChanged.connect(self.menu.canUndoChanged)
        self.undo_stack.undoTextChanged.connect(self.menu.onUndoTextChanged)
        self.undo_stack.redoTextChanged.connect(self.menu.onRedoTextChanged)
        self.history_widget = history.UndoWidget(self.undo_stack, self)
        self.setStructureModel(self._initStructureModel())
        self.msv_status_bar = toolbar.MSVStatusBar(self)
        self.msv_status_bar.setStyleSheet(stylesheets.MSV_STATUS_BAR)
        self.res_status_bar = toolbar.MSVResStatusBar(self)
        self.res_status_bar.setStyleSheet(stylesheets.MSV_RES_STATUS_BAR)
        self.res_status_bar.clearResSelectionRequested.connect(
            self.clearResidueSelection)
        self.config_toggles = toolbar.ConfigurationTogglesBar(self)
        self.config_toggles.setStyleSheet(stylesheets.CONFIGURATION_TOGGLES)
        self.config_toggles.options_btn.popup_dialog\
            
.openPropertyDialogRequested.connect(
            self._makeCurrentWidgetCallback("openPropertyDialog"))
        self._status_bar = QtWidgets.QStatusBar(self)
        self._status_bar.setObjectName("MSVQStatusBar")
        self._status_bar.setSizeGripEnabled(False)
        self._status_bar.addWidget(self.msv_status_bar)
        self._status_bar.addPermanentWidget(self.res_status_bar)
        self._status_bar.addPermanentWidget(self.config_toggles)
        self._task_status_bar = toolbar.TaskStatusBar()
        self.quick_annotate_dialog = self.config_toggles.annotations_btn.popup_dialog
        self.color_dialog = self.config_toggles.sequence_colors_btn.popup_dialog
        self._setUpColorDialog()
        self.view_dialog = self.config_toggles.options_btn.popup_dialog
        self.toolbar = self._initToolbar()
        self._connectToolbarSignals()
        self._tab_bar = tab_widget.TabBarWithLockableLeftTab()
        self.query_tabs = tab_widget.MSVTabWidget(
            None,
            tab_bar=self._tab_bar,
            struc_model=self._structure_model,
            undo_stack=self.undo_stack)
        # Note: Initializing MSVTabWidget with parent causes incorrect tab bar
        # close button behavior
        self.query_tabs.setParent(self)
        self.query_tabs.newWidgetCreated.connect(self._setUpNewWidget)
        self._new_tab_btn = tab_widget.NewTabBtn()
        self._new_tab_btn.clicked.connect(self.query_tabs.createNewTab)
        self.query_tabs.canAddTabChanged.connect(
            self._new_tab_btn.updateCanAddTab)
        self._tab_toolbar = QtWidgets.QToolBar()
        self._tab_toolbar.setObjectName('tab_toolbar')
        self._tab_toolbar.setFloatable(False)
        self._tab_toolbar.setMovable(False)
        self._tab_toolbar.addWidget(self._tab_bar)
        self._tab_toolbar.addWidget(self._new_tab_btn)
        self._fetch_domain_timer = QtCore.QTimer()
        self._fetch_domain_timer.setSingleShot(True)
        self._fetch_domain_timer.setInterval(0)
        self._fetch_domain_timer.timeout.connect(self._fetchDomains)
        self._save_project_timer = QtCore.QTimer()
        self._save_project_timer.setInterval(ONE_MINUTE)
        self._save_project_timer.timeout.connect(self._autosaveProject)
        self.setFocusPolicy(QtCore.Qt.ClickFocus) 
    def _saveProjectOnlyAfterEdit(self):
        save_always = not self.model.auto_save_only_on_edits
        if save_always:
            self._save_project_timer.start()
        else:
            self._save_project_timer.stop()
    def _updateDendrogramViewer(self):
        if self.dendrogram_viewer:
            page = self.model.current_page
            self.dendrogram_viewer.setAlignment(page.aln)
    def _connectToolbarSignals(self):
        # Lambda slots with references to self cause problems with garbage
        # collection.  To avoid this, we replace self with a weakref.
        self = weakref.proxy(self)
        self.toolbar.insertGaps.connect(
            self._makeCurrentWidgetCallback("insertGapsToLeftOfSelection"))
        self.toolbar.deleteGaps.connect(
            self._makeCurrentWidgetCallback("deleteSelectedGaps"))
        self.toolbar.insertResidues.connect(
            self._makeCurrentWidgetCallback("insertResidues"))
        self.toolbar.deleteSelection.connect(
            self._makeCurrentWidgetCallback("deleteSelection"))
        self.toolbar.changeResidues.connect(
            self._makeCurrentWidgetCallback("changeResidues"))
        self.toolbar.replaceSelection.connect(
            self._makeCurrentWidgetCallback("replaceSelection"))
        self.toolbar.exitEditMode.connect(self.disableEditMode)
        self.toolbar.findHomologs.connect(
            self._makeCurrentWidgetCallback('openBlastSearchDialog'))
        self.toolbar.requestFind.connect(self.requestFind)
        self.toolbar.requestFetch.connect(self.requestFetch)
        self.toolbar.nextPatternMatch.connect(
            lambda: self.onMovePatternClicked(True))
        self.toolbar.prevPatternMatch.connect(
            lambda: self.onMovePatternClicked(False))
        self.toolbar.quick_align_dialog.alignmentRequested.connect(
            self._makeCurrentWidgetCallback('onQuickAlignRequested'))
        self.toolbar.other_tasks_dialog.findHomologs.connect(
            self._makeCurrentWidgetCallback('openBlastSearchDialog'))
        self.toolbar.other_tasks_dialog.computeSequenceDescriptors.connect(
            self._makeCurrentWidgetCallback(
                '_runComputeSequenceDescriptorsDialog'))
        self.toolbar.other_tasks_dialog.findFamily.connect(
            self._makeCurrentWidgetCallback('generatePfam'))
        self.toolbar.other_tasks_dialog.homologResults.connect(
            self.displayBlastResults)
        self.toolbar.other_tasks_dialog.copySeqsToNewTab.connect(
            self._duplicateIntoNewTab)
        self.toolbar.importFromWorkspace.connect(self.onImportIncludedRequested)
        self.toolbar.importSelectedEntries.connect(
            self.onImportSelectedRequested)
        self.toolbar.importFile.connect(self.importSequences)
        self.toolbar.pattern_edit_dialog.patternListChanged.connect(
            self._updateTopMenuPatterns)
        self.toolbar.pattern_edit_dialog.emitPatternList()
        self.toolbar.buildHomologyModelRequested.connect(
            self.showHomologyModeling)
        self.toolbar.allPredictionsRequested.connect(
            self._makeCurrentWidgetCallback('generateAllPredictions'))
        self.toolbar.disulfideBondPredictionRequested.connect(
            self._makeCurrentWidgetCallback(
                'generatePredictionAnnotation',
                SEQ_ANNO_TYPES.pred_disulfide_bonds))
        self.toolbar.secondaryStructurePredictionRequested.connect(
            self._makeCurrentWidgetCallback(
                'generatePredictionAnnotation',
                SEQ_ANNO_TYPES.pred_secondary_structure))
        self.toolbar.solventAccessibilityPredictionRequested.connect(
            self._makeCurrentWidgetCallback('generatePredictionAnnotation',
                                            SEQ_ANNO_TYPES.pred_accessibility))
        self.toolbar.disorderedRegionsPredictionRequested.connect(
            self._makeCurrentWidgetCallback('generatePredictionAnnotation',
                                            SEQ_ANNO_TYPES.pred_disordered))
        self.toolbar.domainArrangementPredictionRequested.connect(
            self._makeCurrentWidgetCallback('generatePredictionAnnotation',
                                            SEQ_ANNO_TYPES.pred_domain_arr))
[docs]    def initLayOut(self):
        # InitMixin
        super().initLayOut()
        self.blast_results_dialog = self._initBlastResultsDialog()
        self.main_layout.addWidget(self.query_tabs)
        self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.history_widget)
        self.setMenuWidget(self.menu)
        self.addToolBar(self._tab_toolbar)
        self.addToolBarBreak()
        self.addToolBar(self.toolbar)
        self.main_layout.addWidget(self._status_bar)
        self.main_layout.addWidget(self._task_status_bar)
        self.setContentsMargins(0, 0, 0, 0)
        self.resize(1140, 700)
        self.setStyleSheet(stylesheets.MSV_GUI) 
[docs]    def initFinalize(self):
        # InitMixin
        super().initFinalize()
        self.model.setUndoStack(self.undo_stack)
        self.updateMenuActions()
        # Remove any tab switch commands from the undo stack.  (They get added
        # as a side of effect of creating the initial Workspace and View 1
        # tabs.)
        self.undo_stack.clear()
        if self._structure_model.IMPLEMENTS_AUTOLOAD:
            self._autoloadProject()
        # 'Renumber Residues' menu-item has to be re-displayed for MSV-2964
        self.setFocus() 
[docs]    @QtCore.pyqtSlot(object)
    def setModel(self, model):
        super().setModel(model)
        self._structure_model.setGuiModel(self.model) 
[docs]    def getSignalsAndSlots(self, model):
        current_page = model.current_page
        signals = current_page.aln_signals
        return [
            (signals.resSelectionChanged, self.onResSelectionChanged),
            (signals.sequencesInserted, self._fetchDomainsIfNeeded),
            (signals.invalidatedDomains, self._fetchDomainsIfNeeded),
            (signals.hiddenSeqsChanged, self._onHiddenSeqsChanged),
            (signals.seqSelectionChanged, self._warnIfCantSetConstraints),
            (signals.sequencesInserted, self._autosaveProject),
            (model.align_settings.align_only_selected_seqsChanged, self._warnIfCantSetConstraints),
            (model.align_settings.pairwise.set_constraintsChanged, self._onSetConstraintsChanged),
            (current_page.options.seq_filter_enabledChanged, self.updateToolbar),
            (model.sequence_local_onlyChanged, self.onSequenceLocalOnlyChanged),
            (model.pages.mutated, self._onPagesMutated),
            (model.current_pageReplaced, self._onCurrentPageReplaced),
            (model.edit_modeChanged, self._onEditModeChanged),
        ]  # yapf: disable 
[docs]    def defineMappings(self):
        # Implementation of abstract method of MapperMixin.
        M = self.model_class
        MA = self.menu.menu_actions
        current_options = M.current_page.options
        curr_menu_statuses = M.current_page.menu_statuses
        return [
            (self.query_tabs, M),
            (self.view_dialog, M.current_page),
            (self.color_dialog, current_options),
            (self.quick_annotate_dialog, current_options),
            (self.config_toggles, current_options),
            (self.toolbar.other_tasks_dialog, M.current_page),
            (self.menu, curr_menu_statuses),
            (self.toolbar.quick_align_dialog, M.current_page),
            (self.toolbar.ui.find_seq_le, current_options.seq_filter),
            (self.toolbar.edit_toolbar_manager, M.current_page),
            (mappers.TargetSpec(setter=self._updateToolbarPickMode), M.current_page.options.pick_mode),
            (MA.hide_annotations, current_options.annotations_enabled),
            (MA.hide_colors, current_options.colors_enabled),
            (MA.auto_align, M.auto_align),
            (MA.set_constraints, M.align_settings.pairwise.set_constraints),
            (MA.sequence_local, M.sequence_local_only),
            (self.get_sequences_dialog.ui.local_server_cb, M.sequence_local_only),
            (MA.blast_local, M.blast_local_only),
            (MA.pdb_local, M.pdb_local_only),
            (self.pdb_dialog.btn_group, M.pdb_local_only),
            (self.menu.menu_actions.auto_save, M.auto_save_only_on_edits),
            (self.menu.menu_actions.light_mode, M.light_mode),
            (mappers.TargetSpec(setter=self.onLightModeToggled), M.light_mode),
            (self.menu.menu_actions.edit_sequence, M.edit_mode),
            (mappers.TargetSpec(setter=self.setEditMode), M.edit_mode),
            (self.config_toggles.edit_btn, M.edit_mode),
            (self._saveProjectOnlyAfterEdit,M.auto_save_only_on_edits)
        ] # yapf:disable 
[docs]    @QtCore.pyqtSlot()
    def saveProjectAs(self):
        """
        Prompt the user for where to save a project (.msv2) and save it.
        """
        file_name = filedialog.get_save_file_name(parent=self,
                                                  caption="Save MSV Project",
                                                  filter="MSV Project (*.msv2)",
                                                  id='save_msv_project')
        if file_name is None:
            return
        self._saveModelAsJson(file_name)
        self._save_file_name = file_name 
[docs]    @QtCore.pyqtSlot()
    def saveProject(self):
        """
        Save the project to whatever file was last saved to or opened. If
        no file has been saved to or opened, then prompt the user for
        where to save the file.
        """
        if self._save_file_name is None:
            self.saveProjectAs()
        else:
            self._saveModelAsJson(self._save_file_name) 
    def _saveModelAsJson(self, json_fname):
        """
        Try to save the model to `json_fname`. If an exception is raised
        during encoding and there already exists a file at `json_fname`, the
        file will be left untouched.
        """
        with fileutils.tempfilename() as tmp_fname:
            with open(tmp_fname, 'w') as projfile:
                json.dump(self.model, projfile)
            shutil.move(tmp_fname, json_fname)
[docs]    @QtCore.pyqtSlot()
    def openProject(self):
        """
        Prompt the user for a project file (.msv2) and load it. Any future
        "Save"s will be overwrite the same project file as was opened.
        """
        if not self._isInBlankState():
            msg_box = messagebox.QuestionMessageBox(
                parent=self,
                title="Confirm Content Replacement",
                text=
                "All tabs other than Workspace will be discarded when the new "
                "project is opened. Continue anyway?",
                yes_text="OK",
                no_text=None,
                add_cancel_btn=True,
                save_response_key="open_project_and_overwrite")
            resp = msg_box.exec_()
            if not resp:
                return
        filename = filedialog.get_open_file_name(parent=self,
                                                 caption="Open MSV Project",
                                                 filter="MSV Project (*.msv2)",
                                                 id='open_msv_proj')
        if filename:
            try:
                self._setModelFromJson(filename)
            except NewerProjectException as exc:
                project_version = exc.version
                box = dialogs.LoadProjectFailedMessageBox(
                    self, project_version, filename)
                box.exec()
                return 
    def _isInBlankState(self):
        """
        Return whether the panel is in a "blank" state. "blank" meaning that
        there are no View tabs or there is only one view tab and it has no
        sequences.
        """
        model = self.model
        view_pages = [page for page in model.pages if not page.is_workspace]
        if len(view_pages) == 0:
            return True
        elif len(view_pages) == 1 and len(view_pages[0].aln) == 0:
            return True
        else:
            return False
[docs]    @QtCore.pyqtSlot()
    def importProject(self):
        filename = filedialog.get_open_file_name(
            parent=self,
            caption="Import MSV Project",
            filter="MSV Project (*.msv2)",
            id='import_msv_project',
        )
        if filename:
            try:
                new_model = self._loadModelFromJson(filename)
            except NewerProjectException as exc:
                project_version = exc.version
                box = dialogs.LoadProjectFailedMessageBox(
                    self, project_version, filename)
                box.exec()
                return
            # Emancipate new_current_page so it can be reassigned
            new_model.current_page = gui_models.NullPage()
            self.model.pages.extend(new_model.pages) 
    def _loadModelFromJson(self, json_fname):
        with open(json_fname, 'r') as infile:
            json_str = infile.read()
        return self._loadModelFromJsonStr(json_str)
    def _loadModelFromJsonStr(self, json_str):
        try:
            return json.loads(json_str, DataClass=gui_models.MsvGuiModel)
        except (AttributeError, KeyError, IndexError, TypeError, ValueError):
            mmshare_version = schrodinger.get_mmshare_version()
            project_version = json.get_json_version_from_string(json_str)
            if project_version is not None and project_version > mmshare_version:
                raise NewerProjectException(project_version) from None
            raise
    def _setModelFromJson(self, json_fname):
        new_model = self._loadModelFromJson(json_fname)
        if not self.model.pages[0].is_workspace:
            self._copyJsonBlacklistToNewModel(self.model, new_model)
            self.setModel(new_model)
        else:
            # We can't set the model since there's a workspace tab and
            # we don't want to destroy it, so just remove all non-workspace
            # pages and add in the new ones from the loaded project.
            for _ in range(len(self.model.pages) - 1):
                self.model.pages.pop()
            new_current_page = new_model.current_page
            # Emancipate new_current_page so it can be reassigned
            new_model.current_page = gui_models.NullPage()
            self.model.appendSavedPages(new_model.pages)
            if new_current_page.isNullPage():
                # If the loaded model doesn't have a current page, then
                # it was exported while the current page was set on the
                # the workspace page.
                self.model.current_page = self.model.pages[0]
            else:
                self.model.current_page = new_current_page
            self.model.light_mode = new_model.light_mode
            self._structure_model.updateViewPages(self.model)
        self.undo_stack.clear()
    @staticmethod
    def _copyJsonBlacklistToNewModel(old_model, new_model):
        """
        For all subparams in a JSON blacklist, replace the new model's default
        value with the old model's value.
        """
        concrete_params = [old_model]
        concrete_params.extend(parameters.get_all_compound_subparams(old_model))
        for old_concrete_param in concrete_params:
            blacklist = old_concrete_param.getJsonBlacklist()
            if not blacklist:
                continue
            # Params in `getJsonBlacklist` must be singly nested, so extract the
            # un-nested concrete param
            abstract_param = old_concrete_param.getAbstractParam()
            new_concrete_param = abstract_param.getParamValue(new_model)
            for abstract_subparam in blacklist:
                value = abstract_subparam.getParamValue(old_concrete_param)
                abstract_subparam.setParamValue(new_concrete_param, value)
[docs]    def show(self):
        """
        We call raise here so that the panel comes to the front on all platforms
        when running in standalone mode.
        """
        super(MSVPanel, self).show()
        self.raise_() 
[docs]    def showEvent(self, event):
        if not event.spontaneous() and self._structure_model is None:
            self.setStructureModel()
            self.query_tabs.setStructureModel(self._structure_model)
            if self._structure_model.IMPLEMENTS_AUTOLOAD:
                self._autoloadProject()
        super().showEvent(event) 
[docs]    def closeEvent(self, event):
        if self._homology_pane is not None:
            self._homology_pane.close()
        if maestro:
            self._autosaveProject()
            for tab_wid in self.query_tabs:
                tab_wid._structure_model = None
            self._structure_model.disconnect()
            self._structure_model.deleteLater()
            self._structure_model = None
        super().closeEvent(event) 
    @property
    def current_widget(self):
        return self.currentWidget()
[docs]    def enterEvent(self, event):
        """
        See Qt documentation for method documentation.
        """
        super().enterEvent(event)
        self._checkForRenamedSeqs() 
[docs]    def setCalculatedModels(self):
        """
        Set models that contain calculated data
        """
        widget = self.currentWidget()
        self.msv_status_bar.mapper.setModel(widget.seq_status_model)
        self.res_status_bar.mapper.setModel(widget.res_status_model)
        self.updateOtherTabs() 
[docs]    def setStructureModel(self, model=None):
        """
        Set a structure model for the panel.
        :param model: The structure model to set or None to create a new one
        :type  model: structure_model.StructureModel or None
        """
        if model is None:
            model = self._initStructureModel()
            model.setGuiModel(self.model)
        self._structure_model = model
        if self._structure_model.IMPLEMENTS_AUTOLOAD:
            self._structure_model.projectSaveRequested.connect(
                self._autosaveProject)
            self._structure_model.projectLoadRequested.connect(
                self._autoloadProject)
        self._structure_model.seqProjectTitlesChanged.connect(
            self.onSeqProjectTitlesChanged)
        self._structure_model.structureWarningProduced.connect(
            self._onStructureWarningProduced) 
    @QtCore.pyqtSlot(str)
    def _onStructureWarningProduced(self, message: str):
        self.warning(message)
    @QtCore.pyqtSlot()
    @QtCore.pyqtSlot(bool)
    def _autosaveProject(self, reset_save=False):
        if not self._structure_model.IMPLEMENTS_AUTOLOAD:
            return
        try:
            project_name = self._structure_model.getMsvAutosaveProjectName()
        except project.ProjectException:
            # This happens if the project is already closed, e.g. when Maestro
            # is in the process of closing
            return
        if reset_save:
            self._save_file_name = None
        self._saveModelAsJson(project_name)
    @QtCore.pyqtSlot()
    def _autoloadProject(self):
        """
        Get the name of the autosave project from the structure model
        and load it. If the project doesn't exist, then just reset the model.
        """
        project_name = self._structure_model.getMsvAutosaveProjectName()
        try:
            self._setModelFromJson(project_name)
        except FileNotFoundError:
            self.model.reset()
        except NewerProjectException as exc:
            project_version = exc.version
            box = dialogs.LoadProjectFailedMaestroQuestionBox(
                self, project_version)
            response = box.exec()
            if response:
                new_filename = filedialog.get_save_file_name(
                    self,
                    caption='Save MSV Project',
                    filter='MSV Project (*.msv2)')
                moved = False
                if new_filename:
                    try:
                        shutil.move(project_name, new_filename)
                    except OSError:
                        pass
                    else:
                        moved = True
                if not moved:
                    # If the user cancels or the move fails, make a backup file
                    # in the project
                    shutil.move(project_name, project_name + ".bak")
            else:
                # Discard msv project
                fileutils.force_remove(project_name)
            self.model.reset()
        except (AttributeError, KeyError, IndexError, TypeError, ValueError):
            self.model.reset()
            self.warning(
                "There was an issue loading the MSV project associated "
                "with the current Maestro project.")
            traceback.print_exc()
        else:
            self._structure_model.updateViewPages(self.model)
    def _initStructureModel(self):
        """
        :return: A new structure model instance
        """
        return structure_model.StructureModel(self, self.undo_stack)
    def _initToolbar(self):
        """
        :return: A new MSVToolbar instance
        """
        tb = toolbar.MSVToolbar(self)
        tb.pickClosed.connect(self._onPickClosed)
        tb.resetPickRequested.connect(
            self._makeCurrentWidgetCallback("resetPick"))
        tb.ui.close_find_seq_btn.clicked.connect(self.disableFindSequence)
        return tb
    def _initBlastResultsDialog(self):
        """
        Instantiate, set up, and return the Blast Results Dialog.
        """
        dialog = dialogs.BlastResultsDialog(parent=self, pages=self.model.pages)
        dialog.addSequencesRequested.connect(self._onBlastAddSequencesRequested)
        dialog.addStructuresRequested.connect(
            self._onBlastAddStructuresRequested)
        return dialog
[docs]    @QtCore.pyqtSlot()
    def displayBlastResults(self):
        """
        Show the Blast Results Dialog for the current page. If the current page
        does not have a finished blast task, will show the results of the most
        recently viewed blast task.
        """
        if not self.blast_results_dialog.hasAnyResults():
            self.warning("No BLAST results to show.")
            return
        self._displayBlastResults() 
    def _displayBlastResults(self, task=None):
        """
        Show the Blast Results Dialog.
        :param task: A blast task to update the dialog or None to keep the most
            recently viewed results.
        :type task: blast.BlastTask or NoneType
        """
        self.blast_results_dialog.setCurrentPage(self.model.current_page)
        if task is not None:
            self.blast_results_dialog.setModel(task)
        self.blast_results_dialog.run(modal=True, blocking=True)
    @QtCore.pyqtSlot(list, gui_models.PageModel)
    def _onBlastAddSequencesRequested(self, sequences, page):
        """
        :param sequences: Structureless sequences to add
        :type sequences: list[sequence.ProteinSequence]
        :param page: The page to import the sequences into
        :type page: gui_models.PageModel
        """
        if page.is_workspace:
            self._addSeqsToCopyOfWorkspaceTab(sequences)
        else:
            page_idx = self.model.pages.index(page)
            widget = self.query_tabs.widget(page_idx)
            widget.importSeqs(sequences)
    @QtCore.pyqtSlot(list, gui_models.PageModel)
    def _onBlastAddStructuresRequested(self, pdb_ids, page):
        """
        :param pdb_ids: PDB IDs to download
        :type pdb_ids: list[str]
        :param page: The page to import the sequences into
        :type page: gui_models.PageModel
        """
        page_idx = self.model.pages.index(page)
        widget = self.query_tabs.widget(page_idx)
        widget.downloadBlastPDBs(pdb_ids)
    @QtCore.pyqtSlot(blast.BlastTask)
    def _runPredictorBlastTask(self, blast_task):
        self._runBlastTask(blast_task, show_results=False)
    @QtCore.pyqtSlot(blast.BlastTask)
    def _runBlastTask(self,
                      blast_task,
                      *,
                      location=blast.LOCAL,
                      show_results=True):
        """
        Run a blast task.
        While each task belongs to a widget, we run it in the gui to avoid
        problems if the widget is deleted while the task is running.
        :type blast_task: blast.BlastTask
        :param location: Either blast.LOCAL or blast.REMOTE
        :type location: str
        :param show_results: Whether to show results when the task is done
        :type show_results: bool
        """
        if blast_task.input.query_sequence is None:
            self.warning(title="Set BLAST Query Sequence",
                         text="Cannot run BLAST with no sequence set")
            return
        if blast_task.status is blast_task.RUNNING:
            msg = ("The previous BLAST Search launched from this tab is still "
                   "in progress. Wait for it to complete and try again, or "
                   "duplicate this tab and run the search from there.")
            self.warning(title="Cannot Start BLAST Search", text=msg)
            return
        if location == blast.LOCAL:
            has_local_db = blast_task.checkLocalDatabase()
            if not has_local_db:
                if self.model.blast_local_only:
                    dialogs.NoLocalBlastServerWarning(self).exec()
                    return
                else:
                    remote_ok = dialogs.RemoteBlastMessageBox(self).exec()
                    if remote_ok:
                        location = blast.REMOTE
                    else:
                        return
        blast_task.input.settings.location = location
        timeout_s = blast_task.getExpectedRuntime()
        with qt_utils.wait_cursor:
            blast_task.output.clear()
            blast_task.start()
            self._task_status_bar.watchTask(blast_task)
            blast_task.wait(timeout_s)  # TODO PANEL-18317
        if blast_task.status is blast_task.DONE:
            if show_results:
                self._displayBlastResults(blast_task)
        elif blast_task.status is blast_task.FAILED:
            retry = msv_widget.check_if_can_retry_blast(
                dialog_parent=self,
                task=blast_task,
                blast_local_only=self.model.blast_local_only)
            if retry:
                self._runBlastTask(blast_task, location=blast.REMOTE)
        else:
            blast_task.kill()
            self.warning("The BLAST search has timed out.")
    @QtCore.pyqtSlot()
    def _updateDuplicateIntoExistingMenuActions(self):
        """
        Update the 'Duplicate' > 'Into Existing Tab' menu action with the
        current view pages.
        """
        curr_widget = self.currentWidget()
        if not curr_widget:
            return
        def get_duplicate_into_existing_menu():
            """
            :return: Return the 'Duplicate' > 'Into Existing Tab' menu.
            :rtype: QtWidgets.QMenu
            """
            duplicate_into_existing_tab_menu = QtWidgets.QMenu()
            for page_idx, page in enumerate(self.model.pages):
                if page.is_workspace or page is self.model.current_page:
                    continue
                action = QtWidgets.QAction(page.title,
                                           duplicate_into_existing_tab_menu)
                action.setData(page_idx)
                duplicate_into_existing_tab_menu.addAction(action)
            # Switching tabs will cause the menu to be replaced (go out of
            # scope), so use a queued connection to delay until the menu
            # is no longer shown
            duplicate_into_existing_tab_menu.triggered.connect(
                self._onDuplicateIntoExistingTabMenuTrigger,
                QtCore.Qt.QueuedConnection)
            return duplicate_into_existing_tab_menu
        duplicate_into_existing_actions = (
            self.menu.menu_actions.duplicate_into_existing_tab,
            curr_widget.aln_info_view.seq_context_menu.duplicate_into_existing_tab,
        )  # yapf: disable
        for action in duplicate_into_existing_actions:
            tab_menu = get_duplicate_into_existing_menu()
            action.setMenu(tab_menu)
            # msv.gui.menu.MenuAction extends QAction.setMenu to enable menu
            # actions.
            has_actions = len(tab_menu.actions()) > 0
            action.setEnabled(has_actions)
    @QtCore.pyqtSlot(object, object)
    def _onPagesMutated(self, new_pages, old_pages):
        if self._structure_model is not None:
            self._structure_model.onPagesMutated(new_pages, old_pages)
        self._updateDuplicateIntoExistingMenuActions()
        self.updateOtherTabs()
    @QtCore.pyqtSlot(msv_widget.AbstractMsvWidget)
    def _setUpNewWidget(self, widget):
        """
        Set up a new or duplicated widget by connecting signals
        :param widget: A new alignment widget
        :type  widget: msv_widget.AbstractMsvWidget
        """
        # Lambda slots with references to self cause problems with garbage
        # collection.  To avoid this, we replace self with a weakref.
        self = weakref.proxy(self)
        widget.duplicateIntoNewTabRequested.connect(self.duplicateIntoNewTab)
        widget.translateIntoNewTabRequested.connect(self.translateIntoNewTab)
        widget.renameSequenceRequested.connect(self.renameSequence)
        widget.residueHovered.connect(self.onResidueHovered)
        widget.residueUnhovered.connect(self.onResidueUnhovered)
        widget.openColorPanelRequested.connect(self.showColorPopup)
        widget.resHighlightStatusChanged.connect(
            self.onResHighlightStatusChanged)
        widget.proteinStructAlignResultsReady.connect(
            self._createProteinStructAlignTabs)
        widget.seq_status_model.num_totalChanged.connect(self.updateOtherTabs)
        widget.options_model.res_formatChanged.connect(
            self._residueFormatChanged)
        widget.aln_info_view.getPdbStClicked.connect(
            self._fetchPdbStrucsForSelSeqs)
        widget.aln_info_view.unlinkFromEntryRequested.connect(
            lambda: self._openLinkSeqsDialog(open_with_selected_seqs=True))
        widget.aln_info_view.linkToEntryRequested.connect(
            self._openLinkSeqsDialog)
        widget.taskStarted.connect(self._task_status_bar.watchTask)
        widget.startBlastTaskRequested.connect(self._runBlastTask)
        widget.startPredictorBlastTaskRequested.connect(
            self._runPredictorBlastTask)
        widget.options_model.sequence_annotations.mutated.connect(
            self.onDomainsChanged)
        widget.view.sequenceEditModeRequested.connect(self._toggleEditMode)
        widget.view.copyToNewTabRequested.connect(self.duplicateResIntoNewTab)
        widget.alignmentFinished.connect(self._autosaveProject)
[docs]    @QtCore.pyqtSlot()
    def showDendrogram(self):
        """
        Displays and populates dendrogram viewer widget and dendrogram
        settings dialog.
        """
        current_widget = self.currentWidget()
        if len(current_widget.getAlignment().getShownSeqs()) < 2:
            msg = ("At least two sequences need to be present in the sequence "
                   "viewer to run an alignment")
            self.warning(title="Warning", text=msg)
            return
        if not self.dendrogram_viewer:
            self.dendrogram_viewer = DendrogramViewer()
        self.dendrogram_viewer.show(current_widget.getAlignment()) 
    def _makeMenu(self):
        """
        Return a menu of the appropriate kind
        :rtype: `schrodinger.application.msv.gui.menu.MsvMenuBar`
        :return: A menu bar
        """
        MenuClass = menu.MsvMenuBar
        return MenuClass(parent=self)
[docs]    @QtCore.pyqtSlot()
    def closeProject(self):
        if not self._isInBlankState():
            msg_box = messagebox.QuestionMessageBox(
                parent=self,
                title="Confirm Discarding Tabs",
                text=
                "Closing the project will discard all tabs other than Workspace."
                " Continue anyway?",
                yes_text="OK",
                no_text=None,
                add_cancel_btn=True,
                save_response_key="close_msv_project")
            resp = msg_box.exec_()
            if not resp:
                return
        self._save_file_name = None
        self.model.reset() 
    def _makeMenuMap(self):
        """
        :rtype: dict
        :return: A mapping of menu actions to callbacks
        """
        acts = self.menu.menu_actions
        on_top_widget = self._makeCurrentWidgetCallback
        # Lambda slots with references to self cause problems with garbage
        # collection.  To avoid this, we replace self with a weakref.
        self = weakref.proxy(self)
        menu_map = {
            acts.close_project: self.closeProject,
            acts.open_project: self.openProject,
            acts.import_project: self.importProject,
            acts.save_project: self.saveProject,
            acts.save_as: self.saveProjectAs,
            acts.edit_as_text: self.editSequenceAsPlainText,
            acts.duplicate_at_bottom: on_top_widget('duplicateAtBottom'),
            acts.duplicate_at_top: on_top_widget('duplicateAtTop'),
            acts.duplicate_in_place: on_top_widget('duplicateInPlace'),
            acts.duplicate_as_ref: on_top_widget('duplicateAsRef'),
            acts.duplicate_into_new_tab: self.duplicateIntoNewTab,
            acts.dendrogram: self.showDendrogram,
            acts.get_pdb: self.getPDB,
            acts.get_pdb_sts: self._fetchPdbStrucsForSelSeqs,
            acts.get_sequences: self.getSequences,
            acts.import_sequences: self.importSequences,
            acts.import_from_maestro_workspace: on_top_widget('importIncluded'),
            acts.import_from_maestro_selected: on_top_widget('importSelected'),
            acts.paste_sequences: self.pasteSequences,
            acts.export_sequences: on_top_widget('exportSequences'),
            acts.save_image: on_top_widget('saveImage'),
            acts.close: self.close,
            acts.copy: self.copySelection,
            acts.delete_sel_residues: on_top_widget('deleteSelection'),
            acts.delete_sel_gaps: on_top_widget('deleteSelectedGaps'),
            acts.delete_gap_cols: on_top_widget('deleteGapOnlyColumns'),
            acts.delete_sel_seqs: on_top_widget('removeSelectedSeqs'),
            acts.delete_all_predictions: on_top_widget('deleteAllPredictions'),
            acts.delete_redundant_seqs: on_top_widget('deleteRedundantSeqs'),
            acts.delete_this_tab: self.deleteTab,
            acts.delete_all_view_tabs: self.resetTabs,
            acts.set_as_ref_seq: self.setReferenceSeq,
            acts.rename_seq: self.renameSequence,
            acts.move_set_as_ref: self.setReferenceSeq,
            acts.replace_sel_with_gaps: self.replaceSelectionWithGaps,
            acts.reset_remote_server_ask: self.resetRemoteServerAsk,
            acts.select_all_sequences: on_top_widget('selectAllSequences'),
            acts.select_no_sequences: on_top_widget('deselectAllSequences'),
            acts.select_sequence_with_structure:
                on_top_widget('selectSequencesWithStructure'),
            acts.select_sequence_by_identity:
                on_top_widget('selectSequenceByIdentity'),
            acts.select_sequence_antibody_heavy:
                on_top_widget('selectAntibodyHeavyChain'),
            acts.select_sequence_antibody_light:
                on_top_widget('selectAntibodyLightChain'),
            acts.select_invert_seq_selection:
                on_top_widget('invertSequenceSelection'),
            acts.select_all_residues: on_top_widget('selectAllResidues'),
            acts.select_no_residues: on_top_widget('deselectAllResidues'),
            acts.select_residues_with_structure:
                on_top_widget('selectResiduesWithStructure'),
            acts.select_antibody_cdr: on_top_widget('selectAntibodyCDR'),
            acts.select_binding_sites: on_top_widget('selectBindingSites'),
            acts.select_protein_interface:
                on_top_widget('selectProteinInterface'),
            acts.select_columns_with_structure:
                on_top_widget('selectColsWithStructure'),
            acts.select_deselect_gaps: on_top_widget('deselectGaps'),
            acts.select_invert_res_selection:
                on_top_widget('invertResSelection'),
            acts.select_expand_along_cols:
                on_top_widget('expandSelectionAlongCols'),
            acts.select_expand_ref_sel_only:
                on_top_widget('expandSelectionReferenceOnly'),
            acts.select_identities: on_top_widget('selectIdentityColumns'),
            acts.select_aligned_residues:
                on_top_widget('selectAlignedResidues'),
            acts.translate_seq: on_top_widget('translateSelectedSequences'),
            acts.hide_selected_seqs: on_top_widget('hideSelectedSeqs'),
            acts.show_all_seqs: on_top_widget('showAllSeqs'),
            acts.show_workspace_seqs: on_top_widget('showWorkspaceSequences'),
            acts.find_seqs_in_list: on_top_widget('enableFindSequence'),
            acts.collapse_all: lambda: self.setExpansionAll(False),
            acts.collapse_selected: lambda: self.setExpansionSelected(False),
            acts.collapse_unselected: lambda: self.setExpansionUnselected(False
                                                                         ),
            acts.expand_all: lambda: self.setExpansionAll(True),
            acts.expand_selected: lambda: self.setExpansionSelected(True),
            acts.expand_unselected: lambda: self.setExpansionUnselected(True),
            acts.configure_annotations: self.showAnnotationPopup,
            acts.configure_colors: self.showColorPopup,
            acts.configure_view: self.showViewPopup,
            acts.reset_to_defaults: self.resetViewToDefaults,
            acts.multiple_alignment: on_top_widget(
                'callAlignMethod', viewconstants.SeqAlnMode.Multiple),
            acts.pairwise_alignment: on_top_widget(
                'callAlignMethod', viewconstants.SeqAlnMode.Pairwise),
            acts.pairwise_ss_alignment: on_top_widget(
                'callAlignMethod', viewconstants.SeqAlnMode.PairwiseSS),
            acts.profile_alignment: on_top_widget(
                'callAlignMethod', viewconstants.SeqAlnMode.Profile),
            acts.align_from_superposition: on_top_widget(
                'callAlignMethod', viewconstants.SeqAlnMode.Structure),
            acts.align_based_sequence: on_top_widget(
                'callAlignMethod', viewconstants.StructAlnMode.Superimpose),
            acts.align_binding_sites: on_top_widget(
                'callAlignMethod', viewconstants.StructAlnMode.BindingSite),
            acts.clear_constraints: on_top_widget('clearConstraints'),
            acts.reset_align: self.resetAlignSettings,
            acts.anchor_selection: on_top_widget('anchorSelection'),
            acts.clear_anchoring: on_top_widget('clearAnchored'),
            acts.redo: on_top_widget('redo'),
            acts.show_color_scheme_editor:
                on_top_widget('showColorSchemeEditor'),
            acts.start_ipython_session: self.startIPythonSession,
            acts.show_debug_gui: self.showDebugGui,
            acts.time_scroll: on_top_widget('timeScrolling'),
            acts.time_scroll_by_page: on_top_widget('timeScrollingByPage'),
            acts.profile_scroll: on_top_widget('profileScrolling'),
            acts.profile_scroll_by_page:
                on_top_widget('profileScrollingByPage'),
            acts.view_history: self.toggleHistory,
            acts.undo: on_top_widget('undo'),
            acts.prot_struct_align: on_top_widget('runStructureAlignment'),
            acts.sort_ascending_by_name: lambda: self.sortBy(
                viewconstants.SortTypes.Name, False),
            acts.sort_ascending_by_chain_id: lambda: self.sortBy(
                viewconstants.SortTypes.ChainID, False),
            acts.sort_ascending_by_gaps: lambda: self.sortBy(
                viewconstants.SortTypes.NumGaps, False),
            acts.sort_ascending_by_length: lambda: self.sortBy(
                viewconstants.SortTypes.Length, False),
            acts.sort_ascending_by_seq_identity: lambda: self.sortBy(
                viewconstants.SortTypes.Identity, False),
            acts.sort_ascending_by_seq_similarity: lambda: self.sortBy(
                viewconstants.SortTypes.Similarity, False),
            acts.sort_ascending_by_seq_homology: lambda: self.sortBy(
                viewconstants.SortTypes.Conservation, False),
            acts.sort_ascending_by_seq_score: lambda: self.sortBy(
                viewconstants.SortTypes.Score, False),
            acts.sort_descending_by_name: lambda: self.sortBy(
                viewconstants.SortTypes.Name, True),
            acts.sort_descending_by_chain_id: lambda: self.sortBy(
                viewconstants.SortTypes.ChainID, True),
            acts.sort_descending_by_gaps: lambda: self.sortBy(
                viewconstants.SortTypes.NumGaps, True),
            acts.sort_descending_by_length: lambda: self.sortBy(
                viewconstants.SortTypes.Length, True),
            acts.sort_descending_by_seq_identity: lambda: self.sortBy(
                viewconstants.SortTypes.Identity, True),
            acts.sort_descending_by_seq_similarity: lambda: self.sortBy(
                viewconstants.SortTypes.Similarity, True),
            acts.sort_descending_by_seq_homology: lambda: self.sortBy(
                viewconstants.SortTypes.Conservation, True),
            acts.sort_descending_by_seq_score: lambda: self.sortBy(
                viewconstants.SortTypes.Score, True),
            acts.reverse_last_sort: self.reverseLastSort,
            acts.select_link_seq_to_entries: self._openLinkSeqsDialog,
            acts.select_update_workspace_selection:
                on_top_widget('updateWorkspaceSelection'),
            acts.move_to_top: lambda: self.moveSelectedSequences(viewconstants.
                                                                 Direction.Top),
            acts.move_up: lambda: self.moveSelectedSequences(viewconstants.
                                                             Direction.Up),
            acts.move_down: lambda: self.moveSelectedSequences(viewconstants.
                                                               Direction.Down),
            acts.move_to_bottom: lambda: self.moveSelectedSequences(
                viewconstants.Direction.Bottom),
            acts.renumber_residues: self.renumberResidues,
            acts.msv_help: self._openHelpPage,
            acts.homology_modeling_help: self._openHelpPage,
            acts.alignment_pane_help: self._openHelpPage,
            acts.getting_started_help: self._openMSVGuide,
            acts.bioluminate_intro: self._openMSVGuide,
            acts.chimeric_hm_help: self._openMSVGuide,
            acts.batch_hm_help: self._openMSVGuide,
            acts.antibody_anno_help: self._openMSVGuide,
        }
        return menu_map
    @QtCore.pyqtSlot(list)
    def _updateTopMenuPatterns(self, pattern_list):
        """
        Create a menu for the pattern lists and set it on the top menu item
        """
        pattern_menu = self.toolbar.createSavedPatternsMenu(pattern_list)
        pattern_menu.triggered.connect(self._requestFindFromAction,
                                       QtCore.Qt.QueuedConnection)
        action = self.menu.menu_actions.select_pattern_matching_residues
        action.setMenu(pattern_menu)
    @QtCore.pyqtSlot(QtWidgets.QAction)
    def _requestFindFromAction(self, action):
        """
        Find pattern from an action that has the pattern as its data
        """
        pattern = action.data()
        if pattern is not None:
            # If the action is not associated with a pattern, its data is None
            self.requestFind(pattern)
    @QtCore.pyqtSlot()
    @QtCore.pyqtSlot(bool)
    def _openLinkSeqsDialog(self, open_with_selected_seqs=False):
        if not self.model.current_page.split_chain_view:
            self.warning(
                'Sequences can be linked or unlinked for individual '
                'chains only. Turn on Split Chains mode (under the + button) '
                'and try again.')
            return
        link_seqs_dlg = dialogs.LinkSeqsDialog(self, self._structure_model,
                                               self.model.current_page,
                                               open_with_selected_seqs)
        link_seqs_dlg.run(modal=True)
    def _setUpMenu(self):
        """
        Set up the application menu, wiring up all signals
        """
        menu_map = self._makeMenuMap()
        for action, method in menu_map.items():
            # Remove later; actions are disabled by default so that we can see
            # at a glance which have been implemented
            action.setImplemented(True)
            action.triggered.connect(method, QtCore.Qt.QueuedConnection)
        self.menu.menu_actions.reverse_last_sort.setEnabled(
            bool(self._last_sort_reversed))
        acts = self.menu.menu_actions
        maestro_only_acts = [
            acts.import_from_maestro,
            acts.import_from_maestro_workspace,
            acts.import_from_maestro_selected,
            acts.select_link_seq_to_entries,
            acts.select_update_workspace_selection,
            acts.show_workspace_seqs,
            acts.align_based_sequence,
        ]
        enable = bool(maestro)
        for a in maestro_only_acts:
            a.setEnabled(enable)
            if not enable:
                a.setToolTip("Not available outside of Maestro")
    def _setUpColorDialog(self):
        """
        Set up the Color dialog, wiring up all the signals.
        """
        self.color_dialog.colorSelResRequested.connect(
            self._makeCurrentWidgetCallback('setSelectedResColor'))
        self.color_dialog.clearHighlightsRequested.connect(
            self._makeCurrentWidgetCallback('clearAllHighlights'))
        self.color_dialog.applyColorsToWorkspaceRequested.connect(
            self._makeCurrentWidgetCallback('applyColorsToWorkspace'))
        self.color_dialog.defineCustomColorSchemeRequested.connect(
            self._makeCurrentWidgetCallback('showColorSchemeEditor'))
        self.color_dialog.setEnableColorPicker(False)
[docs]    @QtCore.pyqtSlot()
    def onResSelectionChanged(self):
        """
        Handles a change in residue selections
        """
        has_selections = self.model.current_page.aln.res_selection_model.hasSelection(
        )
        self.color_dialog.setEnableColorPicker(has_selections) 
[docs]    @QtCore.pyqtSlot()
    def onResHighlightStatusChanged(self):
        """
        Callback for when residue highlights change.
        """
        aln = self.model.current_page.aln
        has_highlights = bool(aln.getHighlightColorMap())
        self.color_dialog.setClearHighlightStatus(has_highlights) 
    @QtCore.pyqtSlot()
    def _fetchDomainsIfNeeded(self):
        if (SEQ_ANNO_TYPES.domains
                in self.model.current_page.options.sequence_annotations):
            # This method can be called in the middle of inserting a sequence.
            # Because of that, we need to delay the call to _fetchDomains until
            # after the viewmodel has finished inserting the new sequences;
            # otherwise, AnnotationProxyModel will try to add the domain
            # annotation rows before there's a sequence row.
            self._fetch_domain_timer.start()
[docs]    @QtCore.pyqtSlot(set, set)
    def onDomainsChanged(self, new_annos, old_annos):
        if SEQ_ANNO_TYPES.domains in new_annos - old_annos:
            self._fetchDomainsWithWarning() 
[docs]    @QtCore.pyqtSlot()
    def onSequenceLocalOnlyChanged(self):
        if (SEQ_ANNO_TYPES.domains
                in self.model.current_page.options.sequence_annotations and
                not self.model.sequence_local_only):
            self._fetchDomainsWithWarning() 
    @QtCore.pyqtSlot()
    def _onHiddenSeqsChanged(self):
        """
        Change the text of select/expand/collapse options to All or Displayed
        based on whether any sequences are hidden
        """
        aln = self.model.current_page.aln
        any_hidden = aln.anyHidden()
        text = "Displayed" if any_hidden else "All"
        acts = self.menu.menu_actions
        acts.select_all_sequences.setText(f"{text} Sequences")
        acts.expand_all.setText(text)
        acts.collapse_all.setText(text)
    def _makeCurrentWidgetCallback(self, meth_name, *fixed_args):
        """
        Returns a callback that executes the specified method on the current
        widget
        :param meth_name: The name of the method to call on the current widget
        :type meth_name: str
        :rtype: callable
        :return: The requested callback
        """
        # Lambda slots with references to self cause problems with garbage
        # collection.  To avoid this, we replace self with a weakref.
        self = weakref.proxy(self)
        def _inner(*args):
            current_widget = self.currentWidget()
            meth = getattr(current_widget, meth_name)
            # This callback may be called with a signal which has args the slot
            # cannot accept (e.g. QAbstractButton.clicked emits `checked: bool`
            # even for non-checkable buttons and many slots take 0 args), so we
            # inspect the slot args and truncate signal args if necessary
            arg_spec = inspect.getfullargspec(meth)
            # subtract 1 for `self`
            num_args = len(arg_spec.args) - len(fixed_args) - 1
            var_args = arg_spec.varargs is not None
            if var_args or num_args == len(args):
                # If the slot takes *args or the same number of args as the
                # signal, pass all args
                meth(*fixed_args, *args)
            else:
                # Truncate signal args if method cannot accept them
                meth(*fixed_args, *args[:num_args])
        return _inner
[docs]    @QtCore.pyqtSlot()
    def resetTabs(self):
        """
        Remove all current view tabs, leaving the workspace tab if present, and
        add a new, empty view tab.
        """
        if not self._isInBlankState():
            msg_box = messagebox.QuestionMessageBox(
                parent=self,
                title="Confirm Discarding Tabs",
                text="Delete all View tabs from the current project?",
                yes_text="OK",
                no_text=None,
                add_cancel_btn=True,
                save_response_key="delete_view_tabs")
            resp = msg_box.exec_()
            if not resp:
                return
        self.query_tabs.removeAllViewPages() 
[docs]    @QtCore.pyqtSlot()
    def resetRemoteServerAsk(self):
        """
        When the menu item for the Ask Remote Server option is clicked, this
        should update the preferences for whether the Do Not Show Again dialog
        is shown for remote searches.
        """
        keys = (dialogs.REMOTE_FETCH_KEY, dialogs.REMOTE_BLAST_KEY,
                dialogs.REMOTE_OR_LOCAL_BLAST_KEY)
        for key in keys:
            if settings.get_persistent_value(key, None):
                settings.remove_preference_key(key) 
[docs]    def onLightModeToggled(self, enabled):
        """
        Turn on or off the light_mode property, and update all of the widgets
        whose style depends on that property
        """
        self.setProperty("light_mode", enabled)
        for widget in self.query_tabs:
            qt_utils.update_widget_style(widget.view)
            qt_utils.update_widget_style(widget.aln_info_view)
            qt_utils.update_widget_style(widget.aln_metrics_view)
            qt_utils.update_widget_style(widget.v_scrollbar)
            qt_utils.update_widget_style(widget.h_scrollbar)
            qt_utils.update_widget_style(widget.splitter.handle(1))
            qt_utils.update_widget_style(widget.view_widget) 
[docs]    def sortBy(self, sort_by, reverse):
        """
        Sort the alignment by the specified criteria
        :param sort_by: The criterion to sort on
        :type sort_by: viewconstants.sortTypes
        :param reverse: Whether the data should be sorted in reverse.
        :type reverse: bool
        """
        widget = self.currentWidget()
        self.menu.menu_actions.reverse_last_sort.setEnabled(True)
        self._last_sort_reversed = lambda: self.sortBy(sort_by, not reverse)
        widget.sortBy(sort_by, reverse=reverse) 
[docs]    def reverseLastSort(self):
        """
        Reverses the last sort done via the edit menu
        """
        if self._last_sort_reversed is not None:
            self._last_sort_reversed() 
[docs]    @QtCore.pyqtSlot()
    def setReferenceSeq(self):
        """
        Set the currently selected sequence as the reference sequence
        """
        widget = self.currentWidget()
        widget.setSelectedSeqAsReference() 
[docs]    @QtCore.pyqtSlot()
    def renameSequence(self):
        """
        Renames the selected sequence
        """
        widget = self.currentWidget()
        selected_seq = widget.getSelectedSequences()
        if len(selected_seq) != 1:
            raise RuntimeError("Can only rename one sequence at a time")
        seq_to_rename = selected_seq[0]
        self._rename_sequence_dialog = dialogs.RenameSequenceDialog(
            seq_to_rename, parent=self)
        self._rename_sequence_dialog.renameSequenceRequested.connect(
            self._renameSeq)
        self._rename_sequence_dialog.exec() 
    @QtCore.pyqtSlot(sequence.ProteinSequence, str, str)
    def _renameSeq(self, seq, new_name, new_chain):
        """
        Rename sequence.
        :param seq: Sequence to be renamed
        :type seq: sequence.ProteinSequence
        :param new_name: New name for the sequence
        :type new_name: str
        :param new_chain: New chain name for the sequence
        :type new_chain: str
        """
        current_page = self.model.current_page
        if new_chain != "" and not current_page.split_chain_view:
            raise ValueError("Cannot edit chain in combined chain mode")
        name_changed = new_name != seq.name
        if name_changed and current_page.is_workspace:
            msg = ("This will change the entry title in the Maestro project "
                   "and all linked sequences in this tab.  Continue anyway?")
            response = self.question(title='Workspace Sequence Title Change',
                                     text=msg)
            if not response:
                return
        desc = f"Rename Sequence {seq.fullname} to {new_name}"
        with command.compress_command(self.undo_stack, desc):
            if current_page.split_chain_view:
                # Change chain name for split chain view only
                aln = current_page.aln
                aln.changeSeqChain(seq, new_chain)
            if name_changed:
                # Change sequence name and possibly the linked structure title
                self._changeSeqName(seq, new_name)
    def _changeSeqName(self, seq, new_name):
        """
        Change the name of the given sequence. It may also prompt the user to
        change the title of the linked structure.
        """
        current_page = self.model.current_page
        sm = self._structure_model
        linked_seqs = sm.getLinkedAlnSeqs(seq)
        if not linked_seqs:
            aln = current_page.aln
            aln.renameSeq(seq, new_name)
            return
        rename_linked_seqs = False
        rename_entry = False
        if current_page.is_workspace:
            sm.unsynchEntryID(seq.entry_id)
        elif len(linked_seqs) == 1:
            # Only this seq is linked to an entry
            msg = ("The sequence you renamed had the same name as its linked "
                   "entry. Do you want to change the entry's title so they "
                   "continue to match?")
            rename_entry = self.question(title='Linked Sequence Renamed',
                                         text=msg,
                                         no_text="No",
                                         add_cancel_btn=False)
        elif len(linked_seqs) > 1:
            dlg = dialogs.ConfirmLinkedSeqRenameMessageBox(self)
            rename_linked_seqs = dlg.exec_()
            rename_entry = dlg.rename_entry_cb.isChecked()
        sm.renameSeq(seq,
                     new_name,
                     rename_linked_seqs=rename_linked_seqs,
                     rename_entry=rename_entry)
[docs]    @QtCore.pyqtSlot(dict, bool)
    def onSeqProjectTitlesChanged(self, seq_title_map, update_now):
        """
        Called when the Project Table row entry title is changed for sequences
        :param seq_title_map: Map of sequences whose titles have changed to
                              their new titles
        :type seq_title_map: dict(sequence.ProteinSequence: str)
        :param update_now: Whether the updates to the renamed sequences
                           should be made immediately
        :type update_now: bool
        """
        ws_aln = self._structure_model.getWorkspaceAlignment()
        for seq, new_name in seq_title_map.items():
            if seq in ws_aln:
                self._structure_model.renameSeq(seq, new_name)
            else:
                self._renamed_seqs[seq] = new_name
        if update_now:
            self._checkForRenamedSeqs() 
    @util.skip_if("_checking_for_renamed_seqs")
    def _checkForRenamedSeqs(self):
        """
        Checks to see if the panel has been notified of sequences renamed
        outside of MSV and, if so, ask user if the MSV sequences should
        also be renamed.
        """
        with self._checkingForRenamedSeqs():
            if not self._renamed_seqs:
                return
            ws_seqs = [
                s for s in self._renamed_seqs
                if self.model.getAlignmentOfSequence(s) is None
            ]
            view_seqs = [s for s in self._renamed_seqs if s not in ws_seqs]
            if view_seqs:
                msg = (
                    "The title of one or more linked entries has been changed. "
                    "Do you want to update the titles of all the associated "
                    "sequences to match? Note that this may affect more than "
                    "one tab.")
                response = self.question(title='Linked Entries Renamed',
                                         text=msg,
                                         yes_text="Yes",
                                         no_text="No",
                                         add_cancel_btn=False)
                if response:
                    for seq in view_seqs:
                        new_title = self._renamed_seqs.pop(seq)
                        aln = self.model.getAlignmentOfSequence(seq)
                        aln.renameSeq(seq, new_title)
                else:
                    for seq in view_seqs:
                        self._structure_model.unsynched_seqs.add(seq)
            self._renamed_seqs = {}
[docs]    def moveSelectedSequences(self, direction):
        """
        Move the selected sequences in the given direction
        :param direction: Direction to move sequence
        :type  direction: viewconstants.Direction
        """
        widget = self.currentWidget()
        widget.moveSelectedSequences(direction) 
[docs]    @QtCore.pyqtSlot()
    def replaceSelectionWithGaps(self):
        """
        Replace selected with gaps
        """
        self.currentWidget().replaceSelectionWithGaps() 
[docs]    @QtCore.pyqtSlot()
    def renumberResidues(self):
        """
        Renumber residues of the selected sequences. If there is no selection,
        renumbers all the sequences in MSV workspace.
        """
        window_title = "Renumber Residues"
        widget = self.currentWidget()
        selected_seq = widget.getSelectedSequences()
        if selected_seq:
            sequences = selected_seq
            if len(sequences) == 1:
                seq_name = sequences[0].name
                chain = sequences[0].chain
                title_extension = (f"{seq_name}_{chain}" if chain else seq_name)
            else:
                title_extension = f"{len(sequences)} Sequences Selected"
            window_title = f"{window_title} - {title_extension}"
        else:
            sequences = list(widget.getAlignment())
        self.renumber_residues_dlg = dialogs.RenumberResiduesDialog(sequences)
        self.renumber_residues_dlg.setWindowTitle(window_title)
        self.renumber_residues_dlg.renumberResiduesRequested.connect(
            self._renumberResidues)
        self.renumber_residues_dlg.renumberResiduesByTempRequested.connect(
            self._renumberResiduesByTemplates)
        self.renumber_residues_dlg.renumberResiduesByAntibodyCDRRequested.connect(
            self._renumberResiduesByAntibodyCDR)
        self.renumber_residues_dlg.run(modal=True, blocking=True) 
    @QtCore.pyqtSlot(list, int, int, bool)
    def _renumberResidues(self, sequences, start, incr, preserve_ins):
        """
        Renumbers the residues so that the first residue gets the 'start' number
        and the next gets an increment by 'increment' and so on.
        :param sequences: list of sequences to be renumbered
        :type sequences: list[schrodinger.protein.sequence.ProteinSequence]
        :param start: Starting residue number
        :type start: int
        :param incr: Increase in residue number
        :type incr: int
        :param preserve_ins: Whether to preserve insertion code in the sequence
            or not.
        :type preserve_ins: bool
        """
        for seq in sequences:
            self._structure_model.renumberResidues(seq, start, incr,
                                                   preserve_ins)
    @QtCore.pyqtSlot(list, list)
    def _renumberResiduesByTemplates(self, sequences, templates):
        """
        Renumbers the residues of a sequence based on the residue numbers of
        the template sequence.
        :param sequences: list of sequences to be renumbered
        :type sequences: list[schrodinger.protein.sequence.ProteinSequence]
        :param templates: sequences of template protein
        :type templates: list[schrodinger.protein.sequence.ProteinSequence]
        """
        try:
            for seq, template_seq in zip(sequences, itertools.cycle(templates)):
                self._structure_model.renumberResiduesByTemplate(
                    seq, template_seq)
        except structure_model.RenumberResiduesError:
            self.error("Template sequence is too different from source "
                       "sequence. Please try again with a different template.")
            return
    @QtCore.pyqtSlot(list, list)
    def _renumberResiduesByAntibodyCDR(self, seqs, new_numbers):
        """
        Renumber the sequences based on the Antibody CDR numbering scheme.
        :param seqs: List of sequence to be renumbered
        :type seqs: List[protein.sequence.ProteinSequence]
        :param new_numbers: List of residue numbers per the Antibody CD scheme
            for each sequence to be renumbered.
        :return: List [List[str]]
        """
        for seq, numbers in zip(seqs, new_numbers):
            self._structure_model.renumberResiduesByAntibodyCDR(seq, numbers)
[docs]    @QtCore.pyqtSlot()
    def editSequenceAsPlainText(self):
        widget = self.currentWidget()
        sel_seqs = widget.getSelectedSequences()
        edit_sequence_as_text_dlg = dialogs.EditSequenceAsTextDialog(
            sel_seqs[0])
        edit_sequence_as_text_dlg.editSequenceAsTextRequested.connect(
            self._editSelectedSequence)
        edit_sequence_as_text_dlg.addSequenceAsTextRequested.connect(
            self._addNewSequence)
        edit_sequence_as_text_dlg.run(modal=True, blocking=True) 
    @QtCore.pyqtSlot(sequence.ProteinSequence)
    def _editSelectedSequence(self, seq):
        """
        Applies changes to selected sequence.
        :param seq: sequence with edits
        :type seq: sequence.ProteinSequence
        """
        widget = self.currentWidget()
        curr_alignment = widget.getAlignment()
        sel_seq = widget.getSelectedSequences()[0]
        seq_index = curr_alignment.index(sel_seq)
        desc = f'Edit Sequence {sel_seq.name}'
        with command.compress_command(self.undo_stack, desc):
            curr_alignment.renameSeq(sel_seq, seq.name)
            if str(sel_seq) != str(seq):
                curr_alignment.mutateResidues(seq_index, 0, len(sel_seq),
                                              str(seq))
    @QtCore.pyqtSlot(sequence.ProteinSequence)
    def _addNewSequence(self, seq):
        """
        Adds a new sequence.
        :param seq: sequence with edits
        :type seq: sequence.ProteinSequence
        """
        widget = self.currentWidget()
        widget.importSeqs([seq])
[docs]    @QtCore.pyqtSlot()
    def clearResidueSelection(self):
        widget = self.currentWidget()
        widget.deselectAllResidues() 
[docs]    @QtCore.pyqtSlot()
    def importSequences(self):
        """
        Import sequences and structures from a user-specified file and add them
        to the alignment.  If the user attempts to add structureless sequences
        to the workspace alignment, then we duplicate the tab and add the
        sequences to the duplicate.
        """
        filenames = self._promptForSequenceFilenames()
        if not filenames:
            return
        self.importFiles(filenames) 
    @QtCore.pyqtSlot(bool)
    def _importSequencesAtTop(self, change_reference=False):
        """
        Import sequences and structures from a file and move them to the top of
        the alignment.
        :param change_reference: Whether to change the reference sequence
        """
        widget = self.currentWidget()
        if widget.model.is_workspace:
            raise RuntimeError("This method should only be called from a view "
                               "tab")
        filenames = self._promptForSequenceFilenames()
        if not filenames:
            return
        self._importAbstractSeqAtTop(filenames=filenames,
                                     change_ref=change_reference)
    @QtCore.pyqtSlot(bool)
    def _importTemplateAtTop(self, allow_multiple_files):
        """
        Import templates from file(s) and move them to the top of the alignment.
        :param allow_multiple_files: Whether to import multiple files or not.
        :type allow_multiple_files: bool
        """
        widget = self.currentWidget()
        if widget.model.is_workspace:
            raise RuntimeError("This method should only be called from a view "
                               "tab")
        file_formats = [
            ('PDB', [fileutils.PDB]),
            ("Any file", ['ALL']),
        ]
        if maestro:
            file_formats.insert(1, ("Maestro", [fileutils.MAESTRO]))
        name_filters = fileutils.get_name_filter(
            collections.OrderedDict(file_formats))
        if allow_multiple_files:
            filenames = filedialog.get_open_file_names(
                parent=self,
                caption='Import Templates',
                dir=self._getLatestDirectory(),
                filter=';;'.join(name_filters),
            )
        else:
            filename = filedialog.get_open_file_name(
                parent=self,
                caption='Import Template',
                filter=';;'.join(name_filters),
            )
            filenames = [filename] if filename is not None else None
        if not filenames:
            return
        self._importAbstractSeqAtTop(filenames=filenames)
    def _importAbstractSeqAtTop(self, *, filenames, change_ref=False):
        """
        Import sequences from the given file(s) and move them to the top of
        the alignment.
        :param filenames: List of filenames to be imported
        :param change_ref: Whether to change the reference sequence
        """
        widget = self.currentWidget()
        with dialogs.wait_dialog("Importing sequences, please wait...",
                                 parent=self):
            try:
                seqs = self._structure_model.importFiles(filenames)
            except IOError as err:
                self.error(str(err))
                return
            plural_sequences = inflect.engine().plural("Sequence", len(seqs))
            desc = f"Add {len(seqs)} {plural_sequences} at the Top"
            with command.compress_command(self.undo_stack, desc):
                widget.importSeqs(seqs)
                if change_ref:
                    widget.getAlignment().setReferenceSeq(seqs[0])
                widget.moveSelectedSequences(viewconstants.Direction.Top)
    def _promptForSequenceFilenames(self):
        """
        Show an import dialog and return a list of selected file paths or None.
        The dialog is opened from current working directory or the latest
        imported directory.
        :rtype: list[str] or NoneType
        """
        directory = self._getLatestDirectory()
        import_dialog = dialogs.SequenceImportDialog(parent=self,
                                                     maestro=bool(maestro),
                                                     directory=directory)
        success = import_dialog.exec_()
        if not success:
            return
        self._last_imported_dir = import_dialog.directory().absolutePath()
        return import_dialog.selectedFiles()
    def _getLatestDirectory(self):
        """
        Get the current working directory or the latest imported directory.
        """
        directory = self._last_imported_dir
        cwd = os.getcwd()
        # cwd should only change if the user changes the maestro working
        # directory, so use the new cwd.
        if self._prev_maestro_working_dir != cwd:
            directory = self._prev_maestro_working_dir = cwd
        return directory
[docs]    @QtCore.pyqtSlot()
    def duplicateIntoNewTab(self):
        """
        Duplicate selected sequences in new tab
        """
        seqs_to_copy = self.model.current_page.aln.seq_selection_model.getSelection(
        )
        if not seqs_to_copy:
            return
        self._duplicateIntoNewTab(seqs_to_copy) 
    @QtCore.pyqtSlot(list)
    def _duplicateIntoNewTab(self, seqs):
        """
        Duplicate sequences in new tab
        :param seqs: The sequences to duplicate
        :type seqs: iterable[sequence.ProteinSequence]
        """
        assert seqs
        if self.model.current_page.split_chain_view:
            split_seqs = seqs
        elif isinstance(next(iter(seqs)),
                        sequence.CombinedChainProteinSequence):
            # If combined chain seqs are passed, extract the split chain seqs
            split_seqs = list(
                itertools.chain.from_iterable(cseq.chains for cseq in seqs))
        split_chain_view = self.model.current_page.split_chain_view
        old_aln = self.model.current_page.split_aln
        idxs_to_keep = {old_aln.index(seq) for seq in split_seqs}
        # Deepcopy the alignment and remove non-desired seqs in order to keep as
        # much aln info as possible (e.g. residue selection)
        new_aln = copy.deepcopy(old_aln)
        seqs_to_remove = [
            seq for idx, seq in enumerate(new_aln) if idx not in idxs_to_keep
        ]
        new_aln.removeSeqs(seqs_to_remove)
        desc = "Duplicate into new tab"
        with command.compress_command(self.undo_stack, desc):
            self.query_tabs.createNewTab(aln=new_aln)
            new_widget = self.currentWidget()
            new_widget.model.split_chain_view = split_chain_view
        return new_widget
    @QtCore.pyqtSlot(QtWidgets.QAction)
    def _onDuplicateIntoExistingTabMenuTrigger(self, action):
        """
        Duplicate selected sequences into an existing tab.
        :param action: Action that was triggered in the QMenu.
        :type action: QtWidgets.QAction
        """
        tab_idx = action.data()
        current_widget = self.currentWidget()
        seqs_map = current_widget.copySelectedSeqs()
        if not seqs_map:
            return
        page_to_duplicate_into = self.model.pages[tab_idx]
        cur_aln = current_widget.getAlignment()
        duplicate_into_widget = self.query_tabs.widget(tab_idx)
        new_aln = duplicate_into_widget.getAlignment()
        inflect_engine = inflect.engine()
        seqs_str = inflect_engine.no('sequence', len(seqs_map))
        desc = f"Duplicate {seqs_str} into existing tab"
        with command.compress_command(duplicate_into_widget.undo_stack, desc):
            new_aln.duplicateSeqs(seqs_map,
                                  replace_selection=True,
                                  source_aln=cur_aln)
        self.model.current_page = page_to_duplicate_into
[docs]    @QtCore.pyqtSlot()
    def duplicateResIntoNewTab(self):
        """
        Duplicate the sequences-with only selected residues-to a new tab.
        """
        sel_residues = self.model.current_page.aln.res_selection_model.getSelection(
        )
        seqs_to_copy = {res.sequence for res in sel_residues}
        desc = 'Copy selection to new tab'
        with command.compress_command(self.undo_stack, desc):
            widget = self._duplicateIntoNewTab(seqs_to_copy)
            if isinstance(self._structure_model,
                          structure_model.MaestroStructureModel):
                for seq in widget.model.split_aln:
                    if seq.hasStructure():
                        self._structure_model.unlinkSequence(seq)
            widget.view.deleteUnselectedResidues()
        return widget 
[docs]    @QtCore.pyqtSlot(set)
    @QtCore.pyqtSlot(tuple)
    @QtCore.pyqtSlot(list)
    def translateIntoNewTab(self, seqs):
        """
        Translate the given sequences and add them to a new tab.
        :param seqs: The sequences to translate
        :type seqs: iterable[sequence.NucleicAcidSequence]
        """
        assert seqs
        new_seqs = [
            seq.getTranslation()
            for seq in seqs
            if isinstance(seq, sequence.NucleicAcidSequence)
        ]
        desc = "Translate into new tab"
        with command.compress_command(self.undo_stack, desc):
            self.query_tabs.createNewTab()
            new_widget = self.currentWidget()
            new_widget.importSeqs(new_seqs) 
    @QtCore.pyqtSlot(tuple)
    @QtCore.pyqtSlot(list)
    def _createProteinStructAlignTabs(self, results):
        """
        Create new tabs for protein structure alignment results
        :param results: List of protein structure alignment results
        """
        with command.compress_command(self.undo_stack,
                                      "Add structure alignment results tabs"):
            for idx, result in enumerate(results):
                page = self.model.addViewPage()
                new_title = f"Struct Align {idx + 1}"
                new_index = self.model.pages.index(page)
                self.query_tabs.renameTab(new_index, new_title)
                page.aln.addSeqs((result.ref_seq, result.other_seq))
                page.options.sequence_annotations.add(
                    SEQ_ANNO_TYPES.secondary_structure)
[docs]    @QtCore.pyqtSlot(list)
    def importFiles(self, filenames, wait_dialog=True):
        """
        Import sequences and structures from filenames and add them
        to the alignment.  If the user attempts to add structureless sequences
        to the workspace alignment, then we duplicate the tab and add the
        sequences to the duplicate.
        :param filenames: Iterable of file paths
        :type  filenames: iterable(str)
        :param wait_dialog: Whether to show a wait dialog while importing
        :type  wait_dialog: bool
        """
        with contextlib.ExitStack() as stack:
            if wait_dialog:
                wait_msg = "Importing sequences, please wait..."
                stack.enter_context(dialogs.wait_dialog(wait_msg, parent=self))
            try:
                seqs = self._structure_model.importFiles(filenames)
            except IOError as err:
                self.error(str(err))
                return
            if self._isSeqsInOptimalSize(seqs):
                self._addSeqsToViewTabOrWorkspaceCopy(seqs) 
    def _isSeqsInOptimalSize(self,
                             seqs: List[sequence.ProteinSequence]) -> bool:
        """
        Check if the number of sequences or number of total residues are under
        optimal value.
        """
        total_seq_count, total_res_count = self._getTotalSeqsResCount(seqs)
        if (total_seq_count > OPTIMAL_SEQ_COUNT) or (total_res_count >
                                                     OPTIMAL_RES_COUNT):
            return self.question(
                title="Large Import",
                text=("Importing a large number of sequences or "
                      "sequences with a very large residues "
                      "may degrade performance."
                      "\nContinue anyway?"))
        return True
    def _getTotalSeqsResCount(
            self, seqs: List[sequence.ProteinSequence]) -> Tuple[int, int]:
        """
        Get the total sequences and total residues count in the MSV panel.
        :param seqs: New sequences that are being imported to MSV.
        """
        pages = self.model.pages
        existing_seqs = [seq for page in pages for seq in page.aln]
        total_seqs = existing_seqs + seqs
        total_seqs_count = len(total_seqs)
        total_res_count = sum(len(seq) for seq in total_seqs)
        return total_seqs_count, total_res_count
[docs]    @QtCore.pyqtSlot()
    def onImportIncludedRequested(self):
        """
        Callback method when import of included entries in workspace is
        requested for the current tab.
        """
        self.currentWidget().importIncluded() 
[docs]    @QtCore.pyqtSlot()
    def onImportSelectedRequested(self):
        """
        Callback method when import of currently selected entries in PT is
        requested for the current tab.
        """
        self.currentWidget().importSelected() 
    @QtCore.pyqtSlot(list)
    def _addSeqsToViewTabOrWorkspaceCopy(self, sequences):
        """
        Add sequences to current tab or duplicated tab.
        If the current tab is the workspace tab and some of the sequences are
        structureless, make a copy of the workspce tab and import the sequences
        into that.
        :param sequences: Sequences to add.
        :type  sequences: iterable[sequence.Sequence]
        """
        widget = self.currentWidget()
        if widget.isWorkspace():
            structureless = [seq for seq in sequences if not seq.hasStructure()]
            # If everything has a structure, then we're done. The imported
            # sequences have already been added to the workspace tab since their
            # structures were added to the workspace during the structure model
            # import.
            if structureless:
                # The user just tried to import structureless sequences into the
                # workspace tab.  Since we can't do that, make a copy of the
                # workspace tab and import the sequences into that.
                self._addSeqsToCopyOfWorkspaceTab(structureless)
        else:
            widget.importSeqs(sequences)
    def _addSeqsToCopyOfWorkspaceTab(self, sequences):
        """
        Copy the workspace tab and add the sequences to it.
        If the current tab is not the workspace tab, has no effect.
        :param sequences: Sequences to add
        :type  sequences: list(protein.Sequence)
        """
        if not self.currentWidget().isWorkspace():
            raise RuntimeError(
                "Called _addSeqsToCopyOfWorkspaceTab from non-workspace tab")
        self.query_tabs.duplicateTab(0)
        new_tab = self.query_tabs.widget(len(self.query_tabs.model.pages) - 1)
        new_tab.importSeqs(sequences)
[docs]    def resetAlignSettings(self):
        """
        Reset the alignment settings
        """
        self.model.align_settings.reset() 
[docs]    @QtCore.pyqtSlot()
    def getSequences(self):
        """
        Opens the widget's Get Sequences dialog.
        If the current tab is the workspace, the tab will be duplicated and the
        sequences will be added to the copy. If the current tab is a query tab,
        the sequences will be added to it.
        """
        self.get_sequences_dialog.exec() 
[docs]    @QtCore.pyqtSlot()
    def pasteSequences(self):
        """
        Opens up the paste sequences dialog and adds the sequences pasted
        to the current alignment.
        """
        if self._paste_sequence_dialog is None:
            self._paste_sequence_dialog = dialogs.PasteSequenceDialog()
            self._paste_sequence_dialog.addSequencesRequested.connect(
                self._addSeqsToViewTabOrWorkspaceCopy)
        self._paste_sequence_dialog.run(modal=True, blocking=True) 
[docs]    @QtCore.pyqtSlot()
    def copySelection(self):
        """
        Calls widget.copySelection if the widget exists.
        """
        widget = self.currentWidget()
        widget.copySelection() 
[docs]    @QtCore.pyqtSlot()
    def deleteTab(self):
        """
        Removes the current tab. If it's the last tab, resets it.
        """
        curr_tab_idx = self.query_tabs.currentIndex()
        self.query_tabs.onTabCloseRequested(curr_tab_idx) 
[docs]    @QtCore.pyqtSlot(str)
    def requestFind(self, pattern):
        """
        Find pattern in sequences and select the matches.
        :param pattern: PROSITE pattern (see
            `protein.sequence.find_generalized_pattern` for documentation).
        :type pattern: str
        """
        success, message = self.currentWidget()._selectPattern(pattern)
        if not success:
            if not message:
                message = "Unknown error"
            self.warning(title="Error in pattern search", text=message)
            return
        self.toolbar.setPatternFound() 
        # TODO MSV-1508: enter "patterns found mode" (remove residue BG color)
        # TODO MSV-1524: auto-scroll to first match
[docs]    @QtCore.pyqtSlot(list, bool)
    @QtCore.pyqtSlot(str, bool)
    @QtCore.pyqtSlot(list)
    @QtCore.pyqtSlot(str)
    def requestFetch(self, ids, local_only=None):
        """
        Fetch ID(s) from PDB, Entrez, or UniProt.
        :param ids: Database ID or IDs (comma-separated str or list)
        :type ids: str or list
        :param local_only: Whether only local download is allowed or None to
            determine the value based on fetch settings
        :type local_only: bool or NoneType
        """
        fetch_ids = seqio.process_fetch_ids(ids, dialog_parent=self)
        if fetch_ids is None:
            return
        if local_only is None:
            seq_remote_ok = not self.model.sequence_local_only
            pdb_remote_ok = not self.model.pdb_local_only
        else:
            seq_remote_ok = pdb_remote_ok = not local_only
        seq_result, pdb_result = dialogs.download_seq_pdb(
            fetch_ids,
            parent=self,
            seq_remote_ok=seq_remote_ok,
            pdb_remote_ok=pdb_remote_ok)
        seq_paths = seq_result.paths
        pdb_paths = pdb_result.paths
        seq_error_ids = seq_result.error_ids
        pdb_error_ids = pdb_result.error_ids
        self.currentWidget().loadPdbs(pdb_paths)
        for seq_path in seq_paths:
            seqs = self._structure_model.importFile(seq_path)
            self._addSeqsToViewTabOrWorkspaceCopy(seqs)
        remote_error_ids = []
        if pdb_error_ids:
            if pdb_remote_ok:
                remote_error_ids.extend(pdb_error_ids)
            else:
                dialogs.LocalPDBNoResultsMessageBox(
                    self, ", ".join(pdb_error_ids),
                    count=len(pdb_error_ids)).exec()
        if seq_error_ids:
            if seq_remote_ok:
                remote_error_ids.extend(seq_error_ids)
            else:
                dialogs.LocalFetchNoResultsMessageBox(
                    self, ", ".join(seq_error_ids),
                    count=len(pdb_error_ids)).exec()
        if remote_error_ids:
            self.warning(title="No Matching Sequences Found",
                         text="Could not retrieve the following ID(s) from "
                         f"the remote server: {', '.join(remote_error_ids)}") 
[docs]    @QtCore.pyqtSlot()
    def getPDB(self):
        """
        Opens the GetPDB dialog. The PDB structures imported into the MSV will
        will automatically be incorporated into Maestro.
        """
        ok = self.pdb_dialog.exec_()
        if not ok:
            return
        pdb_file_name = self.pdb_dialog.pdb_filepath
        if pdb_file_name:
            widget = self.currentWidget()
            widget.loadPdbs(pdb_file_name.split(';')) 
    @QtCore.pyqtSlot()
    def _fetchPdbStrucsForSelSeqs(self):
        """
        Fetch PDB structures for the currently selected sequences.
        """
        cur_wid = self.current_widget
        valid_id_map = cur_wid.getValidIdMap()
        if not valid_id_map:
            self.warning(
                "No PDBs to retrieve: the selected sequences either lack PDB IDs or have structures already."
            )
            return
        remote_ok = not self.model.pdb_local_only
        fetch_ids = seqio.process_fetch_ids(valid_id_map.keys(),
                                            dialog_parent=self)
        _, pdb_result = dialogs.download_seq_pdb(fetch_ids,
                                                 parent=self,
                                                 pdb_remote_ok=remote_ok)
        pdb_map = pdb_result.path_map
        error_ids = pdb_result.error_ids
        desc = "Get PDB Structures"
        with command.compress_command(self.undo_stack, desc):
            for pdb_id, orig_seq in valid_id_map.items():
                fpath = pdb_map.get(pdb_id)
                if not fpath:
                    continue
                self._structure_model.loadFileAndLink(fpath, orig_seq)
        if error_ids:
            if remote_ok:
                self.warning(title="No Matching Structures Found",
                             text="Could not retrieve the following ID(s) from "
                             f"the remote server: {', '.join(error_ids)}")
            else:
                dialogs.LocalPDBNoResultsMessageBox(
                    self, ", ".join(error_ids), count=len(error_ids)).exec()
    def _fetchDomainsWithWarning(self):
        """
        Try to fetch domain info for sequences in current widget. Show
        warnings if it fails to fetch any domain info
        """
        if not len(self.model.current_page.aln):
            return
        error_ids = self._fetchDomains()
        # if not all sequences failed, dont need to show messages
        if len(error_ids) < len(self.model.current_page.aln):
            return
        if self.model.sequence_local_only:
            if self._should_show_domain_download_error_local_only:
                dialogs.LocalFetchDomainsNoResultsMessageBox(self).exec()
                self._should_show_domain_download_error_local_only = False
        elif self._should_show_domain_download_error:
            dialogs.FetchDomainsNoResultsMessageBox(self).exec()
            self._should_show_domain_download_error = False
    @QtCore.pyqtSlot()
    def _fetchDomains(self):
        """
         Try to fetch domain info for sequences in current widget.
        :return: a list of sequences that failed to fetch domain info
        :rtype: list[sequence.ProteinSequence]
        """
        remote_ok = not self.model.sequence_local_only
        error_seqs = []
        for seq in self.model.current_page.split_aln:
            try:
                # TODO: use accession number
                seq_id = seq.long_name.split('|')[1]
            except IndexError:
                error_seqs.append(seq)
                continue
            try:
                domain_file = seqio.SeqDownloader.downloadUniprotSeq(
                    seq_id, remote_ok, use_xml=True)
                if domain_file is None:
                    raise seqio.GetSequencesException
            except seqio.GetSequencesException:
                error_seqs.append(seq)
            else:
                seq.annotations.parseDomains(domain_file)
        return error_seqs
[docs]    def onMovePatternClicked(self, forward=True):
        """
        Callback for prev_pattern and next_pattern buttons.
        :param forward: whether to move pattern view forward
        :type forward: bool
        """
        self.currentWidget().movePattern(forward=forward) 
[docs]    @QtCore.pyqtSlot()
    def updateOtherTabs(self):
        """
        Update the total number of seqs and tabs info
        """
        n_other_tabs = 0
        n_other_seqs = 0
        for page in self.model.pages:
            if page is self.model.current_page:
                continue
            n_other_tabs += 1
            aln = page.aln
            if aln is not None:
                n_other_seqs += len(aln)
        self.msv_status_bar.panel_status.setValue(
            dict(num_seqs_in_other_tabs=n_other_seqs, num_tabs=n_other_tabs)) 
[docs]    @QtCore.pyqtSlot(object)
    def onResidueHovered(self, res):
        # Note: terminal gaps are None
        if res is None or res.is_gap:
            self.onResidueUnhovered()
        else:
            self.msv_status_bar.enterHoverMode()
            residue_text = "{0.long_code} {0.resnum}{0.inscode}".format(res)
            self.msv_status_bar.setResidue(residue_text.strip())
            # Chain information needs to come from the split sequence even in
            # combined chain mode
            try:
                seq = res.split_sequence
            except AttributeError:
                seq = res.sequence
            if seq is not None:
                self.msv_status_bar.setChain(seq.chain)
                self.msv_status_bar.setSequence(seq.name) 
[docs]    @QtCore.pyqtSlot()
    def onResidueUnhovered(self):
        self.msv_status_bar.setResidue("")
        self.msv_status_bar.setChain("")
        self.msv_status_bar.setSequence("")
        self.msv_status_bar.exitHoverMode() 
[docs]    @QtCore.pyqtSlot(bool)
    def setEditMode(self, enable):
        """
        Enable or disable edit mode.
        :param enable: Whether to enable edit mode.
        :type enable: bool
        """
        current_wid = self.currentWidget()
        if not current_wid:
            return
        self.toolbar.setEditMode(enable)
        current_wid.setEditMode(enable)
        # we currently don't allow editing in three-letter mode
        if enable:
            options_model = current_wid.options_model
            if options_model.res_format is ResidueFormat.ThreeLetter:
                options_model.res_format = ResidueFormat.OneLetter 
    @QtCore.pyqtSlot(object)
    def _onEditModeChanged(self, enable):
        """
        Autosave the project when edit mode changes to off
        """
        if enable:
            return
        self._autosaveProject()
    @QtCore.pyqtSlot(bool)
    def _toggleEditMode(self, toggle):
        self.model.edit_mode = toggle
[docs]    @QtCore.pyqtSlot()
    def enableEditMode(self):
        self.model.edit_mode = True 
[docs]    @QtCore.pyqtSlot()
    def disableEditMode(self):
        self.model.edit_mode = False 
[docs]    def updateTabEditMode(self):
        """
        Update edit mode in the current tab and the enabled/disabled status of
        buttons in the edit toolbar in response to changing tabs.
        """
        cur_widget = self.currentWidget()
        # we currently don't allow editing in three-letter mode, so switch out
        # of edit mode if switching to a tab in three-letter mode
        if (self.model.edit_mode and cur_widget.options_model.res_format is
                viewconstants.ResidueFormat.ThreeLetter):
            self.disableEditMode()
        else:
            cur_widget.setEditMode(self.model.edit_mode) 
    @QtCore.pyqtSlot()
    def _residueFormatChanged(self):
        """
        Disable edit mode if the user switches to three-letter mode.
        """
        if (self.model.edit_mode and
                self.currentWidget().options_model.res_format is
                viewconstants.ResidueFormat.ThreeLetter):
            self.disableEditMode()
[docs]    @QtCore.pyqtSlot()
    def showHomologyModeling(self, show_view_tab=False):
        """
        Shows the homology modeling pane.
        :param show_view_tab: Whether to show View tab or not.
        :type show_view_tab: bool
        """
        if not self.model.current_page.split_chain_view:
            response = self.question(
                text="Homology modeling is not yet supported in combined chain "
                "mode. Press OK to return to split chain mode and open the "
                "homology modeling panel.",
                title="Homology Modeling")
            if response:
                self.model.current_page.split_chain_view = True
            else:
                return
        if self.model.current_page.is_workspace:
            if show_view_tab:
                self.query_tabs.setCurrentIndex(1)
            else:
                response = self.question(
                    text=
                    "The Workspace tab cannot be used for homology modeling. "
                    "Do you want to copy the Workspace contents into a View tab "
                    "now?",
                    title="Homology Modeling")
                if response:
                    self.model.duplicatePage(0)
                else:
                    return
        if self.model.current_page.aln.anyHidden():
            response = self.question(
                text="Homology Modeling cannot be used with hidden sequences. "
                "Show all sequences now?",
                title="Homology Modeling")
            if response:
                self.model.current_page.aln.showAllSeqs()
            else:
                return
        if self._homology_pane is None:
            pane = homology_panel.HomologyPanel(msv_tab_widget=self.query_tabs)
            self._homology_pane = pane
            pane.importSeqRequested.connect(self._importSequencesAtTop)
            pane.importTemplateRequested.connect(self._importTemplateAtTop)
            pane.showBlastResultsRequested.connect(self.displayBlastResults)
            pane.copySeqsRequested.connect(self._copyHeteromultimerSeqs)
            pane.taskStarted.connect(self._watchHomologyTask)
            self.addDockWidget(QtCore.Qt.RightDockWidgetArea, pane)
        self._homology_pane.setModel(self.model)
        self._homology_pane.adjustSize()
        self._homology_pane.show() 
    @QtCore.pyqtSlot()
    def _copyHeteromultimerSeqs(self):
        """
        Copy selected sequences into new tabs for heteromultimer homology
        modeling.
        """
        pairs = self._getHeteromultimerPairs()
        if pairs is None:
            return
        og_current_page = self.model.current_page
        og_selected = list(self.model.heteromultimer_settings.selected_pages)
        new_pages = []
        for seqs in pairs:
            new_widget = self._duplicateIntoNewTab(seqs)
            new_pages.append(new_widget.model)
            self.model.current_page = og_current_page
        self.model.heteromultimer_settings.selected_pages.extend(og_selected)
        self.model.heteromultimer_settings.selected_pages.extend(new_pages)
    def _getHeteromultimerPairs(self):
        """
        Process selected sequences into valid heteromultimer pairs to copy into
        new tabs.
        The selection must be either 1 or 2 sequences, or an even number of
        sequences alternating between structureless and structured (a series of
        target/template pairs).
        :return: Pairs of sequences or None if sequences are not valid.
        :rtype: list[tuple(sequence.ProteinSequence, sequence.ProteinSequence)]
            or NoneType
        """
        pairs = None
        if not maestro:
            # TODO MSV-3239
            self.warning("Heteromultimer homology modeling does not work "
                         "standalone, please open MSV in Maestro.")
            return pairs
        aln = self.model.current_page.aln
        if not aln.seq_selection_model.hasSelection():
            return pairs
        seqs = aln.getSelectedSequences()  # in order
        msg = None
        if len(seqs) <= 2:
            pairs = [seqs]
        elif len(seqs) % 2:
            msg = ("Copy Selected requires one or an even number of selected "
                   "sequences.")
        else:
            possible_pairs = list(zip(*[iter(seqs)] * 2))
            for seq1, seq2 in possible_pairs:
                if seq1.hasStructure() or not seq2.hasStructure():
                    msg = ("Copy Selected requires sequences to be ordered "
                           "Target 1, Template 1, Target 2, Template 2, etc.")
                    break
            else:
                pairs = possible_pairs
        if msg is not None:
            self.warning(msg)
        return pairs
    @QtCore.pyqtSlot(AbstractTask)
    def _watchHomologyTask(self, task):
        self._task_status_bar.watchTask(task)
        task.statusChanged.connect(self._onHomologyTaskStatusChanged)
[docs]    def importHomologyResult(self, job_id, entry_id):
        """
        Import a homology modeling entry into the tab it was launched from, or
        the current tab if no associated tab is found.
        """
        job_page = self._homology_pane.getHomologyJobPage(job_id)
        if job_page is not None and job_page in self.model.pages:
            # Switch to launch tab if possible
            self.model.current_page = job_page
        widget = self.currentWidget()
        cmd = f"entrywsincludeonly entry {entry_id}"
        maestro.command(cmd)
        if not widget.isWorkspace():
            # This imports the included entry if it isn't already in the tab
            widget.importIncluded(replace_selection=False) 
    @QtCore.pyqtSlot(object)
    def _onHomologyTaskStatusChanged(self, status):
        """
        Show dialogs about homology status only if MSV is visible
        """
        if not self.isVisible():
            return
        if status is TaskStatus.FAILED:
            self.warning("Homology modeling failed")
        elif status is TaskStatus.DONE and not maestro:
            self.info("Homology modeling finished")
    @QtCore.pyqtSlot(object)
    def _onPickClosed(self, pick_mode):
        self.model.current_page.options.pick_mode = None
    def _updateToolbarPickMode(self, pick_mode):
        if pick_mode is None:
            self.toolbar.exitPickMode(pick_mode)
        else:
            self.toolbar.enterPickMode(pick_mode)
    @QtCore.pyqtSlot()
    def _onCurrentPageReplaced(self):
        if self.model.current_page.isNullPage():
            return
        self.setCalculatedModels()
        self.updateTabEditMode()
        self.updateMenuActions()
        self.updateToolbar()
        self._updateDendrogramViewer()
        self._disablePairwisePicking()
        self.onResHighlightStatusChanged()
        self._onHiddenSeqsChanged()
        self.setProperty("on_ws_tab", self.model.current_page.is_workspace)
        widgets = [self._tab_bar, self.toolbar]
        for widget in widgets:
            qt_utils.update_widget_style(widget)
    def _disablePairwisePicking(self):
        # Disable pairwise picking for all pages when switching tabs
        # to avoid getting into an undefined state
        for page in self.model.pages:
            if page.options.pick_mode == picking.PickMode.Pairwise:
                # This will disable set_constraints via OptionsModel
                page.options.pick_mode = None
    @QtCore.pyqtSlot(object)
    def _onSetConstraintsChanged(self, set_constraints):
        """
        Update pick mode when set_constraints is toggled.
        """
        if set_constraints and not self._canSetConstraints():
            # Top-level settings are synced with the pages, so use a
            # single-shot timer to disable set constraints after the sync
            QtCore.QTimer.singleShot(0, self._disableSetConstraints)
            # Warning was already shown
            return
        pick_mode = picking.PickMode.Pairwise if set_constraints else None
        self.model.current_page.options.pick_mode = pick_mode
    @QtCore.pyqtSlot()
    def _disableSetConstraints(self):
        self.model.align_settings.pairwise.set_constraints = False
    @QtCore.pyqtSlot()
    def _warnIfCantSetConstraints(self):
        """
        If "Set constraints" is on but the state is no longer valid, warn the
        user and turn off "Set constraints"
        """
        align_settings = self.model.align_settings
        if not align_settings.pairwise.set_constraints:
            return
        if not self._canSetConstraints():
            align_settings.pairwise.set_constraints = False
    def _canSetConstraints(self):
        aln = self.model.current_page.aln
        align_settings = self.model.align_settings
        num_seqs = len(aln)
        if num_seqs == 2:
            return True
        if num_seqs < 2:
            self.warning('Two sequences are required for alignments.')
            return False
        if not align_settings.align_only_selected_seqs:
            self._showCantSetConstraintsWarning()
            return False
        ref_seq = aln.getReferenceSeq()
        num_sel_nonref_seqs = 0
        for seq in aln.seq_selection_model.getSelection():
            if seq != ref_seq:
                num_sel_nonref_seqs += 1
        if num_sel_nonref_seqs != 1:
            self._showCantSetConstraintsWarning()
            return False
        return True
    def _showCantSetConstraintsWarning(self):
        self.warning(
            'Constraints cannot be used with multiple pairwise alignments. '
            'Ensure that a single sequence is selected and "Selected only" '
            'is checked before requesting constraints.')
[docs]    @QtCore.pyqtSlot()
    def disableFindSequence(self):
        self.model.current_page.options.seq_filter_enabled = False 
[docs]    def setExpansionAll(self, expand=True):
        """
        Set the expansion state of all sequences
        """
        widget = self.currentWidget()
        widget.setSequenceExpansionState(expand=expand) 
[docs]    def setExpansionSelected(self, expand=True):
        """
        Set the expansion state of the selected sequences
        """
        widget = self.currentWidget()
        selected = widget.getSelectedSequences()
        widget.setSequenceExpansionState(selected, expand=expand) 
[docs]    def setExpansionUnselected(self, expand=True):
        """
        Set the expansion state of the unselected sequences
        """
        widget = self.currentWidget()
        aln = widget.getAlignment()
        selected = aln.seq_selection_model.getSelection()
        unselected = set(aln) - selected
        widget.setSequenceExpansionState(unselected, expand=expand) 
[docs]    def resetViewToDefaults(self):
        """
        Reset the options controlled by the Annotations, Colors, and View popups
        """
        widget = self.currentWidget()
        model = widget.model
        should_toggle_split_chain = not model.isSplitChainViewDefault()
        if (should_toggle_split_chain and
                not self.view_dialog.canToggleSplitChains()):
            # the user clicked Cancel in a confirmation dialog
            return
        desc = "Reset View to Defaults"
        with command.compress_command(self.undo_stack, desc):
            self.quick_annotate_dialog.resetMappedParams()
            self.color_dialog.resetMappedParams()
            widget.clearAllHighlights()
            self.view_dialog.resetMappedParams()
            if should_toggle_split_chain:
                model.split_chain_view = not model.split_chain_view 
    @QtCore.pyqtSlot()
    def _openHelpPage(self):
        """
        Open the help page.
        """
        help_page = self.sender().data()
        qt_utils.help_dialog(help_page)
    @QtCore.pyqtSlot()
    def _openMSVGuide(self):
        """
        Open the MSV Guide web page.
        """
        url = self.sender().data()
        documentation.open_url(url)
    #===========================================================================
    # Debug Utilities
    #===========================================================================
[docs]    def startIPythonSession(self):
        QtWidgets.QApplication.instance().processEvents()  # Let menu close
        header_msg = '=' * 18 + 'You have started an interactive IPython session' + '=' * 18
        print(header_msg)
        current_widget = self.currentWidget()
        viewmodel = current_widget._table_model
        metrics_model = viewmodel.metrics_model
        info_model = viewmodel.info_model
        export_model = viewmodel.export_model
        top_model = viewmodel.top_model
        wrap_model = viewmodel._wrap_proxy
        anno_model = viewmodel._annotation_proxy
        base_model = viewmodel._base_model
        current_alignment = current_widget.getAlignment()
        def suppress_unused(*args):
            pass
        suppress_unused(msv_rc, metrics_model, info_model, export_model,
                        top_model, wrap_model, anno_model, base_model,
                        current_alignment)
        helpmsg = self.printIPythonHelp
        helpmsg()
        from schrodinger.Qt import QtCore
        QtCore.pyqtRemoveInputHook()
        import IPython
        IPython.embed()
        QtCore.pyqtRestoreInputHook() 
[docs]    def printIPythonHelp(self):
        help_msg = """
            The following variables have been defined for your convenience:
            -current_widget
            -viewmodel
            -export_model
            -info_model
            -metrics_model
            -top_model
            -wrap_model
            -anno_model
            -base_model
            -current_alignment
            When you are finished with your session, enter 'exit' to continue.
        """
        print(help_msg) 
[docs]    def showDebugGui(self):
        af2.debug.start_gui_debug(self) 
[docs]    def toggleHistory(self):
        """
        Toggle the history widget's visibility
        """
        self.history_widget.setVisible(not self.history_widget.isVisible()) 
    #===========================================================================
    # Command machinery
    #===========================================================================
[docs]    def undo(self):
        """
        Undo the last operation
        """
        if self.undo_stack.canUndo():
            self.undo_stack.undo() 
[docs]    def redo(self):
        """
        Redo the last undone operation
        """
        if self.undo_stack.canRedo():
            self.undo_stack.redo() 
[docs]    def focusInEvent(self, event):
        super().focusInEvent(event)
        self._save_project_timer.start() 
[docs]    def focusOutEvent(self, event):
        super().focusOutEvent(event)
        self._save_project_timer.stop()  
MSV_HOMOLOGY_VIEWNAME = HM_VIEWNAME  # Used as key for job incorp callback
[docs]def homology_job_completed_callback(jobdata):
    job = jobdata.getJob()
    if not job.succeeded():
        return
    jmgr = jobhub.get_job_manager()
    jmgr.incorporateJob(jobdata)
    # TODO do we really need this as a separate call?
    jobdata.setIncorporationAccepted()
    entry_ids = jobdata.getEntryIdsToInclude()
    if entry_ids:
        # Prevent Maestro banner
        jobdata.setNotificationAccepted()
        show_homology_banner(job.JobId, entry_ids[0]) 
[docs]def homology_job_incorporated_callback(job_id, first_entry_id, last_entry_id):
    show_homology_banner(job_id, first_entry_id) 
[docs]def show_homology_banner(job_id, first_entry_id):
    text_1 = "Homology modeling job has finished"
    action_1 = "Review Model..."
    module = "schrodinger.application.msv.gui.msv_gui"
    command_1 = f"{module}.homology_job_review_model {job_id} {first_entry_id}"
    action_2 = "Refine Loops..."
    command_2 = f"{module}.homology_job_refine_loops {first_entry_id}"
    maestro_hub = maestro_ui.MaestroHub.instance()
    maestro_hub.emitAddBanner(text_1, '', action_1, command_1, action_2,
                              command_2, True)
    p = MSVPanel.getPanelInstance(create=False)
    if p is not None and p.isVisible():
        p.importHomologyResult(job_id, first_entry_id)
        QtCore.QTimer.singleShot(100,
                                 lambda: p.info("Homology modeling finished")) 
[docs]def homology_job_review_model(job_id, entry_id):
    """
    Callback for homology job incorporated banner action
    """
    p = MSVPanel.getPanelInstance()
    p.show()  # force creating structure model
    p.importHomologyResult(job_id, entry_id)
    p.run() 
[docs]def homology_job_refine_loops(entry_id):
    """
    Callback for homology job incorporated banner action
    """
    commands = (f"entrywsincludeonly entry {entry_id}", "showpanel refinement",
                "psprsdefaulthelixloops")
    for cmd in commands:
        maestro.command(cmd) 
panel = MSVPanel.panel
if __name__ == '__main__':
    panel()