Source code for schrodinger.application.bioluminate.propfilter

"""
This contains a QDialog class that allows the user to define criteria to filter
properties by.

It also contains classes that hold the criteria and determine if properties
match the criteria.

Copyright Schrodinger, LLC. All rights reserved.

"""
# Contributors: Jeff A. Saunders, Matvey Adzhigirey, David Giesen

# ToDo:
#
# Enable "Add" only when settings appropriate?
#

import schrodinger.ui.qt.propertyselector as propertyselector
from schrodinger import structure
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.ui.qt import swidgets

# Import the module(s) created from the QtDesigner *.ui file(s):
from . import propfilter_ui

#
# Global constants
#
TEXTBOX_FONT = "Courier New"
EQUALS = '='
NOT_EQUALS = 'Not ='
CONTAINS = 'Contains'
STARTSWITH = 'Starts with'
ENDSWITH = 'Ends with'
NOT_CONTAINS = 'Does not contain'
EXISTS = 'Exists'
NOT_EXISTS = 'Does not exist'
SELECT = 'Select'
GREATEREQUAL = '>='
LESSEREQUAL = '<='
GREATER = '>'
LESSER = '<'
IS_TRUE = 'is True'
IS_FALSE = 'is False'
AND = 'AND'
OR = 'OR'
STRING_OPS = [
    EQUALS, NOT_EQUALS, CONTAINS, NOT_CONTAINS, STARTSWITH, ENDSWITH, EXISTS,
    NOT_EXISTS, SELECT
]
NUMBER_OPS = [
    EQUALS, NOT_EQUALS, GREATEREQUAL, LESSEREQUAL, GREATER, LESSER, EXISTS,
    NOT_EXISTS, SELECT
]
BOOL_OPS = [IS_TRUE, IS_FALSE, EXISTS, NOT_EXISTS]
MUST_HAVE_VALUE = set([
    EQUALS, NOT_EQUALS, CONTAINS, NOT_CONTAINS, STARTSWITH, ENDSWITH,
    GREATEREQUAL, LESSEREQUAL, GREATER, LESSER
])


[docs]class StringDatabaseCriterion(object): """ Holds a criterion for a string property and determines whether a database object or structure meets that criterion """
[docs] def __init__(self, dataname, username, criteria, joiner=""): """ Create a StringDatabaseCriterion object :type dataname: str :param dataname: The internal name of the property this criterion checks. The dataname is of the form x_y_z. :type username: str :param username: The name the user sees for this property :type criteria: list of tuple :param criteria: List of one or two criterion. Each criterion is a tuple consisting of (operator, value). operator is some relationship defined by the constants in this module, and value is the target value. For instance, in property > 5, '>' is the operator and 5 is the value. Operator constants are: - EQUALS - NOT_EQUALS - CONTAINS - STARTSWITH - ENDSWITH - NOT_CONTAINS - EXISTS - NOT_EXISTS - SELECT - GREATEREQUAL - LESSEREQUAL - GREATER - LESSER - IS_TRUE - IS_FALSE For the SELECT operator, value can be a list of values. A property that matches any one of those values exactly matches the criterion. :type joiner: str :param joiner: If two criteria are provided, joiner is the boolean that connects them - either AND or OR. Default is the empty string, which means that only the first criterion in the criteria list will be examined. """ self.property = dataname self.criteria = criteria self.joiner = joiner # Create the string that represents this criterion self.string = username.replace(' ', '_') self.string = self.string + ' ' + criteria[0][0].lower() if criteria[0][1] is not None: self.string = self.string + ' ' + str(criteria[0][1]) if joiner: self.string = self.string + ' ' + joiner self.string = self.string + ' ' + criteria[1][0].lower() if criteria[1][1] is not None: self.string = self.string + ' ' + str(criteria[1][1])
def __str__(self): return self.string
[docs] def checkForMatch(self, obj_val, operator, goal, isCaseSensitive=False): """ Check to see if a value matches this criterion. Returns True if:: "obj_val operator goal" ("Schrodinger contains odin" returns True) :type obj_val: str :param obj_val: The value being checked :type operator: str :param operator: An operator as defined in the `__init__` method criteria paramater. :type goal: str or list :param goal: The value to compare against, or in the case of the SELECT operator, goal is a list of str :type isCaseSensitive: bool :rtype: bool :return: True if "obj_val operator goal" is True, False if not :raises ValueError: if operator is not recognized """ if obj_val is None: return operator == NOT_EXISTS elif operator == EQUALS: if not isCaseSensitive: return str(obj_val).lower() == str(goal).lower() else: return obj_val == goal elif operator == NOT_EQUALS: if not isCaseSensitive: return str(obj_val).lower() != str(goal).lower() else: return obj_val != goal elif operator == CONTAINS: if not isCaseSensitive: return str(obj_val).lower().find(str(goal).lower()) > -1 else: return obj_val.find(goal) > -1 elif operator == NOT_CONTAINS: if not isCaseSensitive: return str(obj_val).lower().find(str(goal).lower()) == -1 else: return obj_val.find(goal) == -1 elif operator == STARTSWITH: if not isCaseSensitive: return str(obj_val).lower().startswith(str(goal).lower()) else: return obj_val.startswith(goal) elif operator == ENDSWITH: if not isCaseSensitive: return str(obj_val).lower().endswith(str(goal).lower()) else: return obj_val.endswith(goal) elif operator == EXISTS: return True elif operator == NOT_EXISTS: return False elif operator == SELECT: if not isCaseSensitive: goal2 = [] for i in range(len(goal)): goal2.append(str(goal[i]).lower()) return str(obj_val).lower() in goal2 else: strval = str(obj_val) return strval in goal else: raise ValueError('Unknown operator', operator)
[docs] def matches(self, dbobj=None, struct=None, stem='bioluminate', isCaseSensitive=False): """ Checks to see if a `schrodinger.application.prime.packages.PrimeStructureDatabase.PrimeStructureDBEntry` database object or a `schrodinger.structure.Structure` object matches this Criterion. :type dbobj: `schrodinger.application.prime.package.PrimeStructureDatabase.PrimeStructureDBEntry` :param dbobj: The database object to check for a match. Only dbobj or struct should be provided, but not both. If both are provided, the struct object is checked and the dbobj is ignored. :type struct: `schrodinger.structure.Structure` :param struct: The Structure object to check for a match. Only dbobj or struct should be provided, but not both. If both are provided, the struct object is checked and the dbobj is ignored. :type stem: str :param stem: If given, this value will be inserted at the beginning of the second part of the Criterion property name when checking a struct object. Property x_y_z will become x_stemy_z. Default is 'bioluminate'. This is done because the database removes stem from property names when storing data. :type isCaseSensitive: bool :rtype: bool :return: True if the provided object passes this Criterion, False if not. """ if not self.criteria: # An empty Criterion matches anything return True if struct: propname = self.property[:2] + stem + self.property[2:] try: obj_val = struct.property[propname] except KeyError: obj_val = None else: try: obj_val = dbobj.data[self.property] except KeyError: obj_val = None results = [] for operator, goal in self.criteria: results.append( self.checkForMatch(obj_val, operator, goal, isCaseSensitive)) if self.joiner: if self.joiner == AND: return all(results) else: return any(results) else: return results[0]
[docs]class NumericalDatabaseCriterion(StringDatabaseCriterion):
[docs] def checkForMatch(self, obj_val, operator, goal, isCaseSensitive=None): """ Check to see if a value matches this criterion. Returns True if:: "obj_val operator goal" ("20 >= 10" returns True) :type obj_val: int or float :param obj_val: The value being checked :type operator: int or float :param operator: An operator as defined in the `__init__` method of the `StringDatabaseCriterion` class criteria paramater. :type goal: int, float or list :param goal: The value to compare against, or in the case of the SELECT operator, goal is a list of int or float :param isCaseSensitive: ignored (added for compatibility with base class) :rtype: bool :return: True if "obj_val operator goal" is True, False if not :raises ValueError: if operator is not recognized """ if obj_val is None: return operator == NOT_EXISTS elif operator == EQUALS: return obj_val == goal elif operator == NOT_EQUALS: return obj_val != goal elif operator == GREATEREQUAL: return obj_val >= goal elif operator == GREATER: return obj_val > goal elif operator == LESSEREQUAL: return obj_val <= goal elif operator == LESSER: return obj_val < goal elif operator == EXISTS: return True elif operator == NOT_EXISTS: return False elif operator == SELECT: strval = str(obj_val) return strval in goal else: raise ValueError('Unknown operator', operator)
[docs]class BooleanDatabaseCriterion(StringDatabaseCriterion):
[docs] def checkForMatch(self, obj_val, operator, goal=None, isCaseSensitive=None): """ Check to see if a value matches this criterion. Returns True if:: "obj_val operator" ("Schrodinger is True" returns True) :type obj_val: int or float :param obj_val: The value being checked :type operator: int or float :param operator: An operator as defined in the `__init__` method of the `StringDatabaseCriterion` class criteria paramater. :param goal: Not used, none of the Boolean operators requires a value to compare against. :param isCaseSensitive: ignored (added for compatibility with base class) :rtype: bool :return: True if "obj_val operator" is True, False if not :raises ValueError: if operator is not recognized """ if obj_val is None: return operator == NOT_EXISTS elif operator == IS_TRUE: return obj_val elif operator == IS_FALSE: return not obj_val elif operator == EXISTS: return True elif operator == NOT_EXISTS: return False else: raise ValueError('Unknown operator', operator)
[docs]class CriteriaListModel(QtCore.QAbstractListModel): """ Class for storing the criteria list information. """
[docs] def __init__(self): QtCore.QAbstractListModel.__init__(self) self.criteria = []
[docs] def rowCount(self, parent=QtCore.QModelIndex()): # noqa: M511 """ Returns number of rows """ return len(self.criteria)
[docs] def clear(self): self.beginResetModel() self.criteria = [] self.endResetModel()
[docs] def addLine(self, line, linetype=None): row_idx = self.rowCount() self.beginInsertRows(QtCore.QModelIndex(), row_idx, row_idx) self.criteria.append([line, linetype]) self.endInsertRows()
[docs] def data(self, index, role=Qt.DisplayRole): """ Given a cell index, returns the data that should be displayed in that cell (text or check button state). Used by the view. """ if role == Qt.DisplayRole: try: line, linetype = self.criteria[index.row()] except IndexError: return return line
[docs] def flags(self, index): """ Returns flags for the specified cell. Whether selectable or not. """ if not index.isValid(): return 0 try: line, linetype = self.criteria[index.row()] except IndexError: return Qt.ItemIsEnabled if linetype: return Qt.ItemIsSelectable | Qt.ItemIsEnabled else: return Qt.ItemIsEnabled
[docs]class SelectingFilteredListWidget(swidgets.SFilteredListWidget): """ A QListWidget that contains a edit to allow the user to filter the contents of the widget. If a filter is being applied, all the matching contents are selected. """
[docs] def filterList(self, filtervalue): """ Modified from the parent class to select all the objects in the ListWidget that match the filter. :type filtervalue: str :param filtervalue: The value used to filter the ListWidget """ swidgets.SFilteredListWidget.filterList(self, filtervalue) if filtervalue: self.selectAll()
[docs]class PropertySelectorWithRanges(propertyselector.PropertySelector): """ A PropertySelector class that appends ranges to the names of numeric properties. """
[docs] def __init__(self, master, *args, **kwargs): """ Create a PropertySelectorWithRanges instance. :type master: object with getPropertyMinMax method :param master: The master panel that supplies the min and max values of numerical properties for the range display """ propertyselector.PropertySelector.__init__(self, *args, **kwargs) self.master = master
[docs] def checkUserNames(self): """ Assigns PropertyName.user_name attributes, for all members in self.proplist. The user_names are unique within the PropertySelector instance. The user_name strings are typically just the PropertyName.userName(), but may be modified with a numerical suffix to make them unique, or decorated with the family name (which does not garuntee uniqeness, but may make the items easier for the end user to visually group). """ # Assign the user_name attributes. assigned_user_names = [] for property_name in self.proplist: orig_name = property_name.userName() if self.show_family_name: fam = property_name.family orig_name = "%s (%s)" % (orig_name, structure.PROP_LONG_NAME.get(fam, fam)) user_name = orig_name if user_name in assigned_user_names: attempt = 1 while user_name in assigned_user_names: user_name = "%s-%d" % (orig_name, attempt) attempt += 1 property_name.user_name = user_name assigned_user_names.append(property_name.user_name)
[docs]class ValueSelectionDialog(QtWidgets.QDialog): """ A SFilteredListWidget that allows the user to select certain values of a property for the criterion. """
[docs] def __init__(self, parent, values, propmenu, value_edit): """ Create a ValueSelectionDialog instance :type parent: QWidget with setSelectedValues method :param parent: The parent widget of this dialog. The accept method of this dialog calls the setSelectedValues method of the parent with the list of selected text in the QListWidget :type values: list :param values: List of values to display in the QListWidget :type propmenu: QComboBox or object with setCurrentIndex method :param propmenu: The cancel method of this dialog sets propmenu.setCurrentIndex(0) :param value_edit: value_edit is passed to parent.setSelectedValues in the accept method of the dialog """ QtWidgets.QDialog.__init__(self, parent) layout = swidgets.SVBoxLayout(self) items = [str(x) for x in values] self.listwidget = SelectingFilteredListWidget( layout=layout, mode='extended', items=items, label='Select values to accept') self.propmenu = propmenu self.value_edit = value_edit # Bottom buttons dbb = QtWidgets.QDialogButtonBox dialog_buttons = dbb(dbb.Ok | dbb.Cancel) dialog_buttons.accepted.connect(self.accept) dialog_buttons.rejected.connect(self.reject) layout.addWidget(dialog_buttons)
[docs] def accept(self): """ Actions when the user presses the accept button. Calls parent.setSelectedValues(values, value_edit) where values is a list of selected values and value_edit is passed to the __init__ method """ self.parent().setSelectedValues(self.listwidget.selectedText(), self.value_edit) return QtWidgets.QDialog.accept(self)
[docs] def reject(self): """ Actions when the user presses the cancel button. Calls propmenu.setCurrentIndex(0) on the propmenu passed to the __init__ function. """ self.propmenu.setCurrentIndex(0) return QtWidgets.QDialog.reject(self)
[docs]class PropFilterDialog(QtWidgets.QDialog): """ Panel to allow the user to define criteria for filtering property values """
[docs] def __init__(self, parent, criteria=None, databases=None): """ Create a PropFilterDialog instance :type parent: QWidget or object with setCriteria method :param parent: The accept method of this dialog calls the setCriteria method of the parent with a list of `StringDatabaseCriterion`, `NumericalDatabaseCriterion` and `BooleanDatabaseCriterion` objects defined by the user :type criteria: list :param criteria: A list of existing `StringDatabaseCriterion`, `NumericalDatabaseCriterion` and `BooleanDatabaseCriterion` objects that will display and can be deleted in this panel :type database: `schrodinger.application.prime.packages.PrimeStructureDatabase.PrimeStructureDB` :param database: The database to open during panel creation. If no database is provided, either a database will have to be provided with the `loadDatabase` method or properties passed in via the `setProperties` method before displaying the panel. """ QtWidgets.QDialog.__init__(self, parent) # Set up GUI widgets and hook up signals self.ui = propfilter_ui.Ui_Form() self.ui.setupUi(self) self.prop_selector = PropertySelectorWithRanges( self, self.ui.propselector_widget) self.prop_selector.listbox.itemSelectionChanged.connect( self.selectProperty) self.ui.prop_edit.textChanged[str].connect(self.checkPropertyType) self.ui.operator1_combo.currentTextChanged.connect( self.operator1Changed) self.ui.operator2_combo.currentTextChanged.connect( self.operater2Changed) self.ui.joiner_combo.addItems(("", AND, OR)) self.ui.joiner_combo.currentTextChanged.connect(self.joinerChanged) self.ui.addcriterion_btn.clicked.connect(self.addCriterion) self.ui.addcriterion_btn.setAutoDefault(True) self.ui.addcriterion_btn.setDefault(True) # # Filtering information for criteria display # self.filter_model = CriteriaListModel() self.ui.filter_listview.setSelectionMode( QtWidgets.QAbstractItemView.SingleSelection) self.ui.filter_listview.setModel(self.filter_model) self.ui.filter_listview.selectionModel().selectionChanged.connect( self.filterSelectionChanged) self.ui.filter_listview.setFont(QtGui.QFont(TEXTBOX_FONT)) # Delete self.ui.delete_button.clicked.connect(self.deleteEntry) self.filterSelectionChanged() # Bottom buttons dbb = QtWidgets.QDialogButtonBox dialog_buttons = dbb(dbb.Ok | dbb.Cancel) dialog_buttons.accepted.connect(self.accept) dialog_buttons.rejected.connect(self.reject) self.ui.main_vlayout.addWidget(dialog_buttons) # Give the panel a decent size size = self.size() size.setWidth(800) self.resize(size) self.databases = [] self.criteria = [] self.setDefaults(criteria=criteria) if databases: self.loadDatabases(databases) self.updateText()
[docs] def getCumulativeMatches(self): if not self.databases: return [0 * len(self.criteria)] cumulative_matches = [] for database in self.databases: entries = database.SelectEntries() for crit in self.criteria: match = [] for entry in entries: if crit.matches( dbobj=entry, isCaseSensitive=self.ui.checkBox.isChecked()): match.append(entry) cumulative_matches.append(len(match)) entries = match[:] return cumulative_matches
[docs] def accept(self): """ Add the selected criteria to the parent - calls parent.setCriteria() with a list of `StringDatabaseCriterion`, `NumericalDatabaseCriterion` and `BooleanDatabaseCriterion` objects """ self.parent().setCriteria(self.criteria) return QtWidgets.QDialog.accept(self)
[docs] def filterSelectionChanged(self, selected=None, deselected=None): """ Enables or disables the Delete button as appropriate depending on the selection in the Criteria display area :param selected: Unused :param deselected: Unused """ # Determine if any of the selected lines are Criterion lines rows_selected = False selected_rows = self.ui.filter_listview.selectionModel().selectedRows() for index in selected_rows: rownum = index.row() line, linetype = self.filter_model.criteria[rownum] if linetype: rows_selected = True break self.ui.delete_button.setEnabled(rows_selected)
[docs] def selectProperty(self): """ Populate the property name edit with the property name of the property the user just selected. """ selected_props = self.prop_selector.getSelected() if selected_props: self.ui.prop_edit.setText(selected_props[0].userName())
[docs] def joinerChanged(self, result): """ Enable/disable the second criteria widgets as appropriate for the joiner ("", AND, OR) that the user just selected. :type result: str :param result: The current value selected in the joiner combobox """ is_joiner = bool(result) self.ui.operator2_combo.setEnabled(is_joiner) self.ui.value2_edit.setEnabled(is_joiner)
[docs] def updateText(self): """ Rewrite the text in the criteria text box """ self.filter_model.clear() # Tag some of the lines so they can be selected by the special # mouse bindings (in selectTextLine). if self.criteria: self.filter_model.addLine('#\n# Filter criteria\n#') cumulative = self.getCumulativeMatches() for criterion in self.criteria: self.filter_model.addLine(str(criterion), "criterion") self.filter_model.addLine(' Cumulative matches = %d' % cumulative.pop(0))
[docs] def loadDatabases(self, databases): """ Load in the data from the database - populates the upper text box with property names. Adds Misc, Light, or Heavy to property names so the user can filter them by class. :type database: `schrodinger.application.prime.packages.PrimeStructureDatabase.PrimeStructureDB` :param database: The database to load data from """ self.databases = databases # Fixes PANEL-1965: We only consider 'common' properties, which are # found in all databases. props = self.findCommonDatanames(databases) propertynames = [] # Add the Light, Heavy or Misc class to the property name by making the # second block of the property name one of those strings. This fakes # the property display into showing and filtering on these classes. for prop in props: tokens = prop.split('_') if tokens[0] not in ('i', 'r', 's', 'b'): continue t2_lower = tokens[2].lower() tokens[1] = self.convertToClassToken(t2_lower) classed_name = '_'.join(tokens) propname = structure.PropertyName(dataname=classed_name) # Add a property to the PropertyName object that is the actual # database property name propname.actual_dataname = prop propertynames.append(propname) def propsort(property): return property.userName() propertynames.sort(key=propsort) self.setProperties(propertynames)
[docs] def convertToClassToken(self, s): """ This function converts given string to one of class tokens (either 'Light', 'Heavy' or 'Misc'). Valid 'light' and 'heavy' strings are defined in light and heavy variables respectively. :param s: property class string :type s: str :return: class token :rtype: str """ light = ['light', 'l1', 'l2', 'l3'] heavy = ['heavy', 'h1', 'h2', 'h3'] token = None for val in light: if s.startswith(val): token = 'Light' break if not token: for val in heavy: if s.startswith(val): token = 'Heavy' break if not token: token = 'Misc' return token
[docs] def findCommonDatanames(self, databases): """ This function find column names that are present in all databases. :param databases: List of the databases to load data from. :type databases: list :return: list of common column names :rtype: list """ propnames = [db.GetColumns() for db in databases] if propnames: common_props = list(set(propnames[0]).intersection(*propnames[1:])) else: common_props = [] return common_props
[docs] def addCriterion(self): """ Adds a criterion when the user hits the Add button """ # Validity and type checks prop = str(self.ui.prop_edit.text()) if not prop: self.warning('No property name specified') return try: propname = self.propertyname_objs[prop.lower()] except KeyError: self.warning('Unrecognized property name specified') return is_string = propname.type == structure.PROP_STRING is_int = propname.type == structure.PROP_INTEGER is_float = propname.type == structure.PROP_FLOAT is_bool = propname.type == structure.PROP_BOOLEAN # Extract the data from the widgets def convert_val(value): if value == "": return None if is_bool: return bool(value) elif is_int: return int(value) elif is_float: return float(value) else: return value # First criterion operator = str(self.ui.operator1_combo.currentText()) if operator != SELECT: value = convert_val(self.ui.value1_edit.text()) else: value = self.ui.value1_edit.selected_values if value is None and operator in MUST_HAVE_VALUE: self.warning('A value is required for this criterion') return joiner = str(self.ui.joiner_combo.currentText()) crits = [(operator, value)] # User selected AND/OR, so grab the second criterion if joiner: value2 = convert_val(self.ui.value2_edit.text()) operator2 = str(self.ui.operator2_combo.currentText()) if not operator2: self.warning('AND/OR specified but no second criterion') return if value2 is None and operator2 in MUST_HAVE_VALUE: self.warning('A value is required for the second criterion') return crits.append((operator2, value2)) # Create the Criterion object try: property_name = propname.actual_dataname except AttributeError: property_name = propname.dataName() if is_int or is_float: # Using userName() rather than prop makes sure the capitalization is # correct criterion = NumericalDatabaseCriterion(property_name, propname.userName(), crits, joiner=joiner) elif is_bool: criterion = BooleanDatabaseCriterion(property_name, propname.userName(), crits, joiner=joiner) else: criterion = StringDatabaseCriterion(property_name, propname.userName(), crits, joiner=joiner) self.criteria.append(criterion) self.updateText() self.ui.prop_edit.setFocus() self.ui.prop_edit.selectAll()
[docs] def operator1Changed(self, result): """ Disable/enable the value edit as appropriate for the selected operator. Opens a `ValueSelectionDialog` if the operator is SELECT. :type result: str :param result: The current value in the 1st operator combobox """ result = str(result) if result not in MUST_HAVE_VALUE: # Operators without values. self.ui.value1_edit.clear() self.ui.value1_edit.setReadOnly(True) else: self.ui.value1_edit.setReadOnly(False) if result == SELECT: # Open up a filtered listwidget to let the user choose values self.letUserSelectValues(self.ui.operator1_combo, self.ui.value1_edit)
[docs] def operater2Changed(self, result): """ Disable/enable the value edit as appropriate for the selected operator. Opens a `ValueSelectionDialog` if the operator is SELECT. :type result: str :param result: The current value in the 2nd operator combobox """ result = str(result) if result not in MUST_HAVE_VALUE: # Operators without values. self.ui.value2_edit.clear() self.ui.value2_edit.setReadOnly(True) else: self.ui.value2_edit.setReadOnly(False) if result == SELECT: # Open up a filtered listwidget to let the user choose values self.letUserSelectValues(self.ui.operator2_combo, self.ui.value2_edit)
[docs] def letUserSelectValues(self, propmenu, value_edit): """ Open a `ValueSelectionDialog` to allow the user to select values of the property that will match the criterion. :type propmenu: QComboBox or object with setCurrentIndex method :param propmenu: The cancel method of the dialog sets propmenu.setCurrentIndex(0) :param value_edit: value_edit is passed to `setSelectedValues` in the accept method of the dialog """ prop = str(self.ui.prop_edit.text()) # Check for valid property name if not prop: self.warning('No property name specified, cannot select values') propmenu.setCurrentIndex(0) return try: propname = self.propertyname_objs[prop.lower()] except KeyError: self.warning('Unrecognized property name specified,' 'cannot select values') propmenu.setCurrentIndex(0) return values = [] for database in self.databases: try: db_values = database.GetValues(propname.actual_dataname) except AttributeError: # actual_dataname may not be set if properties were passed in by a # method other than loadDatabase - just use the regular dataName() db_values = database.GetValues(propname.dataName()) values.extend(db_values) value_edit.selected_values = [] select_dialog = ValueSelectionDialog(self, values, propmenu, value_edit) select_dialog.exec()
[docs] def setSelectedValues(self, values, value_edit): """ Called by `ValueSelectionDialog` to set the text in the proper value_edit to the values the user selected. :type values: list :param values: list of values selected by the user :type value_edit: QLineEdit :param value_edit: The edit that values should be placed in """ valstring = ','.join(values) value_edit.setText(valstring) value_edit.selected_values = values
[docs] def checkPropertyType(self, prop): """ Callback for modifications to the property QLineEdit. Adjust the operator list and value validator according to the property type entered. :type prop: str :param prop: The current value of the property QLineEdit """ prop = str(prop) try: propname = self.propertyname_objs[prop.lower()] except KeyError: # Probably in mid edit return if propname.type == structure.PROP_STRING: # Restrict to "Exists", because the backend doesn't support string # comparisons. operators = STRING_OPS self.ui.value1_edit.setValidator(None) self.ui.value2_edit.setValidator(None) elif propname.type == structure.PROP_INTEGER: operators = NUMBER_OPS self.ui.value1_edit.setValidator(QtGui.QIntValidator(self)) self.ui.value2_edit.setValidator(QtGui.QIntValidator(self)) elif propname.type == structure.PROP_FLOAT: operators = NUMBER_OPS self.ui.value1_edit.setValidator(QtGui.QDoubleValidator(self)) self.ui.value2_edit.setValidator(QtGui.QDoubleValidator(self)) else: operators = BOOL_OPS # No need to set validators, no value entry allowed for booleans self.ui.operator1_combo.clear() self.ui.operator2_combo.clear() self.ui.operator1_combo.addItems(operators) self.ui.operator2_combo.addItems(operators)
[docs] def deleteEntry(self): """ Delete the selected criterion or pattern """ selected_rows = self.ui.filter_listview.selectionModel().selectedRows() if not selected_rows: # Shouldn't happen, but just in case... self.warning('No criterion selected') return for index in selected_rows: rownum = index.row() line, linetype = self.filter_model.criteria[rownum] line = str(line) if linetype == "criterion": for crit in self.criteria: if line == crit.string: self.criteria.remove(crit) self.updateText() break
[docs] def setDefaults(self, criteria=None): """ Set the panel defaults :type criteria: list :param criteria: Pre-existing `StringDatabaseCriterion`, `NumericalDatabaseCriterion` and `BooleanDatabaseCriterion` objects that should be added the criterion area """ if criteria: self.criteria = criteria else: self.criteria = [] self.propertyname_objs = {} self.selectedprop = None self.ui.prop_edit.setText("") self.ui.operator1_combo.clear() self.ui.operator1_combo.addItems(STRING_OPS) self.ui.value1_edit.setText("") self.ui.value2_edit.setText("") self.setProperties(None) self.ui.joiner_combo.setCurrentIndex(0) self.ui.operator2_combo.clear() self.ui.operator2_combo.addItems(STRING_OPS)
[docs] def setProperties(self, property_names): """ Set the list of properties the user can choose from. :type property_names: list of `schrodinger.structure.PropertyName` :param property_names: Each item of the list is a `schrodinger.structure.PropertyName` object for a property that the user can select from """ self.propertyname_objs = {} if not property_names: self.prop_selector.setProperties([]) else: self.prop_selector.setProperties(property_names) usernames = [] # Create a completer that will complete property names as the user # types them into the property name field for pname in property_names: uname = pname.userName() usernames.append(uname) self.propertyname_objs[uname.lower()] = pname completer = QtWidgets.QCompleter(usernames) completer.setCaseSensitivity(Qt.CaseInsensitive) self.ui.prop_edit.setCompleter(completer)
[docs] def warning(self, text): """ Pop up a warning dialog :type text: str :param text: The text to display in the dialog """ QtWidgets.QMessageBox.warning(self, "Warning", text)
[docs] def getPropertyMinMax(self, property): """ Get the min and max values of property from the database. :type property: str :param property: The name of a database property to get the min and max of :rtype: list :return: [min, max] of property. Returns [None, None] if the database is not defined. """ # NOTE: This method is currently not used; see BIOLUM-2661 min_value = None max_value = None for database in self.databases: new_min, new_max = database.GetRange(property) if min_value is None: min_value = new_min if max_value is None: max_value = new_max if new_min < min_value: min_value = new_min if new_max > max_value: max_value = new_max return [min_value, max_value]
if __name__ == '__main__': from schrodinger.application.prime.packages import \ PrimeStructureDatabase as psd app = QtWidgets.QApplication([]) panel = PropFilterDialog(None) database = psd.PrimeStructureDB('testdb.db') panel.loadDatabase(database) panel.exec()