Source code for schrodinger.ui.qt.select_residues_dialog

"""
Module of classes that allow for user selection of
one or more residues based on the contents of the Maestro Workspace.

The main class to use is SelectResiduesDialog.

Copyright Schrodinger, LLC. All rights reserved.
"""

import schrodinger
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.structutils import analyze
from schrodinger.ui import picking

from . import select_residues_dialog_ui

maestro = schrodinger.get_maestro()


[docs]class ResiduesModel(QtCore.QAbstractTableModel): """ This model represents the active site residues table. """
[docs] def __init__(self): QtCore.QAbstractTableModel.__init__(self) self._header_data = ["Residue", "Molecule"] self._data = []
# For interfacing with the Qt view:
[docs] def rowCount(self, ignored=None): """ Returns number of rows """ return len(self._data)
[docs] def columnCount(self, ignored=None): """ Returns number of columns """ return len(self._header_data)
[docs] def data(self, index, role): """ Given a cell index, return the string that should be displayed in that cell. Used by the View class. """ row_obj = self._data[index.row()] coli = index.column() if role == Qt.DisplayRole: if coli == 0: return row_obj.res_str elif coli == 1: return row_obj.mol_num
[docs] def headerData(self, section, orientation, role): """ Returns the string that should be displayed in the specified header cell. Used by the View. """ if role == Qt.DisplayRole: if orientation == Qt.Horizontal: return self._header_data[section]
[docs] def setAllData(self, rows): self.modelAboutToBeReset.emit() self._data = rows self.modelReset.emit()
[docs] def addRow(self, res): """ Add the specified residue to the table (unless it's already there) """ existing_residues = self.getAllData() existing_res_strings = {res.res_str for res in existing_residues} if res.res_str not in existing_res_strings: self.modelAboutToBeReset.emit() self._data.append(res) self.modelReset.emit()
[docs] def removeRows(self, rows): self.modelAboutToBeReset.emit() for row_num in sorted(rows, reverse=True): res_row = self._data.pop(row_num) res_row.removeAllMarkers() self.modelReset.emit()
[docs] def removeAllRows(self): self.removeRows(list(range(self.rowCount())))
[docs] def getAllData(self): """ Return a list of `picking.ResidueRow` objects for all rows in the table. """ return self._data
[docs]class SelectResiduesDialog(picking.PickResiduesChangedMixin, QtWidgets.QDialog): """ Class for allowing the user to select one or more residues in the Workspace - for example, active site residues. """
[docs] def __init__(self, panel, title=None, label_text=None, allow_empty_list=False, markers_color=None, hide_markers_on_close=False): """ Create a new dialog instance :param panel: The parent panel for this dialog. :type panel: `QtWidgets.QWidget` :param title: Optional title for the dialog. Default is "Active Site Residues". :type titel: str :param label_text: Optional label text for describing what the residues are to be picked for. Default is "Pick residues to define the active site:" :type label_text: str :param allow_empty_list: Whether it is OK to remove all selected residues from the list and close the dialog with an empty res list. :type allow_empty_list: bool :param markers_color: 3-tuple of floats between 0 and 1 each to define a color to use for marking selected residues. If a value of None is passed, no markers will be added. :type markers_color: 3-tuple of floats, 0 <= value <= 1.0, or None :param hide_markers_on_close: indicates whether Maestro Workspace markers should be hidden when dialog is closed. In this mode any existing markers will be shown when dialog is shown. :type hide_markers_on_close: bool """ super(SelectResiduesDialog, self).__init__(panel) self.allow_empty_list = allow_empty_list self.markers_color = markers_color self.hide_markers_on_close = hide_markers_on_close self.ui = select_residues_dialog_ui.Ui_Dialog() self.ui.setupUi(self) self.residues_model = ResiduesModel() self.residues_model.modelReset.connect(self.residuesModelUpdated) self.original_rows = [] if title: self.setWindowTitle(title) if label_text: self.ui.pick_res_label.setText(label_text) self.ui.residues_view.setModel(self.residues_model) self.ui.residues_view.setSelectionBehavior( QtWidgets.QAbstractItemView.SelectRows) self.ui.residues_view.setSelectionMode( QtWidgets.QAbstractItemView.ExtendedSelection) self.ui.residues_view.selectionModel().selectionChanged.connect( self.residueSelectionChanged) self.ui.delete_btn.clicked.connect(self.removeSelectedRows) self.ui.delete_all_btn.clicked.connect(self.clear) self.ui.select_button.clicked.connect(self.openSelectDialog) self.residue_picker = picking.PickAtomToggle( self.ui.pick_residue_cb, pick_text="Pick residue to include in the active site definition", pick_function=self.pickResidueAtom) if not maestro: self.ui.select_button.setVisible(False) self.ui.pick_residue_cb.setVisible(False) self.disabled_win = None
[docs] def display(self): """ Open the dialog. """ # Back-up the original residue list, for when the dialog is cancelled. self.original_rows = list(self.residues_model.getAllData()) parent = self.parent() topmost_window = parent.window() win_type = topmost_window.windowFlags() & QtCore.Qt.WindowType_Mask # Detect windows that are currently docked within the main Maestro # window, and Tool-style windows. (Dockable panels which are no longer # docked within Maestro's main panel will show up as tool windows). # For these types of windows, we need custom behavior: A modeless # dialog that acts somewhat like a modal dialog. See PANEL-7915 # FIXME: Use maestro.get_main_window() to avoid DRY. if topmost_window.objectName() == 'main_maestro_window': # Find the docked widget to disable while the dialog is up: while parent.objectName() != 'main_maestro_window': self.disabled_win = parent parent = parent.parent() elif win_type == QtCore.Qt.Tool: # If the parent is a dockable panel that was undocked, it will be # a Tool panel. Modal dialogs on top of Tool panels don't allow # interaction with the Workspace either, so need to open modeless. self.disabled_win = self.parent() else: # Use a modal dialog, don't disable any parent self.disabled_win = False if self.disabled_win: # If the parent is a dockable window, open the dialog as modeless: self.disabled_win.setEnabled(False) # This the dialog is a child, it will also be disabled by above # call, so re-enable it: self.setEnabled(True) # Force the dialog on top of all windows. Tool windows such # as atom-selector will still be on top, as desired. # NOTE: the dialog will stay on top of windows of other apps too. self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) # Open as a modeless dialog, to make it possible to interact with # the Workspace. self.show() else: # For non-dockable panels, open as a modal dialog to block access # to the parent window. Interaction with the Workspace will still # be possible because it's in a different window. self.open() self.residue_picker.start() # Show any existing markers if maestro and self.hide_markers_on_close: self.showAllMarkers()
[docs] def pickResidueAtom(self, anum): res = maestro.workspace_get().atom[anum].getResidue() self.addResidueToTable(res)
[docs] def openSelectDialog(self): """ Opens an atom selection dialog. When a user specifies the ASL, all atoms matching the ASL will be extended to full residues, and each of these residues will be added to the residues table. """ self.residue_picker.stop() desc = "Select residues to include in the active site definition" asl = maestro.atom_selection_dialog( desc, initial_pick_state=maestro.PICK_RESIDUES) if not asl: return st = maestro.workspace_get() matched_atoms = analyze.evaluate_asl(st, asl) unassigned_atoms = set(matched_atoms) while unassigned_atoms: res = st.atom[unassigned_atoms.pop()].getResidue() self.addResidueToTable(res) unassigned_atoms -= set(res.getAtomIndices())
[docs] def addResidueToTable(self, res): """ Adds a row to the table for the specified residue. :param res: Residue to be added. :type res: `schrodinger.structure.Structure` """ row = picking.ResidueRow(res, markers_color=self.markers_color) self.residues_model.addRow(row)
[docs] def residuesModelUpdated(self): """ Called when rows are added to or removed from the residues table. """ rows_exist = self.residues_model.rowCount() > 0 self.ui.delete_all_btn.setEnabled(rows_exist)
[docs] def residueSelectionChanged(self): """ Called when residue rows are selected or de-selected. """ rows_selected = self.ui.residues_view.selectionModel().hasSelection() self.ui.delete_btn.setEnabled(rows_selected)
[docs] def removeSelectedRows(self): """ Called when "Delete" button is clicked. """ items = self.ui.residues_view.selectionModel().selectedRows() selected_rows = [item.row() for item in items] self.residues_model.removeRows(selected_rows) self.ui.delete_btn.setEnabled(False)
[docs] def accept(self): """ If the user input is valid, accept and close the dialog. Otherwise, display a warning message to the user. """ if self.residues_model.rowCount() == 0: if self.allow_empty_list: center_array = (None, None, None) res_str_list = [] else: msg = "Please specify at least one residue or press Cancel." QtWidgets.QMessageBox.warning(self, "Warning", msg) return else: super(SelectResiduesDialog, self).accept() center_array = self.getResiduesCenter() res_str_list = self.getResiduesList() self.residues_centroid_changed.emit(center_array) self.residues_changed.emit(res_str_list) self.residue_picker.stop() # If opened as mode-less dialog, re-enable the parent window: if self.disabled_win: self.disabled_win.setEnabled(True) # Hide workspace markers if maestro and self.hide_markers_on_close: self.hideAllMarkers() self.hide()
[docs] def reject(self): """ Called when the user closes the dialog with cancel or X. """ # Hide workspace markers if maestro and self.hide_markers_on_close: self.hideAllMarkers() # Restore the residue list to what it was when the dialog was opened: self.residues_model.setAllData(self.original_rows) self.residue_picker.stop() if self.disabled_win: self.disabled_win.setEnabled(True) super(SelectResiduesDialog, self).reject()
[docs] def showAllMarkers(self): """ Displays all workspace markers for each row. """ if maestro: for row in self.residues_model.getAllData(): row.showAllMarkers()
[docs] def hideAllMarkers(self): """ Hides the markers for all rows. """ if maestro: for row in self.residues_model.getAllData(): row.hideAllMarkers()
[docs] def clear(self): """ Clear the residues table. """ self.residues_model.removeAllRows() self.ui.delete_btn.setEnabled(False)
[docs] def getResiduesList(self): """ Return the list of residue strings (e.g. ['A:217', 'A:231b']) of the selected residues. :return List of residue strings for each row. @rtyp: list of str """ all_rows = self.residues_model.getAllData() return super(SelectResiduesDialog, self).getResiduesList(all_rows)
[docs] def getResiduesCenter(self): """ Return the (x, y, z) tuple for the center of the selected residues. Will raise ValueError if no residues were picked. :return: Tuple of center x, y, z coordinates :rtype: tuple of (float, float, float) """ all_rows = self.residues_model.getAllData() return super(SelectResiduesDialog, self).getResiduesCenter(all_rows)