"""
Widget for letting the user show/hide or enable/disable features in a
hypothesis. All features are shown in a grid, and each has a checkbox next
to it.
"""
from collections import defaultdict
from past.utils import old_div
from schrodinger.application.phase import constants
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.Qt.QtCore import pyqtSignal
from schrodinger.ui.qt import table_helper
NUM_COLUMNS = 4
[docs]class FeatureSelector(QtWidgets.QScrollArea):
"""
This frame contains widgets for selecting one or more features in a
hypothesis (e.g. A1, A2, H1, etc.). The editor has a checkbox for each
feature. This frame is intended to be embedded in a layout.
"""
# Gets emitted when the feature selection changes. Value is a set of
# feature names:
selectionChanged = pyqtSignal(set)
[docs] def __init__(self, parent=None):
super(FeatureSelector, self).__init__(parent)
self.setWidgetResizable(True)
self._interior_widget = QtWidgets.QWidget()
self._interior_layout = QtWidgets.QVBoxLayout(self._interior_widget)
self._features_layout = QtWidgets.QGridLayout()
self._interior_layout.addLayout(self._features_layout)
self._interior_layout.addStretch()
self.setWidget(self._interior_widget)
self._checkboxes = {}
self.feature_labels = []
[docs] def reset(self):
"""
Unchecks all available checkboxes.
"""
for checkbox in self._checkboxes.values():
checkbox.setChecked(False)
[docs] def clear(self):
"""
Remove all checkboxes from the features layout.
"""
self.feature_labels = []
while True:
child = self._features_layout.takeAt(0)
if not child:
break
child.widget().deleteLater()
self._checkboxes = {}
[docs] def setFeatures(self, feature_names):
"""
Initialize the editor with checkbox for each specified feature.
"""
# Clear the dicts so that previous widgets can be deleted:
self._checkboxes = {}
# Clear previous contents of layout:
self.clear()
self.feature_labels = feature_names[:]
# Split up the features into 4 columns:
for i, feature_label in enumerate(self.feature_labels):
row = old_div(i, NUM_COLUMNS)
col = i % NUM_COLUMNS
feature_type = feature_label[0]
qcolor = constants.FEATURE_QCOLORS(feature_type)
palette = QtGui.QPalette()
palette.setColor(QtGui.QPalette.WindowText, qcolor)
checkbox = QtWidgets.QCheckBox(feature_label)
checkbox.setPalette(palette)
checkbox.setStyleSheet('font-weight: bold;')
checkbox.toggled.connect(self._selectionChanged)
self._checkboxes[feature_label] = checkbox
self._features_layout.addWidget(checkbox, row, col)
[docs] def getFeatureCheckbox(self, feature_name):
"""
Return the checkbox for the feature.
:param feature_name: The name of the feature (e.g. "A2")
:type feature_name: str
"""
return self._checkboxes[feature_name]
def _selectionChanged(self):
"""
Emit the selectionChanged signal with a set of checked features.
"""
selections = self.getSelectedFeatures()
self.selectionChanged.emit(selections)
[docs] def setSelectedFeatures(self, features):
"""
:param features: The features to select/check.
:type features: set of str
Checks the checkboxes corresponding to the given set of feature,
and unchecks the other checkboxes.
"""
for feature_name in self.feature_labels:
checkbox = self.getFeatureCheckbox(feature_name)
checkbox.setChecked(feature_name in features)
self._selectionChanged()
[docs] def getSelectedFeatures(self):
"""
Return a set of selected features (their names).
"""
selections = set()
for feature_name in self.feature_labels:
checkbox = self.getFeatureCheckbox(feature_name)
if checkbox.isChecked():
selections.add(feature_name)
return selections
[docs]class FeatureRow(object):
"""
Data class that contains information about single feature in a hypothesis.
This can be a regular feature or excluded volume.
"""
[docs] def __init__(self, hypo_eid, hypo_name, feature_name, is_xvol, use_feature):
"""
Initialize feature data.
:param hypo_eid: hypothesis entry id
:type hypo_eid: int
:param hypo_name: hypothesis name. For custom features hypothesis
name should be set to None!
:type hypo_name: str
:param is_xvol: True if this is excluded volume and False otherwise
:type is_xvol: bool
:param feature_name: feature name (empty string in case of excluded
volume)
:type feature_name: str
:param use_feature: indicates whether checkbox for this feature
is toggled on or off
:type use_feature: bool
"""
self.hypo_eid = hypo_eid
self.hypo_name = hypo_name
self.is_xvol = is_xvol
self.feature_name = feature_name
self.use_feature = use_feature
class FeatureColumns(table_helper.TableColumns):
HypoName = table_helper.Column("Hypothesis Name")
FeatureName = table_helper.Column(
"Feature", checkable=True, tooltip="Toggle on/off to use this feature.")
[docs]class FeatureModel(table_helper.RowBasedTableModel):
"""
Features model.
"""
Column = FeatureColumns
ROW_CLASS = FeatureRow
CUSTOM_HYPO_TEXT = "Custom"
XVOL_TEXT = "XVols"
# This signal is emitted when feature check box is toggled
featureToggled = pyqtSignal()
@table_helper.data_method(Qt.DisplayRole, Qt.CheckStateRole)
def _getData(self, col, feature_row, role):
# See base class for documentation
if col == self.Column.HypoName and role == Qt.DisplayRole:
if feature_row.hypo_name is None:
return self.CUSTOM_HYPO_TEXT
else:
return feature_row.hypo_name
if col == self.Column.FeatureName:
if role == Qt.DisplayRole:
if feature_row.is_xvol:
return self.XVOL_TEXT
else:
return feature_row.feature_name
if role == Qt.CheckStateRole:
if feature_row.use_feature:
return Qt.Checked
else:
return Qt.Unchecked
@table_helper.data_method(Qt.ForegroundRole)
def _getForegroundColor(self, col, feature_row, role):
# See table_helper.data_method for method documentation
if col == self.Column.FeatureName and not feature_row.is_xvol:
feature_type = feature_row.feature_name[0]
return constants.FEATURE_QCOLORS(feature_type)
# Use default foreground color
return None
def _setData(self, col, feature_row, value, role, row_num):
# See table_helper._setData for method documentation
if role == Qt.CheckStateRole and col == self.Column.FeatureName:
feature_row.use_feature = bool(value)
self.featureToggled.emit()
return True
return False
[docs] def getSelectedFeatures(self, include_pt_feats, include_custom_feats):
"""
Returns dictionary of checked feature names. It is keyed on hypothesis
entry ids and contains feature names for each hypothesis.
:param include_pt_feats: indicates that selected features that came
from PT hypotheses should be included
:type include_pt_feats: bool
:param include_custom_feats: indicates that selected features that were
manually added by the user should be included
:type include_custom_feats: bool
:return: dictionary of checked feature names
:rtype: dict
"""
feature_selection = defaultdict(list)
for row in self.rows:
add_pt_row = (include_pt_feats and row.hypo_name)
add_custom_row = (include_custom_feats and row.hypo_name is None)
add_row = add_pt_row or add_custom_row
if row.use_feature and add_row and not row.is_xvol:
feature_selection[row.hypo_eid].append(row.feature_name)
return feature_selection
[docs] def getSelectedExcludedVolumes(self):
"""
Returns list of hypothesis entry ids which have excluded volumes checked.
:return: list of hypothesis entry ids, which have have excluded volumes
checked
:rtype: list
"""
xvol_selection = [
row.hypo_eid for row in self.rows if row.use_feature and row.is_xvol
]
return xvol_selection
[docs] def toggleSelection(self, hypo_eid, feature_name):
"""
Flips use_feature flag for a given feature.
:param hypo_eid: feature's hypothesis entry id
:type hypo_eid: int
:param feature_name: feature name
:type feature_name: str
"""
for row_num, row in enumerate(self.rows):
if row.hypo_eid == hypo_eid and row.feature_name == feature_name:
row.use_feature = not row.use_feature
self.rowChanged(row_num)
self.featureToggled.emit()
break
[docs] def clearSelection(self):
"""
Toggles of 'use' check boxes for all features.
"""
for row in self.rows:
row.use_feature = False
self.columnChanged(self.Column.FeatureName)
[docs] def selectedFeaturesCount(self):
"""
Returns total number of selected features (excluding volumes).
"""
selection = [
row.feature_name
for row in self.rows
if row.use_feature and not row.is_xvol
]
return len(selection)
[docs] def getLastCustomFeatureNum(self):
"""
Finds all 'custom' features and returns last feature number. For
example, if custom features are ['A1', 'D11', 'R5'] last custom
feature number will be 11.
"""
custom_feature_nums = [
int(row.feature_name[1:])
for row in self.rows
if row.hypo_name is None
]
return max(custom_feature_nums) if custom_feature_nums else 0
[docs] def updateFeatureNames(self, marker_features):
"""
This function updates feature names in the model so that they are
consistent with markers feature names. This is needed when user
changes feature type using edit feature dialog. In this case only
a single feature row needs to be modified.
:param marker_features: dictionary of feature marker names keyed
on hypothesis entry ids.
:type marer_features: dict
"""
# find feature that changed its name
original_names = self._getFeatureNames()
for row_num, row in enumerate(self.rows):
if row.is_xvol:
continue
hypo_eid = row.hypo_eid
if row.feature_name not in marker_features[hypo_eid]:
# Only one name in marker_features would be different from the
# ones in original_names. Here we find this name and use it as
# a 'new' feature name.
new_name = list(
set(marker_features[hypo_eid]) -
set(original_names[hypo_eid]))[0]
row.feature_name = new_name
row.use_feature = False
self.rowChanged(row_num)
return
def _getFeatureNames(self):
"""
Returns dictionary of all feature names in the model, which is keyed on
hypothesis entry ids.
:return: dictionary of feature names
:rtype: dict
"""
features = defaultdict(list)
for row in self.rows:
features[row.hypo_eid].append(row.feature_name)
return features
# For testing purposes only:
if __name__ == "__main__":
app = QtWidgets.QApplication([__file__])
selector = FeatureSelector()
selector.setFeatures([
'A1', 'A2', 'H1', 'H2', 'P1', 'D1', 'D2', 'D3', 'D4', 'R10', 'R11',
'R12', 'R13'
])
def _print(selection):
print(selection)
selector.selectionChanged.connect(_print)
selector.show()
selector.raise_()
app.exec()