"""
The simplified version of the sequence viewer used in the MSV.
Also provides access to the viewer as a `QtWidgets.QMainWindow` and
as a `QtWidgets.QDockWidget`.
"""
# Contributors: Joshua Williams, Matvey Adzhigirey
#- Imports -------------------------------------------------------------------
import schrodinger.ui.sequencealignment.globals as sv_globals
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtWidgets
from schrodinger.ui.sequencealignment import maestro as sv_maestro
from schrodinger.ui.sequencealignment.sequence_viewer import SequenceViewer
from .. import bwidgets
from ..actions import configs as action_config
from ..actions.factory import Factory
from .toolbars import AntibodyNumberingToolBar
from .toolbars import ConsensusToolBar
from .toolbars import FindToolBar
from .toolbars import ImportToolBar
try:
from schrodinger.maestro import maestro
except ImportError:
maestro = None # For testing outside of Maestro
#- Functions -----------------------------------------------------------------
[docs]def catch_last_added(fn):
"""
A decorator for action callbacks that deal with importing sequences into
the sequence viewer. The decorator will add a `last_sequences_added`
property to the class of the decorated method. This will store all
sequences added to the viewer in the last import step.
This must decorate a class with a `sequence_group` property.
"""
def dec(self, *args, **kwargs):
# Clear the last imported sequences
self.last_sequences_imported = []
# Define the current sequences to compare later
current_seqs = []
for seq in self.sequence_group.sequences:
current_seqs.append(seq)
# Run the decorated method
result = fn(self, *args, **kwargs)
# Find the new sequences and set to last added sequences
for seq in self.sequence_group.sequences:
if seq not in current_seqs and not seq.isRuler():
self.last_sequences_imported.append(seq)
return result
return dec
#- Classes -------------------------------------------------------------------
[docs]class ViewerWindow(QtWidgets.QMainWindow):
"""
Provides the `SimplifiedSequenceViewer` with a window to occupy.
This has the benefit of adding toolbars and allowing the sequence
viewer (which is a `QtWidgets.QSplitter`) to be a stand-alone window.
"""
WINDOW_TITLE = 'Sequence Viewer'
IMPORT_TOOLBAR = 'import_toolbar'
UNDO_REDO_TOOLBAR = 'undo_redo_toolbar'
FIND_TOOLBAR = 'find_toolbar'
ALIGN_TOOLBAR = 'align_toolbar'
CONSENSUS_TOOLBAR = 'consensus_toolbar'
ANTIBODY_NUM_TOOLBAR = 'antibody_numbering_toolbar'
BREAK_TOOLBAR = 'break_toolbar'
DEFAULT_TOOLBARS = [
IMPORT_TOOLBAR,
UNDO_REDO_TOOLBAR,
FIND_TOOLBAR,
ALIGN_TOOLBAR,
]
[docs] def __init__(self, parent):
"""
:param parent: The sequence viewer object that will use this window
:type parent: `SimplifiedSequenceViewer`
:raise RuntimeError: If `parent` is not correct type
"""
if not isinstance(parent, SequenceViewer):
msg = 'The parent must be an instance of the class:\n'
msg += 'schrodinger.ui.sequencealignment.sequence_viewer.SequenceViewer'
raise RuntimeError(msg)
self.viewer = parent
viewer_parent = parent.parent()
super(ViewerWindow, self).__init__(viewer_parent)
# Try to set the window title in the context of the parent's parent
# title. For example if the parent's title is "Antibody Prediction"
# this would be "Antibody Prediction - Sequence Viewer"
try:
title = '%s - %s' % (viewer_parent.windowTitle(), self.WINDOW_TITLE)
except:
title = self.WINDOW_TITLE
self.setWindowTitle(title)
self.setCentralWidget(self.viewer)
self.action_factory = Factory(self.viewer)
"""
The factory to use when creating actions. We want to set all the
action's parent to the passed in parent, which is a SequenceViewer.
All of the actions associated with the viewer are the module,
schrodinger.ui.sequencealignment.sequence_viewer.
"""
self.resize(700, 400)
[docs] def addAntibodyNumberingToolBar(self, area=QtCore.Qt.BottomToolBarArea):
"""
Add a `AntibodyNumberingToolBar` widget to the area indicated
(default: bottom).
:param area: The area to add the dock widget to
"""
toolbar = AntibodyNumberingToolBar(self.viewer)
toolbar.setObjectName(self.ANTIBODY_NUM_TOOLBAR)
self.addToolBar(area, toolbar)
[docs] def setAntibodyNumberingToolBarVisible(self, show=True):
toolbar_name = self.ANTIBODY_NUM_TOOLBAR
toolbar = self.findChild(QtWidgets.QToolBar, toolbar_name)
if not toolbar:
return
if show:
toolbar.showScheme()
toolbar.show()
else:
toolbar.hideScheme()
toolbar.hide()
[docs]class SimplifiedSequenceViewer(SequenceViewer):
"""
Creates a sequence viewer that can be opened by an action or added to a main
window as a dockable item. Here is an example of how to add a dockable item
to a QMainWindow, or AppFramework window::
from schrodinger.applications.bioluminate import sequence_viewer
self.sequence_viewer = sequence_viewer.SimplifiedSequenceViewer(<window>)
self.addDockWidget(
QtCore.Qt.TopDockWidgetArea,
self.sequence_viewer.asDock()
)
If you want to add a button that will open the viewer in a new window::
from schrodinger.applications.bioluminate import sequence_viewer
self.sequence_viewer = sequence_viewer.SimplifiedSequenceViewer(<window>)
self.viewer_button = QtWidgets.QPushButton(self.tr('Open Model Viewer...'))
self.viewer_button.clicked.connect(self.openSequenceViewer)
then in the connected method (openSequenceViewer)::
self.sequence_viewer.window.show()
self.sequence_viewer.window.setFocus()
If you want to simply add a sequence viewer frame to your app::
self.sequence_viewer = SimplifiedSequenceViewer(<window>)
<layout>.addWidget( self.sequence_viewer.asFrame() )
"""
WORKSPACE = 'workspace'
PROJET_TABLE = 'projecttable'
PDB_STRING = 'pdb_string'
FILES = 'files'
MANUAL_SEQUENCE = 'manual_sequence'
# Custom signals need to be defined here for new style signals
sequencesImported = QtCore.pyqtSignal(str)
"""
Signal emitted after any structures have been imported by any means into the
viewer. The string passed in the emit will be one of:
- `SimplifiedSequenceViewer.WORKSPACE`
- `SimplifiedSequenceViewer.PROJECT_TABLE`
- `SimplifiedSequenceViewer.PDB_STRING`
- `SimplifiedSequenceViewer.FILES`
"""
[docs] def __init__(self, parent, toolbars=None, auto_align=False, require3d=True):
"""
:param parent: Parent widget of the sequence viewer
:param toolbars: A list of toolbar flags to use (see `ViewerWindow` for
available toolbars
:type toolbars: list of strings
:param auto_align: Whether to auto-align sequences when a new one is imported
:type auto_align: boolean
:param require3d: Whether to allow fasta sequence files to be imported.
If set to True (default) only structures with 3D
coordinates are allowed.
:type require3d: boolean
:see: `ViewerWindow`
"""
super(SimplifiedSequenceViewer, self).__init__(parent)
# Set the toolbars to the ViewerWindows preset defaults
# if they are not supplied
if toolbars is None:
toolbars = ViewerWindow.DEFAULT_TOOLBARS
# Create the window and set it up
self.window = ViewerWindow(self)
self.window.addToolBars(toolbars)
self.setupViewer()
self.dock_widget = SequenceDockWidget()
""" Widget the allows `self.window` to be dockable """
self.auto_synchronize = True
self._auto_align = auto_align
self._require3d = require3d
self.last_sequences_imported = []
""" Stores the last sequences added to the viewer. """
def _setContextMenus(self):
"""
Private method to create the context menus used in the
viewer.
"""
action_factory = Factory(self)
# Create the "Name" context menus. These are the entries
# on the left side of the sequence viewer.
action_factory.setActions(action_config.VIEWER_NAME_CONTEXT)
self.name_empty_menu = action_factory.getMenu()
action_factory.setActions(
action_config.VIEWER_NAME_CONTEXT,
action_order=action_config.VIEWER_NAME_CONTEXT['annotate_order'])
self.name_annotate_menu = action_factory.getMenu()
action_factory.setActions(
action_config.VIEWER_NAME_CONTEXT,
action_order=action_config.VIEWER_NAME_CONTEXT['constraints_order'])
self.name_constraints_menu = action_factory.getMenu()
action_factory.setActions(
action_config.VIEWER_NAME_CONTEXT,
action_order=action_config.VIEWER_NAME_CONTEXT['aa_order'])
self.name_aa_menu = action_factory.getMenu()
# Create the "Sequence" context menu. These are the entries
# on the right side of the sequence viewer.
action_factory.setActions(action_config.VIEWER_SEQUENCE_CONTEXT)
action_factory.addSubMenu(action_config.COLOR_SEQUENCES,
add_separator=True,
text='Color')
action_factory.addSubMenu(action_config.ANNOTATE_ACTIONS,
text='Annotate')
self.sequence_menu = action_factory.getMenu()
[docs] def setupViewer(self):
"""
Sets up the viewer's window and context menus
"""
self._setContextMenus()
# Set up the auto sync with Maestro
# Commenting out since it is VERY SLOW
#sv_maestro.initMaestro(self.sequence_group)
# Please note the API changed and it should be called now:
#self.initMaestro()
self.setPopupMenus(
self.nameContextCallback,
self.sequenceContextCallback,
)
[docs] def enableMaestroSync(self):
"""
Enables selection and color synchronization with Maestro
"""
self.initMaestro()
self.auto_synchronize = True
[docs] def asDock(self):
"""
Returns the viewer as a dock widget.
"""
self.dock_widget.setWidget(self.window)
return self.dock_widget
[docs] def asFrame(self):
"""
Returns the viewer as a frame widget
"""
return self.window
@property
def protein_sequences(self):
"""
Property for all sequences in the viewer that are valid proteins
:rtype: list of `sequences<schrodinger.ui.sequencealignment.sequence>`
"""
group = self.sequence_group
sequences = [seq for seq in group.sequences if seq.isValidProtein()]
return sequences
@property
def structure_sequences(self):
"""
Property for all sequences in the viewer that have structures.
:rtype: list of `sequences<schrodinger.ui.sequencealignment.sequence>`
"""
group = self.sequence_group
sequences = [seq for seq in group.sequences if seq.has_structure]
return sequences
@property
def selected_sequences(self):
"""
Property returning all selected sequences in the viewer. This does not
include a child sequence.
:rtype: list of `sequences<schrodinger.ui.sequencealignment.sequence>`
"""
group = self.sequence_group
selected = [seq for seq in group.sequences if seq.selected]
return selected
[docs] def getReferenceStructure(self):
"""
Gets the structure associated with the reference sequence from maestro's
PT. If there is no structure for the reference None is returned.
:rtype: `schrodinger.structure.Structure`
"""
reference = self.sequence_group.reference
return self.getSeqStructure(reference)
[docs] def generateSeqProjectRows(self, include_reference=True):
"""
Create a generator for all sequences that have a project table row
associated with them. Yields each sequence's row from the PT.
:param include_reference: Whether to include the reference seq
:type include_reference: bool
:returns: A generator that yields a `schrodinger.project.ProjectRow`
:rtype: generator
"""
reference = self.sequence_group.reference
pt = maestro.project_table_get()
entry_ids = set(s.maestro_entry_id for s in self.structure_sequences)
for entry_id in entry_ids:
if not include_reference:
# Use an entry_id comparison since sequences can be split
# up by chains and the structure will be the same for chain
# A and B
if entry_id == reference.maestro_entry_id:
continue
row = pt.getRow(entry_id)
yield row
[docs] def generateSeqStructures(self, include_reference=True):
"""
Create a generator for all sequences that have a project table row
associated with them. Yields each sequence's structure from the PT.
:param include_reference: Whether to include the reference structure
:type include_reference: bool
:returns: A generator that yields a `schrodinger.structure.Structure`
:rtype: generator
"""
for row in self.generateSeqProjectRows(include_reference):
yield row.getStructure()
[docs] def getSeqStructure(self, sequence):
"""
Get the structure associated with a `Sequence`.
:returns: A structure object if found otherwise `None`
:rtype: `None` or `structure<schrodinger.structure.Structure>`
"""
if sequence not in self.structure_sequences:
return None
else:
pt = maestro.project_table_get()
row = pt.getRow(sequence.maestro_entry_id)
return row.getStructure()
[docs] @catch_last_added
def importFromPDB(self, maestro_incorporate=True, multiple=True):
"""
Import structures from comma-separated list of PDB IDs. If
`multiple=False` only the first PDB ID will be used.
Returns None if user cancelled, or PDB ID(s) if not.
"""
if multiple:
label = "Please enter a comma-separated list of PDBs to import:"
else:
label = 'Please enter a single PDB ID to import:'
pdb_str, ok = QtWidgets.QInputDialog.getText(self, "Enter PDB ID",
label)
if not ok:
return # User cancelled
# Get the pdb ids, if multiple is False we only fetch
# the first pdb id.
pdb_ids = str(pdb_str)
if not multiple:
pdb_ids = [pdb for pdb in pdb_ids.split(',') if pdb.strip()]
if not pdb_ids:
pdb_ids = ''
else:
pdb_ids = pdb_ids[0]
result = self.fetchSequence(pdb_ids,
maestro_incorporate=maestro_incorporate)
if result == "cancelled":
return
elif result == "error":
msg = "Failed to retrieve PDB: %s" % pdb_ids
msg += "\nCheck your internet connection and make sure CWD is writable"
QtWidgets.QMessageBox.warning(self, "Error", msg)
return
if self._auto_align:
if len(self.sequence_group.sequences) >= 1:
self.runClustal()
self.sequencesImported.emit(self.PDB_STRING)
return pdb_ids
[docs] @catch_last_added
def importFromFile(self, *args, **kwargs):
""" Callback for import file action """
filters = kwargs.get('filter')
if not filters:
if self._require3d:
filters = bwidgets.SequenceFileDialog.STRUCTURE_FILTERS
else:
filters = bwidgets.SequenceFileDialog.REFERENCE_FILTERS
kwargs['filter'] = filters
filenames = bwidgets.SequenceFileDialog.get_open_file_names(
add_options=False, **kwargs)
self.importFromFilePaths(filenames)
[docs] def importFromFilePaths(self, filenames, to_maestro=True):
"""
Import a list of filesnames into the sequence viewer.
:param filenames: Filenames to be imported
:type filenames: list of strings
"""
for filename in filenames:
result = self.loadFile(str(filename),
merge=False,
replace=False,
to_maestro=to_maestro,
new_list=self.last_sequences_imported)
# FIXME: Should we only do this for the imported sequences
# or all of them. Right now its all of them.
for seq in self.sequence_group.sequences:
if seq.children:
seq.hideChildren()
self.generateRows()
if self._auto_align:
if len(self.sequence_group.sequences) >= 1:
self.runClustal()
if filenames:
self.sequencesImported.emit(self.FILES)
[docs] @catch_last_added
def importFromWorkspace(self, ignored=None):
"""
Import a structure from the workspace and load it into the sequence
viewer.
"""
# FIXME: This should never happen. The GUI should take care of
# disabling any option that brings us here if we are outside
# Maestro.
if maestro is None:
return
st = maestro.workspace_get()
if len(st.atom) == 0:
return
# Load this into the viewer
self.incorporateIncludedEntries()
self.sequencesImported.emit(self.WORKSPACE)
[docs] @catch_last_added
def importFromProjectTable(self):
"""
Import a structure from a selected PT row and load it into
the sequence viewer.
"""
if maestro is None:
return
self.incorporateSelectedEntries()
self.sequencesImported.emit(self.PROJECT_TABLE)
[docs] @catch_last_added
def createSequence(self):
"""
Used to manually import a sequence into the viewer. We override
the base class here so we can emit the sequencesImported signal.
"""
super(SimplifiedSequenceViewer, self).createSequence()
self.sequencesImported.emit(self.MANUAL_SEQUENCE)
[docs] def addAnnotationAction(self):
sender = self.sender()
data = sender.data()
#TODO: Add error catching here
mode = int(data)
super(SimplifiedSequenceViewer, self).addAnnotation(mode)
[docs] def setColorModeAction(self):
sender = self.sender()
data = sender.data()
#TODO: Add error catching here
mode = int(data)
super(SimplifiedSequenceViewer, self).setColorMode(mode)
[docs] def focusFinder(self):
"""
Switch focus to Find Pattern input box.
"""
self.findToolBar.show()
self.findToolBar.line_edit.setFocus()
[docs] def nameContextCallback(self, position, seq=None):
"""
Callback used when context menus called for the view with
the pdb name.
"""
if not seq:
self.name_empty_menu.popup(position)
elif seq.type == sv_globals.SEQ_ANNOTATION:
self.name_annotate_menu.popup(position)
elif seq.type in [
sv_globals.SEQ_CONSTRAINTS, sv_globals.SEQ_QUERY_CONSTRAINTS
]:
self.name_constraints_menu.popup(position)
else:
self.name_aa_menu.popup(position)
[docs] def sequenceContextCallback(self, position, res=None):
"""
Callback used when context menus called for the view with the
sequences in it.
"""
if not res:
return
self.sequence_menu.popup(position)
[docs] def treeContextCallback(self, position, tree=None):
"""
Callback used when context menus called for the tree view
"""
# Do we need this?
[docs] def selectOnlyByEntry(self, entry_ids):
"""
Selects rows in the sequence viewer that are associated with
the passed in entry ids.
"""
# First unselect all sequences
self.sequence_group.unselectAllSequences()
# Select the sequences passed in
for seq in self.sequence_group.sequences:
# Ignore sequences with entry_ids equal to None since
# this method explicitly states that we are removing
# sequences with entry ids.
if seq.maestro_entry_id is None:
continue
if seq.maestro_entry_id in entry_ids:
seq.selected = True
self.updateView()
[docs] def selectOnlyBySeqs(self, sequences):
"""
Selects rows in the sequence viewer that are associated with
the passed in sequences.
:param sequences: Sequence or sequences to select in viewer
:type sequences: `schrodinger.ui.sequencealignment.sequence.Sequence`
or list of them.
"""
# First unselect all sequences
self.sequence_group.unselectAllSequences()
# Select the sequence passed in
if type(sequences) in [list, tuple]:
for seq in sequences:
seq.selected = True
else:
sequences.selected = True
self.updateView()
[docs] def deleteByEntry(self, entry_ids):
"""
Deletes rows in the sequence viewer that are associated with
the passed in entry ids. If any rows are selected before this
is called, they are retained.
"""
# Get the initially selected sequences
selected = []
for seq in self.sequence_group.sequences:
if seq.selected:
selected.append(seq)
# First select only the sequences you want to delete
self.selectOnlyByEntry(entry_ids)
# Delete the passed in sequences
self.sequence_group.deleteSelected()
# Reselect the initially selected sequences
self.selectOnlyByEntry(selected)
[docs] def deleteBySequences(self, sequences):
"""
Deletes rows in the sequence viewer that are associated with
the passed in sequences. If any rows are selected before this
is called, they are retained.
"""
# Get the initially selected sequences
selected = []
for seq in self.sequence_group.sequences:
if seq.selected:
selected.append(seq)
# First select only the sequences you want to delete
self.selectOnlyBySeqs(sequences)
# Delete the passed in sequences
self.sequence_group.deleteSelected()
# Reselect the initially selected sequences
self.selectOnlyByEntry(selected)
[docs] def importFromMaestro(self, method):
ret = sv_maestro.maestroIncorporateEntries(self.sequence_group,
what=method)
# BIOLUM-1408 Set 'contents changed' flag
self.contents_changed = True
self.updateView()
return ret
[docs] def closeWindow(self):
""" Action for the "Close" button """
self.window.close()