Source code for schrodinger.application.jaguar.gui.tabs.reaction_tab

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)