Source code for schrodinger.application.jaguar.gui.tabs.sub_tab_widgets.base_widgets

import schrodinger
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt

from ... import utils as gui_utils

maestro = schrodinger.get_maestro()

SORT_ROLE, ATOMS_ROLE, ATOM_NUMS_ROLE, STRUCTURE_ROLE = list(
    range(Qt.UserRole + 1, Qt.UserRole + 5))


# TODO: add a "Remove Row" and a "Remove All" to the menu?
[docs]class SubTabTableView(gui_utils.ProjTableLikeView): """ A table view with a right click menu for removing selected rows. This class may be subclassed to configure delegates. :cvar COLUMN: A class containing column constants for the table. :vartype COLUMN: type :cvar SAMPLE_DATA: A dictionary of {column number: sample cell contents}. The sample cell contents are used to set column widths appropriately. Note that sample data for columns COLUMN.ID, COLUMN.TITLE, COLUMN.ATOM, and/or COLUMN.ATOMS will be added automatically if not explicitly included. Any other columns that are not included here will be set to their default width. :vartype SAMPLE_DATA: dict :cvar _DEFAULT_SAMPLE_DATA: A dictionary of {column name (attribute in the COLUMN class variable): sample cell contents}. The sample cell contents are used to set column widths appropriately. If no column of the specified name is found, the sample data will be ignored. Data found in SAMPLE_DATA takes precedence over data found in _DEFAULT_SAMPLE_DATA. Subclasses should alter SAMPLE_DATA rather than _DEFAULT_SAMPLE_DATA whenever possible (due to increased ease of debugging typos). :vartype _DEFAULT_SAMPLE_DATA: dict :cvar MARGIN: The additional width to add to each column included in the sample data :vartype MARGIN: int """ COLUMN = None SAMPLE_DATA = {} _DEFAULT_SAMPLE_DATA = { "ID": "999", "TITLE": "This is a very long entry title", "ATOM": "C999", "ATOMS": "C99, C99, C99, C99" } MARGIN = 20 setMarkerHighlighting = QtCore.pyqtSignal(list, bool)
[docs] def __init__(self, parent=None): super(SubTabTableView, self).__init__(parent) self.right_click_menu = QtWidgets.QMenu(self) self.right_click_menu.addAction("Remove selected", self.removeSelectedRows) self.all_sample_data = self._getSampleData()
[docs] def removeSelectedRows(self): """ Remove the currently selected rows from the table """ to_delete = {index.row() for index in self.selectedIndexes()} to_delete = sorted(to_delete, reverse=True) model = self.model() for row in to_delete: model.removeRow(row)
[docs] def mouseReleaseEvent(self, event): """ Create the popup menu when the user right clicks """ super(SubTabTableView, self).mouseReleaseEvent(event) if event.button() == Qt.RightButton: self.right_click_menu.popup(event.globalPos()) event.accept()
def _getSampleData(self): """ Combine `_DEFAULT_SAMPLE_DATA` and `SAMPLE_DATA` :return: The combined contents of `_DEFAULT_SAMPLE_DATA` and `SAMPLE_DATA` :rtype: dict """ sample_data = self.SAMPLE_DATA.copy() for col_attr, col_data in self._DEFAULT_SAMPLE_DATA.items(): try: col_num = getattr(self.COLUMN, col_attr) except AttributeError: continue if col_num not in sample_data: sample_data[col_num] = col_data return sample_data
[docs] def setModel(self, model): """ After setting the model, resize the columns using the sample data and the header data provided by the model See Qt documentation for an explanation of arguments """ super(SubTabTableView, self).setModel(model) for col_num in self.all_sample_data: self.resizeColumnToContents(col_num)
[docs] def sizeHintForColumn(self, col_num): """ Provide a size hint for the specified column using the sample data. Note that this method does not take header width into account as the header width is already accounted for in `resizeColumnToContents`. See Qt documentation for an explanation of arguments and return value """ font = self.font() font_metrics = QtGui.QFontMetrics(font) col_data = self.all_sample_data.get(col_num, "") width = font_metrics.horizontalAdvance(col_data) + self.MARGIN return width
[docs] def selectionChanged(self, new_sel, old_sel): """ When the table selection changes, emit the appropriate signals to update marker highlighting. :param new_sel: The new table selection :type new_sel: `PyQt5.QtCore.QItemSelection` :param old_sel: The previous table selection :type old_sel: `PyQt5.QtCore.QItemSelection` """ super(SubTabTableView, self).selectionChanged(new_sel, old_sel) self._emitForSelection(old_sel, False) self._emitForSelection(new_sel, True)
def _emitForSelection(self, sel, highlight): """ Emit setMarkerHighlighting for all rows in the given table selection :param sel: The table selection :type sel: `PyQt5.QtCore.QItemSelection` :param highlight: Whether to highlight (True) or unhighlight (False) the specified marker :type highlight: bool """ indices = [index for index in sel.indexes() if index.column() == 0] for index in indices: atoms = index.data(ATOMS_ROLE) if atoms: self._setHighlightingForAtoms(atoms, highlight) def _setHighlightingForAtoms(self, atoms, highlight): """ Emit the setMarkerHighlighting signal to change the workspace marker highlighting for the specified atoms. Subclasses should override this function instead of `_emitForSelection` whenever possible. :param atoms: A list of `schrodinger.structure._StructureAtom` objects to adjust the highlighting for :type atoms: list :param highlight: Whether to highlight or de-highlight the atoms :type highlight: bool """ self.setMarkerHighlighting.emit(atoms, highlight)
[docs]class SubTabProxyModel(QtCore.QSortFilterProxyModel): """ A proxy model that filters out rows related to entry ids that are no longer selected in the project table. :cvar COLUMN: A class containing column constants for the table. :vartype COLUMN: type """ COLUMN = None
[docs] def __init__(self, parent=None): super(SubTabProxyModel, self).__init__(parent) self.setSortRole(SORT_ROLE) self._displayed_eids = [] # maintain Qt4 dynamicSortFilter default self.setDynamicSortFilter(False)
[docs] def setSourceModel(self, model): """ When setting the source model, adopt its COLUMN constants if we don't have one defined. See Qt documentation for explanation of arguments. """ super(SubTabProxyModel, self).setSourceModel(model) if self.COLUMN is None: self.COLUMN = model.COLUMN
[docs] def setDisplayedEids(self, eids): """ Set the entry ids to display in the table. This should correspond to the entry ids currently selected in the project table. :param eids: The entry ids to display :type eids: list """ self._displayed_eids = eids self.invalidateFilter()
[docs] def isAcceptableEid(self, eid): """ Is the specified entry ID currently included in the Input tab table? i.e., Would this entry ID be acceptable to display in the sub-tab table? :param eid: The entry id :type eid: str :return: True if the entry ID is acceptable. False otherwise. :rtype: bool """ return eid in self._displayed_eids
[docs] def filterAcceptsRow(self, source_row, source_parent=None): """ Accept a row only if the entry id is in `self._displayed_eids` or if the entry id is blank See Qt documentation for an explanation of the arguments and return value """ source_model = self.sourceModel() eid_col = source_model.COLUMN.ID eid_index = source_model.index(source_row, eid_col) eid = eid_index.data() return eid in self._displayed_eids or not eid
[docs] def lessThan(self, left, right): """ Compare two indices for sorting. Assume that the ATOM column contains atom names. Assume that the ATOMS column contains either: - a string of atom names separated by commas and white space - a list or tuple of atom names Atom names are then sorted numerically. All other columns are sorted using Python's less than operator (which allows tuples to be sorted as expected). See Qt documentation for an explanation of arguments and return value """ model = self.sourceModel() sort_role = self.sortRole() left_data = left.data(sort_role) right_data = right.data(sort_role) if left.column() == getattr(model.COLUMN, "ATOM", -1): left_key = gui_utils.atom_name_sort_key(left_data) right_key = gui_utils.atom_name_sort_key(right_data) return left_key < right_key elif left.column() == getattr(model.COLUMN, "ATOMS", -1): if isinstance(left_data, str): left_data = [atom.strip() for atom in left_data.split(",")] right_data = [atom.strip() for atom in right_data.split(",")] left_keys = list(map(gui_utils.atom_name_sort_key, left_data)) right_keys = list(map(gui_utils.atom_name_sort_key, right_data)) return left_keys < right_keys else: return left_data < right_data
[docs]class SubTabRow(object): """ An object representing a single row of the table. This class is intended to be subclassed for each sub-tab. The default implementation includes entry id and structure title. """
[docs] def __init__(self, entry_id=None, title=None): self.entry_id = entry_id self.title = title
[docs] def copy(self): """ Create a new row object that is a copy of this row This method must be implemented in subclasses for use with models that have the appendFromModel method. :rtype: `PerAtomBasisRow` :return: The row item that is a copy of this row """ raise NotImplementedError
[docs] def getAtomNums(self): """ Get a list of all atom numbers associated with this row (i.e. all atom to be marked by workspace markers). The default implementation checks for an `atom_num` or `atom_nums` attribute. Subclasses must redefine this function if neither of these attributes exist. :return: A list of atom numbers (relative to the entry, not the workspace structure) :rtype: list """ try: return [self.atom_num] except AttributeError: return self.atom_nums
[docs] def getAtoms(self): """ Get a list of all atoms associated with this row (i.e. all atom to be marked by workspace markers). :return: A list of `schrodinger.structure._StructureAtom` objects :rtype: list """ atom_nums = self.getAtomNums() if atom_nums: struc = self.getStructure() return [struc.atom[i] for i in atom_nums] else: return []
[docs] def getStructure(self): """ Get the structure this row refers to :return: The structure :rtype: `schrodinger.structure.Structure` """ proj = maestro.project_table_get() return proj[self.entry_id].getStructure()
[docs]class SubTabModel(QtCore.QAbstractTableModel): """ A table model for storing sub-tab data. This class is not intended to be instantiated directly and should be subclassed. Subclasses must redefine COLUMN, UNEDITABLE, ROW_CLASS, MARKER_SETTINGS, and _displayAndSortData. Subclasses may also need to redefine addJaguarMarkerForRow and removeJaguarMarkerForRow if more than one marker per row is required. :cvar COLUMN: A class containing column constants for the table. This class must contain: - NUM_COLS: The number of columns in the table (int) - HEADERS: A list of column headers (list) - ID: The entry id column number (int) - TITLE: The entry title column number (int) and should contain additional integer constants for all other columns. :vartype COLUMN: type :cvar UNEDITABLE: A list of all column numbers that should be flagged as uneditable :vartype UNEDITABLE: iterable :cvar ROW_CLASS: The `SubTabRow` subclass that represents a row of data :vartype ROW_CLASS: type :cvar MARKER_SETTINGS: The settings for the workspace markers. This dictionary will be passed to `schrodinger.maestro.markers._BaseMarker. applySettings` :vartype MARKER_SETTINGS: dict :cvar ERROR_BACKGROUND_BRUSH: The brush used to paint the background of cells where the user has entered invalid data. :vartype ERROR_BACKGROUND_BRUSH: `PyQt5.QtGui.QBrush` :ivar addJaguarMarker: A signal emitted when a workspace marker should be added. Emitted with: - The list of atoms to add the marker for (list) - The marker settings (dict) :vartype addJaguarMarker: `PyQt5.QtCore.pyqtSignal` :ivar removeJaguarMarker: A signal emitted when a workspace marker should be removed. Emitted with: - The list of atoms to remove the marker for (list) :vartype removeJaguarMarker: `PyQt5.QtCore.pyqtSignal` """ COLUMN = None UNEDITABLE = () ROW_CLASS = SubTabRow MARKER_SETTINGS = {} ERROR_BACKGROUND_BRUSH = gui_utils.ERROR_BACKGROUND_BRUSH addJaguarMarker = QtCore.pyqtSignal(list, dict) removeJaguarMarker = QtCore.pyqtSignal(list)
[docs] def __init__(self, parent=None): super(SubTabModel, self).__init__(parent) self._rows = []
[docs] def reset(self): self.beginResetModel() self._rows = [] self.endResetModel()
[docs] def appendFromModel(self, model): """ Append the rows of model to this model :type model: subclass of `SubTabModel` :param model: The model to copy data from, should be the same subclass as this object """ for row in model._rows: self._addRow(row.copy())
[docs] def columnCount(self, parent=None): return self.COLUMN.NUM_COLS
[docs] def rowCount(self, parent=None): return len(self._rows)
[docs] def updateEntryTitles(self, eids_to_titles): """ Update the entry titles in case they have changed in the project table :param eids_to_titles: A dictionary of {entry id: title} :type eids_to_titles: dict """ for cur_row in self._rows: eid = cur_row.entry_id if eid in eids_to_titles: cur_row.title = eids_to_titles[eid]
[docs] def addRow(self, *args, **kwargs): """ Add a row to the table. All arguments are passed to `ROW_CLASS` initialization. :return: The row number of the new row :rtype: int """ row = self.ROW_CLASS(*args, **kwargs) return self._addRow(row)
def _addRow(self, row): """ Add the given row object to the table. :type row: ROW_CLASS :param row: The row to add :return: The row number of the new row :rtype: int """ if not isinstance(row, self.ROW_CLASS): raise ValueError('row must be an instance of %s' % str(type(self.ROW_CLASS))) num_rows = len(self._rows) self.beginInsertRows(QtCore.QModelIndex(), num_rows, num_rows) self._rows.append(row) self.endInsertRows() self.addJaguarMarkerForRow(row) return num_rows
[docs] def removeRows(self, row, count, parent=None): # See Qt documentation for an explanation of arguments and return value self.beginRemoveRows(QtCore.QModelIndex(), row, row + count - 1) for cur_row in self._rows[row:row + count]: self.removeJaguarMarkerForRow(cur_row) del self._rows[row:row + count] self.endRemoveRows() return True
[docs] def addJaguarMarkerForRow(self, row): """ Add a workspace marker for the given row. Subclasses must override this class if they require more than one workspace marker per row. Note that any given set of atom(s) may only have one marker per sub-tab. The sub-tab is responsible for ensuring that a marker is not added to a set of atom(s) that already has a marker. :param row: The row to add the marker for :type row: `SubTabRow` """ atoms = row.getAtoms() self.addJaguarMarker.emit(atoms, self.MARKER_SETTINGS)
[docs] def removeJaguarMarkerForRow(self, row): """ Remove the workspace marker for the given row. Subclasses must override this class if they create more than one workspace marker per row. :param row: The row to remove the marker for :type row: `SubTabRow` """ atoms = row.getAtoms() self.removeJaguarMarker.emit(atoms)
[docs] def rowsForEid(self, eid): """ Get all rows that refer to the specified entry ID :param eid: The entry id :type eid: str :return: A list of `ROW_CLASS` objects :rtype: list """ return [row for row in self._rows if row.entry_id == eid]
[docs] def headerData(self, section, orientation, role=Qt.DisplayRole): # See Qt documentation for an explanation of arguments and return value if orientation == Qt.Vertical: return if role == Qt.DisplayRole: return self.COLUMN.HEADERS[section]
[docs] def flags(self, index): """ Flag columns in `UNEDITABLE` as uneditable. See Qt documentation for an explanation of arguments and return value """ col = index.column() flag = Qt.ItemIsEnabled | Qt.ItemIsSelectable if col not in self.UNEDITABLE: flag |= Qt.ItemIsEditable return flag
[docs] def data(self, index, role=Qt.DisplayRole): # See Qt documentation for an explanation of arguments and return value col = index.column() row_num = index.row() row_data = self._rows[row_num] if role in (Qt.DisplayRole, SORT_ROLE): return self._displayAndSortData(col, row_data, role) elif role == Qt.ToolTipRole: return self._toolTipData(col, row_data) elif role == Qt.BackgroundRole: return self._backgroundData(col, row_data) elif role in (ATOMS_ROLE, ATOM_NUMS_ROLE, STRUCTURE_ROLE): return self._otherDataSubTab(col, row_data, role) else: return self._otherData(col, row_data, role)
def _displayAndSortData(self, col, row_data, role): """ Retrieve data for the display and sort roles :param col: The column to return data for :type col: int :param row_data: The ROW_CLASS instance to retrieve data from :type row_data: ROW_CLASS :param role: The role to retrieve data for :type role: int :return: The requested data :rtype: object """ if col == self.COLUMN.ID: if role == Qt.DisplayRole: return row_data.entry_id else: # Entry IDs should be sorted numerically return int(row_data.entry_id) elif col == self.COLUMN.TITLE: return row_data.title elif col == getattr(self.COLUMN, "ATOM", -1): return row_data.atom_name elif col == getattr(self.COLUMN, "ATOMS", -1): if role == Qt.DisplayRole: if row_data.atom_names: return ", ".join(row_data.atom_names) else: return "" elif role == SORT_ROLE: return row_data.atom_names def _backgroundData(self, col, row_data): """ Retrieve data for the background role :param col: The column to return data for :type col: int :param row_data: The ROW_CLASS instance to retrieve data from :type row_data: ROW_CLASS :return: The requested data :rtype: object """ # TODO: color editable columns? lightcyan? def _toolTipData(self, col, row_data): """ Retrieve data for the tool tip role :param col: The column to return data for :type col: int :param row_data: The ROW_CLASS instance to retrieve data from :type row_data: ROW_CLASS :return: The requested data :rtype: object """ # This function intentionally left blank def _otherDataSubTab(self, col, row_data, role): """ Retrieve data for custom roles that are defined for all sub tabs. Note that sub-classes should redefine `_otherDataSubTab` rather than this method. :param col: The column to return data for :type col: int :param row_data: The ROW_CLASS instance to retrieve data from :type row_data: ROW_CLASS :param role: The role to retrieve data for :type role: int :return: The requested data :rtype: object """ if role == ATOMS_ROLE: return row_data.getAtoms() elif role == ATOM_NUMS_ROLE: return row_data.getAtomNums() elif role == STRUCTURE_ROLE: return row_data.getStructure() def _otherData(self, col, row_data, role): """ Retrieve data for custom roles that are defined for the current sub-tab. Note that sub-classes should redefine this method rather than `_otherDataSubTab`. :param col: The column to return data for :type col: int :param row_data: The ROW_CLASS instance to retrieve data from :type row_data: ROW_CLASS :param role: The role to retrieve data for :type role: int :return: The requested data :rtype: object """ # This function intentionally left blank
[docs] def setData(self, index, value, role=Qt.EditRole): """ Set data for the specified index and role. Whenever possible, sub- classes should redefine `_setData` rather than this method. See Qt documentation for an explanation of arguments and return value. """ col = index.column() table_row = index.row() row_data = self._rows[table_row] retval = self._setData(col, row_data, value, role, table_row) if retval is False: return False else: self.dataChanged.emit(index, index) return True
def _setData(self, col, row_data, value, role, row_num): """ Set data for the specified index and role. Note that sub-classes should redefine this method rather than `setData` whenever possible. :param col: The column to set data for :type col: int :param row_data: The ROW_CLASS instance to modify :type row_data: ROW_CLASS :param value: The value to set :param value: object :param role: The role to set data for :type role: int :param row_num: The row number :type row_num: int :return: False if setting failed. All other values are considered successes. :rtype: object """ # This function intentionally left blank
[docs] def clearDataForEid(self, eid): """ Clear all data related to the specified entry ID :param eid: The entry id :type eid: str """ rev_data = reversed(list(enumerate(self._rows))) for row_num, row_data in rev_data: if row_data.entry_id == eid: self.removeRow(row_num)