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