Source code for schrodinger.application.msv.gui.msv_gui

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 QtGui
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 _onAutosaveChanged(self): save_always = self.model.auto_save is viewconstants.Autosave.Regularly 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] def setWidgetLayout(self): """ Set the widget layout. A QMainWindow's layout does not allow nested layouts so we create a dummy widget and set it as the central widget. """ # InitMixin top_level_widget = QtWidgets.QWidget() top_level_widget.setContentsMargins(0, 0, 0, 0) top_level_widget.setLayout(self.widget_layout) self.setCentralWidget(top_level_widget) sp = self.centralWidget().sizePolicy() sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Ignored) self.centralWidget().setSizePolicy(sp)
[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._requestAutoSave), (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.auto_save_group, M.auto_save), (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._onAutosaveChanged, M.auto_save) ] # yapf:disable
[docs] def contextMenuEvent(self, e): """ Propagate context menu events to the appropriate child. Suppress default behavior of showing a menu to toggle toolbars and dock widgets. """ if self.childAt(e.pos()) is self._tab_bar: self._tab_bar.contextMenuEvent(e)
[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() def _requestAutoSave(self): """ Save msv project unless the user has set the option to never save the project. """ if self.model.auto_save is viewconstants.Autosave.Never: return self._autosaveProject() @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.")
[docs] @QtCore.pyqtSlot() def updateToolbar(self): """ Update the toolbar for the current widget. """ page = self.model.current_page if page.options.seq_filter_enabled: self.toolbar.enterFindSeqToolbarMode() elif isinstance(self._structure_model, structure_model.StandaloneStructureModel): self.toolbar.enterStandaloneToolbarMode() elif page.is_workspace: self.toolbar.enterWorkspaceToolbarMode() else: self.toolbar.enterViewToolbarMode()
[docs] def updateMenuActions(self): """ Update the current menu options for the current widget and disable the "Unlink From Entry" and "Get Structure from PDB" actions in the workspace view. """ page_not_ws = not self.model.current_page.is_workspace acts = self.menu.menu_actions for ma in [ acts.edit_sequence, acts.delete_sub, ]: ma.setEnabled(page_not_ws) self._updateDuplicateIntoExistingMenuActions() menu_model = self.menu.model menu_model.can_get_pdb_sts = bool( self.current_widget.getValidIdMap()) and page_not_ws menu_model.can_link_or_unlink_sequences = page_not_ws and bool(maestro) for ma in [ acts.import_from_maestro, acts.import_from_maestro_workspace, acts.import_from_maestro_selected, acts.select_link_seq_to_entries ]: ma.setEnabled(page_not_ws and bool(maestro))
@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 = QtGui.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._requestAutoSave)
[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(QtGui.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] def currentWidget(self): """ Return the widget belonging to the currently active tab, or None if all tabs are closed :rtype: `schrodinger.application.msv.gui.msv_widget.AbstractMsvWidget` or None :return: The current msv widget or None """ return self.query_tabs.currentWidget()
[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(QtGui.QAction) def _onDuplicateIntoExistingTabMenuTrigger(self, action): """ Duplicate selected sequences into an existing tab. :param action: Action that was triggered in the QMenu. :type action: QtGui.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._requestAutoSave() @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] @QtCore.pyqtSlot() def showColorPopup(self): self.config_toggles.sequence_colors_btn.showPopUpDialog()
[docs] def showAnnotationPopup(self): self.config_toggles.annotations_btn.showPopUpDialog()
[docs] def showViewPopup(self): self.config_toggles.options_btn.setChecked(True)
[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()