from schrodinger import get_maestro
from schrodinger import project
from schrodinger import structure
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtWidgets
from schrodinger.ui.qt import entryselector
from schrodinger.ui.qt import filedialog
from schrodinger.ui.qt import structure2d
from .. import ui
from .base_tab import BaseTab
from .base_tab import ProvidesStructuresMixin
from .reaction_molecules_tab import PRODUCT_PREFIX
from .reaction_molecules_tab import REACTANT_PREFIX
maestro = get_maestro()
[docs]class ReactionTab(ProvidesStructuresMixin, BaseTab):
NAME = "Reaction"
HELP_TOPIC = "JAGUAR_REACTION_TAB"
UI_MODULES = (ui.reaction_tab_ui,)
reactionParticipantAdded = QtCore.pyqtSignal(str)
reactionParticipantRemoved = QtCore.pyqtSignal(str)
reactionParticipantStructChanged = QtCore.pyqtSignal(
str, structure.Structure)
[docs] def setup(self):
self.ui.add_reactant_btn.clicked.connect(self.addReactant)
self.ui.add_product_btn.clicked.connect(self.addProduct)
self.reactant_layout = QtWidgets.QHBoxLayout(self)
self.product_layout = QtWidgets.QHBoxLayout(self)
self.ui.reactant_frame.setLayout(self.reactant_layout)
self.ui.product_frame.setLayout(self.product_layout)
self.ui.tile_in_workspace_btn.clicked.connect(self.tileParticipants)
self.reactant_index = 1
self.product_index = 1
self.reactants = []
self.products = []
def _reactionParticipantStructChanged(self, participant, struct):
"""
This is used to bubble up ReactionParticipantView structure changed
signals.
:param participant: Participant with changed structure
:type participant: ReactionParticipant
:param struct: New structure for participant
:type struct: structure.Structure
"""
self.reactionParticipantStructChanged.emit(participant, struct)
def _addParticipant(self, name, participant_list, layout):
"""
This adds a new participant to the GUI, either as a reactant or
product, which is determined based on the passed in list/layout
:param participant_list: reactants or products list
:type participant_list: list of ReactionParticipant instances
:param layout: reactants or products layout
:type layout: QHBoxLayout
"""
plus_item = None
if len(participant_list) > 0:
plus_item = QtWidgets.QLabel(" + ")
participant = ReactionParticipant(self, name, plus_item=plus_item)
participant.structureChanged.connect(self.structureChanged.emit)
participant_list.append(participant)
if plus_item:
layout.addWidget(plus_item)
layout.addLayout(participant.layout)
self.reactionParticipantAdded.emit(str(participant))
[docs] def addReactant(self):
name = "%s %d" % (REACTANT_PREFIX, self.reactant_index)
self._addParticipant(name, self.reactants, self.reactant_layout)
self.reactant_index += 1
[docs] def addProduct(self):
name = "%s %d" % (PRODUCT_PREFIX, self.product_index)
self._addParticipant(name, self.products, self.product_layout)
self.product_index += 1
def _removeParticipantSpecific(self, participant, participant_list, layout):
"""
This does the actual work for removing a participant, be it in the
reactants or products.
:param participant: The participant to remove from the list/layout
:type participant: ReactionParticipant
:param participant_list: reactants or products list
:type participant_list: list of ReactionParticipant instances
:param layout: reactants or products layout
:type layout: QHBoxLayout
"""
#If we're deleting the first of multiple items, then we need to
#remove the plus of the item in front of it
if (participant_list.index(participant) == 0 and
len(participant_list) > 1):
participant.plus_item = participant_list[1].plus_item
participant_list[1].plus_item = None
if participant.plus_item:
layout.removeWidget(participant.plus_item)
participant.plus_item.close()
layout.removeItem(participant.layout)
layout.removeWidget(participant.view)
layout.removeWidget(participant.label)
participant.view.close()
participant.label.close()
participant_list.remove(participant)
self.reactionParticipantRemoved.emit(str(participant))
self.structureChanged.emit()
[docs] def removeParticipant(self, participant):
""" This removes a participant from the GUI """
if participant in self.reactants:
self._removeParticipantSpecific(participant, self.reactants,
self.reactant_layout)
if len(self.reactants) == 0:
self.addReactant()
elif participant in self.products:
self._removeParticipantSpecific(participant, self.products,
self.product_layout)
if len(self.products) == 0:
self.addProduct()
else:
raise KeyError("Participant doesn't exist in GUI.")
@property
def participants(self):
for reactant in self.reactants:
yield reactant
for product in self.products:
yield product
[docs] def tileParticipants(self):
""" This places all current participants in the workspace in tiles """
maestro.command("delete all")
maestro.command("tilemode tile=true")
for participant in self.participants:
participant.addToWorkspace()
maestro.command("fit")
[docs] def workspaceChanged(self, change, pt):
"""
This function is called when changes are made to the workspace.
:param change: what changed in workspace
:type change: str
:param pt: currently active project table
:type pt: `schrodinger.project.Project`
"""
if change != maestro.WORKSPACE_CHANGED_EVERYTHING:
return
participant_set = {p.pt_entry_id for p in self.participants}
workspace_set = {row.entry_id for row in pt.included_rows}
changed_set = participant_set.intersection(workspace_set)
for participant in self.participants:
if participant.pt_entry_id in changed_set:
participant.regeneratePicture()
[docs] def projectClosed(self):
"""
This function is called when the project is closed. In this case
we reset tab since we don't know whether participants came from
the project table or were imported from file.
"""
self.reset()
[docs] def error(self, err):
QtWidgets.QMessageBox.critical(self, "Jaguar error", err)
[docs] def reset(self):
self.reactant_index = 1
self.product_index = 1
participants = list(self.participants)
# We have to create a list out of self.partipants so that we're not
# iterating through the same data structure that we're deleting from
for cur_participant in participants:
self.removeParticipant(cur_participant)
[docs] def validate(self):
for cur_participant in self.participants:
struc = cur_participant.getStructure()
if struc is None or struc.atom_total == 0:
return "No structure loaded for %s" % cur_participant.name
[docs] def getStructureTitleForJobname(self):
# See ProvidesStructuresMixin for method documentation
# Skip over any structures without titles (i.e. if there's a product
# loaded but no reactant)
for cur_participant in self.participants:
struc = cur_participant.getStructure()
if struc is not None:
return struc.title
[docs]class ReactionParticipant(QtCore.QObject):
"""
This class holds all the information for each participant in the reaction,
be it reactant or product. It also contains the drawing objects.
"""
structureChanged = QtCore.pyqtSignal()
[docs] def __init__(self, parent, name, plus_item=None):
QtCore.QObject.__init__(self)
self.name = name
self.plus_item = plus_item
self.scene = structure2d.structure_scene()
self.item = structure2d.structure_item()
self.scene.addItem(self.item)
self.view = ParticipantView(self, self.scene, self.item)
self.view.deleteParticipant.connect(parent.removeParticipant)
self.view.structureChanged.connect(
parent._reactionParticipantStructChanged)
# Delay the structureChanged signal emission (by putting it at the end
# of the Qt event queue) so that the structure has been fully loaded
# when it's emitted. Otherwise, the corresponding slot won't be able to
# retrieve the structure.
delayed_signal = lambda: QtCore.QTimer.singleShot(
0, self.structureChanged.emit)
self.view.structureChanged.connect(delayed_signal)
self.view.error.connect(parent.error)
self.label = QtWidgets.QLabel(name)
self.layout = QtWidgets.QVBoxLayout(parent)
self.layout.addWidget(self.label)
self.layout.addWidget(self.view)
def __str__(self):
return self.name
[docs] def regeneratePicture(self):
"""
This regenerates the 2D picture if the underlying structure has been
edited inside the workspace.
"""
pt = maestro.project_table_get()
row = pt.getRow(self.pt_entry_id)
self.view._setStructure(row.getStructure())
@property
def pt_entry_id(self):
return self.view.pt_entry_id
[docs] def addToWorkspace(self):
""" Convenience wrapper """
self.view.addToWorkspace()
[docs] def getStructure(self):
"""
Get the structure for this participant
:return: The structure
:rtype: `schrodinger.structure.Structure`
"""
entry_id = self.pt_entry_id
if entry_id is None:
return None
else:
try:
pt = maestro.project_table_get()
except project.ProjectException:
return None
row = pt.getRow(self.pt_entry_id)
return row.getStructure()
ParticipantViewParent = structure2d.structure_view
[docs]class ParticipantView(ParticipantViewParent):
"""
This class is a subclass of a QGraphicsView that holds a single
structure_item, to display a single 2D structure. This handles all
interactions with the widget displayed in the panel.
"""
PT_IMPORT_TXT = "Import from Project Table..."
FILE_IMPORT_TXT = "Import from file..."
DRAW_TXT = "Sketch..."
DELETE_TXT = "Delete structure"
structureChanged = QtCore.pyqtSignal(str, structure.Structure)
deleteParticipant = QtCore.pyqtSignal(ReactionParticipant)
error = QtCore.pyqtSignal(str)
[docs] def __init__(self, parent, scene, item):
ParticipantViewParent.__init__(self, scene)
self.parent = parent # We need this to emit a signal for deletion
self.item = item
self.context_menu = QtWidgets.QMenu(self)
self.pt_entry_id = None
for txt in [self.PT_IMPORT_TXT, self.FILE_IMPORT_TXT, self.DELETE_TXT]:
self.context_menu.addAction(txt)
[docs] def mousePressEvent(self, event):
""" Override default Qt function to show context menu """
if event.button() == QtCore.Qt.LeftButton:
self.setAsWorkspace()
if event.button() == QtCore.Qt.RightButton:
action = self.context_menu.exec(self.mapToGlobal(event.pos()))
self.processContextAction(action)
return ParticipantViewParent.mousePressEvent(self, event)
[docs] def addToWorkspace(self):
"""
This includes the entry for this participant, leaving other current
entries in the workspace as well (used with tiling).
"""
if self.pt_entry_id is None:
return
maestro.command("entrywsinclude entry_id %s" % self.pt_entry_id)
[docs] def setAsWorkspace(self):
"""
This replaces the current workspace with only the entry for this
participant.
"""
if self.pt_entry_id is None:
return
maestro.command("tilemode tile=false")
maestro.command("delete atom all")
maestro.command("entrywsinclude entry_id %s" % self.pt_entry_id)
maestro.command("fit")
def _setStructure(self, structure):
"""
Set a Structure object for the structure_item and generate its picture.
:param structure: The structure to be loaded
:type structure: structure.Structure
"""
self.item.set_structure(structure, allowRadicals=True)
self.item.generate_picture()
self.structureChanged.emit(str(self.parent), structure)
def _importStructureFromFile(self):
""" Load first structure from structure file"""
pt = maestro.project_table_get()
filenameTxt = filedialog.get_open_file_name(
parent=self,
caption="Select Structure File",
filter="Maestro File (*.mae *.maegz)",
id="jag_reaction")
if not filenameTxt:
return
struct = structure.Structure.read(filenameTxt)
self._setStructure(struct)
row = pt.importStructure(struct)
row['s_jaguar_Jaguar_Reaction_ID'] = str(self.parent)
self.pt_entry_id = row.entry_id
def _importStructureFromPT(self):
""" Load currently current single selected PT entry """
pt = maestro.project_table_get()
entry_id = entryselector.get_entry(self)
if not entry_id:
return
row = pt.getRow(entry_id)
row['s_jaguar_Jaguar_Reaction_ID'] = str(self.parent)
self._setStructure(row.getStructure().copy())
self.pt_entry_id = row.entry_id
[docs] def processContextAction(self, action):
"""
This loads data from the context menu, if requested
:param action: The action clicked on from the menu
:type action: QAction
"""
if not action:
return
text = action.text()
if text == self.FILE_IMPORT_TXT:
self._importStructureFromFile()
elif text == self.PT_IMPORT_TXT:
self._importStructureFromPT()
elif text == self.DELETE_TXT:
self.deleteParticipant.emit(self.parent)