Source code for schrodinger.ui.qt.propertyselector

#Module: propertyselector.py
# -*- coding: utf-8 -*-
"""
This module provides a PropertySelector GUI class for viewing and selecting
properties.  Pass the widget a list of properties (via the constructor or the
setProperties() method), and then extract properties selected by the user with
the getSelected() method.

PropertySelector will display properties without the type (e.g., ``i_``,
``s_``) or owner/family (e.g., ``mmod_``, ``sd_``) tags, for readability. The
list of properties displayed can be limited to a single family.

The listbox component can be set to multiple or single selection with the
boolean "multi" initialization option.

Copyright Schrodinger, LLC. All rights reserved.

"""

# Issues:
#
# How to keep the current selection when changing the listbox contents?
# What to do if property names (minus type and owner tags) aren't unique?
# More configurable: pass parameters for box height and width
# Should this class be a MegaWidget?
# Add an option to filter on type (string, integer, etc.).
# Long term: move to table format instead of listbox?
# Should we remove the "Deselect all" button in single-selection mode?

import re
import sys

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

maestro = get_maestro()


#
# Classes
#
# TODO: Convert this class to an actual Qt widget. Currently to use it, one
# needs to create "wrapper" widget to put it into first.
[docs]class PropertySelector: """ Class for selecting properties, with features to organize by family/owner, filter by name, and/or sort items in the list. The PropertySelector displays a list of property user names, specified as structure.PropertyName objects. Duplicate user names are distinguished by creating a PropertyName.user_name attribute to with a unique numerical id suffix. e.g. foo, foo-1, foo-2. The name checking is done once, at __init__. :ivar propfamiles: Family name keys for a list of structure.PropertyName instances in that group. :vartype propfamilies: dict :ivar displayed_aux_proplist: List of PropertyNames currently displayed in the auxilary listbox, in the order they appear in the list. This is typically a subset of self.proplist. :vartype displayed_aux_proplist: list :ivar move_to_aux: If True, items will be moved from main table to auxilary table when they are selected. If false, they will be duplicated in both tables if selected (default False) :vartype move_to_aux: bool """
[docs] def __init__( self, parent, proplist=None, multi=False, presort=True, show_family_menu=True, show_family_name=True, show_alpha_toggle=False, show_filter_field=False, show_aux_listbox=False, move_to_aux=False, ): """ PropertySelector requires <parent> argument, which should be a Qt Widget into which this frame should be added. :param proplist: The order of data name items in the list is the default order of items in the list box. :type proplist: list(str) or list(structure.PropertyName) :param multi: True allows selecting multiple properties. Default is False, single item selection. :type multi: bool :param show_family_menu: True add property family menu to control visible list items. Default is True. :type show_family_menu: bool :param show_family_name: True displays the family/owner in parentheses after each name. :type show_family_name: bool :param show_alpha_toggle: True displays the alphabetize checkbox toggle. :type show_alpha_toggle: bool :param show_filter_field: True displays the string filter controls. :type show_filter_field: bool :param show_aux_listbox: True displays the auxilary listbox, add and remove buttons, and decrement/increment buttons. self.getSelected() returns all the items in the auxilary list box, in the order they appear. :type show_aux_listbox: bool :param presort: True orders the properties alphabetically by PropertyName.userName() values. :type presort: bool :param move_to_aux: If True, items will be moved from main table to auxilary table when they are selected. If false, they will be duplicated in both tables if selected. :type move_to_aux: bool """ self.parent = parent if proplist: self.proplist = [ structure.PropertyName(str(prop)) for prop in proplist ] else: self.proplist = [] self.multipleselect = multi self.presort = presort self.selection_mutex = QtCore.QMutex() self.show_family_menu = show_family_menu self.show_family_name = show_family_name self.show_alpha_toggle = show_alpha_toggle self.show_filter_field = show_filter_field self.show_aux_listbox = show_aux_listbox self.move_to_aux = move_to_aux self.displayed_proplist = [] self.selected_proplist = [] self.selected_not_displayed_proplist = [] # FIXME replace with self.selected_proplist self.displayed_aux_proplist = [] # Key: short family name, value: list of PropertyName objects self.propfamilies = {} # Uniquify user names. self.checkUserNames() # Always show the list in alphabetical order, so sort # the actual proplist: if self.presort and not self.show_alpha_toggle: self.proplist = self.sortAlphabetically(self.proplist) self.setupUi(self.parent) # Add the items to the primary listbox. self.setProperties(self.proplist) # Disable add/remove inc/dec buttons: if self.show_aux_listbox: self.updateButtonStates() return
[docs] def setupUi(self, Form): Form.resize(500, 500) Form.setGeometry(QtCore.QRect(20, 81, 441, 322)) self.main_layout = QtWidgets.QVBoxLayout(Form) # Setup listbox_layout: self.listbox_layout = QtWidgets.QVBoxLayout() self.listbox_label = QtWidgets.QLabel(Form) self.listbox_label.setText("Available properties:") self.listbox = QtWidgets.QListWidget(Form) if self.multipleselect: # Enable multiple-select: self.listbox.setSelectionMode( QtWidgets.QAbstractItemView.ExtendedSelection) if self.show_aux_listbox: self.listbox.itemSelectionChanged.connect(self.updateButtonStates) self.listbox.itemDoubleClicked.connect(self.addToAuxListbox) self.listbox_layout.addWidget(self.listbox_label) self.listbox_layout.addWidget(self.listbox) # Pack things into lists_layout: self.lists_layout = QtWidgets.QHBoxLayout() self.lists_layout.addLayout(self.listbox_layout) if self.show_aux_listbox: # Setup add_remove_layout: self.add_remove_layout = QtWidgets.QVBoxLayout() add_spacer = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.add_remove_layout.addItem(add_spacer) self.add_button = QtWidgets.QToolButton(Form) self.add_button.setText(">") self.add_button.setText(u'\u2192') self.add_button.setShortcut("Right") # Ev:94401 self.add_button.clicked.connect(self.addToAuxListbox) self.add_remove_layout.addWidget(self.add_button) self.remove_button = QtWidgets.QToolButton(Form) self.remove_button.setText("<") self.remove_button.setText(u'\u2190') self.remove_button.setShortcut("Left") # Ev:94401 self.remove_button.clicked.connect(self.delFromAuxListbox) self.add_remove_layout.addWidget(self.remove_button) remove_spacer = QtWidgets.QSpacerItem( 20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.add_remove_layout.addItem(remove_spacer) # Setup aux_listbox_layout: self.aux_listbox_layout = QtWidgets.QVBoxLayout() self.aux_listbox_label = QtWidgets.QLabel(Form) self.aux_listbox_label.setText("Selected properties:") self.aux_listbox = QtWidgets.QListWidget(Form) # Aux listbox is always multiple-select: self.aux_listbox.setSelectionMode( QtWidgets.QAbstractItemView.ExtendedSelection) self.aux_listbox.itemSelectionChanged.connect( self.updateButtonStates) self.aux_listbox.itemDoubleClicked.connect(self.delFromAuxListbox) self.aux_listbox_layout.addWidget(self.aux_listbox_label) self.aux_listbox_layout.addWidget(self.aux_listbox) # Setup incr_decr_layout: self.incr_decr_layout = QtWidgets.QVBoxLayout() spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.incr_decr_layout.addItem(spacerItem2) self.increment_button = QtWidgets.QToolButton(Form) self.increment_button.setText(u'\u2191') self.incr_decr_layout.addWidget(self.increment_button) self.increment_button.clicked.connect(self.moveAuxSelectedUp) self.decrement_button = QtWidgets.QToolButton(Form) self.decrement_button.setText(u'\u2193') self.incr_decr_layout.addWidget(self.decrement_button) self.decrement_button.clicked.connect(self.moveAuxSelectedDown) decr_spacer = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.incr_decr_layout.addItem(decr_spacer) self.lists_layout.addLayout(self.add_remove_layout) self.lists_layout.addLayout(self.aux_listbox_layout) self.lists_layout.addLayout(self.incr_decr_layout) # Pack things into main_layout: self.main_layout.addLayout(self.lists_layout) if self.multipleselect: # Select all and Deselect all buttons: self.buttons_layout = QtWidgets.QHBoxLayout() self.selectall_button = QtWidgets.QPushButton(Form) self.selectall_button.setText("Select All") self.selectall_button.clicked.connect(self.selectAll) self.buttons_layout.addWidget(self.selectall_button) self.clear_button = QtWidgets.QPushButton(Form) self.clear_button.setText("Deselect All") self.clear_button.clicked.connect(self.deselectAll) self.buttons_layout.addWidget(self.clear_button) spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.buttons_layout.addItem(spacerItem4) self.main_layout.addLayout(self.buttons_layout) if self.show_family_menu: # Show family pull-down menu: self.show_family_layout = QtWidgets.QHBoxLayout() self.show_family_label = QtWidgets.QLabel(Form) self.show_family_label.setText("Show family:") self.show_family_layout.addWidget(self.show_family_label) self.show_family_option = QtWidgets.QComboBox(Form) self.show_family_option.setSizeAdjustPolicy( QtWidgets.QComboBox.AdjustToContents) self.show_family_layout.addWidget(self.show_family_option) self.show_family_layout.addStretch() # Add a stretch to the right self.main_layout.addLayout(self.show_family_layout) self.show_family_option.currentIndexChanged.connect( self.filteringChanged) if self.show_alpha_toggle: # Sort alphabetically checkbutton: self.alpha_layout = QtWidgets.QHBoxLayout() self.alpha_toggle = QtWidgets.QCheckBox(Form) self.alpha_toggle.setText("Sort alphabetically") self.alpha_toggle.setChecked(self.presort) self.alpha_layout.addWidget(self.alpha_toggle) spacerItem6 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.alpha_layout.addItem(spacerItem6) self.main_layout.addLayout(self.alpha_layout) self.alpha_toggle.toggled.connect(self.alphabetizeNames) if self.show_filter_field: # Filter entry field: self.filter_layout = QtWidgets.QHBoxLayout() self.filter_label = QtWidgets.QLabel(Form) self.filter_label.setText("Filter:") self.filter_layout.addWidget(self.filter_label) self.filter_field = QtWidgets.QLineEdit(Form) self.filter_layout.addWidget(self.filter_field) filter_spacer = QtWidgets.QSpacerItem( 40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.filter_layout.addItem(filter_spacer) self.show_family_label.setBuddy(self.show_family_option) self.filter_label.setBuddy(self.filter_field) self.main_layout.addLayout(self.filter_layout) self.filter_field.textChanged[str].connect(self.filteringChanged) QtCore.QMetaObject.connectSlotsByName(Form)
[docs] def updateButtonStates(self): """ Enable or disable buttons as needed. Called when selection in the tables changes. """ mainsel = self.getListboxSelection() auxsel = self.getAuxListboxSelection() self.add_button.setEnabled(bool(mainsel)) self.remove_button.setEnabled(bool(auxsel)) enableup = len(auxsel) == 1 and auxsel[0] != 0 self.increment_button.setEnabled(enableup) enabledown = len(auxsel) == 1 and \ auxsel[0] != self.aux_listbox.count() - 1 self.decrement_button.setEnabled(enabledown)
[docs] def sortAlphabetically(self, proplist): decorated_proplist = [] for prop_obj in proplist: item = (prop_obj.user_name, prop_obj) decorated_proplist.append(item) decorated_proplist.sort() sorted_proplist = [item[1] for item in decorated_proplist] return sorted_proplist
[docs] def sortByInitialOrder(self, proplist): decorated_proplist = [] for prop in proplist: native_index = self.proplist.index(prop) item = (native_index, prop) decorated_proplist.append(item) decorated_proplist.sort() sorted_proplist = [item[1] for item in decorated_proplist] return sorted_proplist
[docs] def alphabetizeNames(self, is_toggled): """ Alternate order the displayed list items, either alphabetically or in the native order depending on the value of self.alpha_toggle. :param is_toggled: whether to alphabetize the property names :type is_toggled: bool """ if is_toggled: self.displayed_proplist = self.sortAlphabetically( self.displayed_proplist) else: self.displayed_proplist = self.sortByInitialOrder( self.displayed_proplist) usernames = [prop.user_name for prop in self.displayed_proplist] self.listbox.clear() self.listbox.insertItems(0, usernames) return
[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) return
[docs] def filteringChanged(self, ignored): """ Called when value of the family pull-down menu is changed. """ if not self.selection_mutex.tryLock(): return # Get the current selected property names before altering the # listbox display membership. display_list = self.filterProperties(self.proplist) self.selected_not_displayed_proplist = [] selected_list = self.getSelected() for prop in selected_list: if prop not in display_list: self.selected_not_displayed_proplist.append(prop) # Limit display properties to the filtered ones: # Display and select properties: self.displayProperties(display_list, selected_list) self.selection_mutex.unlock()
[docs] def filterProperties(self, props): """ Limit the specified list items to those items that match the self.filter_field filter regex and family option menu. """ # Make a copy so that list that user speicified does not get altered: props = props[:] # Filter based on family (if user specified): if self.show_family_menu: family = str(self.show_family_option.currentText()) if family != 'ALL': short_family = structure.PROP_SHORT_NAME.get(family, family) new_props = [] for prop in props: if prop.family == short_family: new_props.append(prop) props = new_props # Fitler based on filter field: if self.show_filter_field: # Get the filter expression. filter_chars = str(self.filter_field.text()).strip() # FIXME: Deterimine how to handle literal backslash. # Common characters treated here, but this is not a comprehensive # list. meta_chars = ["*", "[", "]", "{", "}", ".", "?", "$", "^", "\\"] char_list = [] for char in filter_chars: if char in meta_chars: char = r"\%s" % char char_list.append(char) filter_re = re.compile(r'%s' % ''.join(char_list)) # Filter the properties based on search filter: new_props = [] for prop_obj in props: if filter_re.search(prop_obj.user_name): new_props.append(prop_obj) props = new_props # Get the current selected property names before altering the # listbox display membership. # Return a copy so that list that user speicified does not get altered: return props
[docs] def createFamilies(self): """ Create a dictionary of families. PropertyName.family keys for a list of PropertyNames in that family. """ for prop_obj in self.proplist: if prop_obj.family in self.propfamilies: # prop_obj.family is a short family name self.propfamilies[prop_obj.family].append(prop_obj) else: self.propfamilies[prop_obj.family] = [prop_obj] # Set option menu items to the full family names: # (and convert family names to long family names) displaynames = [] for fam in self.propfamilies: long_family_name = structure.PROP_LONG_NAME.get(fam, fam) # Some long family names can have more than one short name if long_family_name not in displaynames: displaynames.append(long_family_name) if self.show_family_menu: self.show_family_option.clear() new_items = ["ALL"] + displaynames self.show_family_option.addItems(new_items) return
[docs] def setProperties(self, proplist, selected_indexes=None): """ Set the listbox items to the structure.PropertyName.user_name attributes for the PropertyName instances in proplist. :param proplist: A list of properties to show. :type proplist: list of str (data names) or `structure.PropertyName`. :param selected_indexes: List of indexes of <proplist> that should be initially selected. :type selected_indexes: list of int """ _proplist = [] for prop in proplist: if isinstance(prop, structure.PropertyName): _proplist.append(prop) elif isinstance(prop, str): _proplist.append(structure.PropertyName(prop)) else: raise ValueError("Unexpected property type: %s" % type(prop)) # Note: selected list may include properties that are not in proplist self.selection_mutex.lock() self.proplist = _proplist self.checkUserNames() self.createFamilies() # Will reset family selection self.displayProperties(self.proplist, selected_indexes) self.selection_mutex.unlock()
[docs] def displayProperties(self, display_list, selected_list=None): """ Adjust the display state and selection states to specified lists of PropertyName objects. Use only when selection or display state changes. Use setProperties() to use different properties. display_list (list) A list of PropertyName objects to display. selected_list (list) A list of PropertyName objects to select. """ # Convert to indexes into self.proplist (if needed): selected_indexes = [] # Make sure we store a copy (Ev:84725) if selected_list: for prop in selected_list: if type(prop) == type(1): selected_indexes.append(prop) else: selected_indexes.append(self.proplist.index(prop)) # Limit display properties to the filtered ones: # (will make a copy of the list) display_list = self.filterProperties(display_list) self.selected_proplist = [self.proplist[i] for i in selected_indexes] if self.show_aux_listbox: self.showAuxProperties(self.selected_proplist) # showAuxProperties() must be called first if self.move_to_aux: # Do not show selected props in main list: selected_data_names = [ p.dataName() for p in self.selected_proplist ] for prop in display_list[:]: if prop.dataName() in selected_data_names: display_list.remove(prop) if self.show_alpha_toggle and self.alpha_toggle.isChecked(): self.displayed_proplist = self.sortAlphabetically(display_list) else: self.displayed_proplist = self.sortByInitialOrder(display_list) # display_list are properties to display in main table: display_usernames = [] selected_indexes = [] for i, prop in enumerate(self.displayed_proplist): display_usernames.append(prop.user_name) if not self.show_aux_listbox and prop in self.selected_proplist: selected_indexes.append(i) self.listbox.clear() self.listbox.insertItems(0, display_usernames) if not self.show_aux_listbox: for i in selected_indexes: item = self.listbox.item(i) item.setSelected(True) return
[docs] def getSelected(self): """ Return a list of selected PropertyName objects. """ proplist = None filtered = (self.show_filter_field or self.show_family_menu) if self.show_aux_listbox: # selected is a list of indexes for all items in aux listbox: selected = list(range(self.aux_listbox.count())) proplist = [self.displayed_aux_proplist[int(i)] for i in selected] elif not self.show_aux_listbox and filtered: # The current displayed and selected list. selected = self.getListboxSelection() proplist = [self.displayed_proplist[i] for i in selected] # Add to this anything selected prior to filtering but # not displayed. proplist.extend(self.selected_not_displayed_proplist) else: selected = self.getListboxSelection() proplist = [self.displayed_proplist[i] for i in selected] return proplist
[docs] def selectAll(self): """ Select all properties in the listbox. """ # Select all properties: self.displayProperties(self.proplist, self.proplist)
[docs] def deselectAll(self): """ Clear property selection """ # Clear the selection: self.displayProperties(self.proplist, [])
[docs] def setEnabled(self, enabled): """ Enable/disable the PropertySelector widget """ pass # FIXME
[docs] def getListboxSelection(self): selected = [] for item in self.listbox.selectedItems(): i = int(self.listbox.row(item)) selected.append(i) return selected
[docs] def getAuxListboxSelection(self): selected = [] for item in self.aux_listbox.selectedItems(): i = int(self.aux_listbox.row(item)) selected.append(i) return selected
[docs] def moveAuxSelectedDown(self): """ Reorders the displayed list items in self.aux_listbox such that the selected item moves down one rung in the listbox. """ selected_index_list = self.getAuxListboxSelection() if not selected_index_list: return selected_index = selected_index_list[0] selected_prop = self.displayed_aux_proplist[selected_index] old_index = self.displayed_aux_proplist.index(selected_prop) property_name = self.displayed_aux_proplist.pop(old_index) self.displayed_aux_proplist.insert(old_index + 1, property_name) self.showAuxProperties(self.displayed_aux_proplist, [old_index + 1]) return
[docs] def moveAuxSelectedUp(self): """ Reorders the displayed list items in self.aux_listbox such that the selected item moves up one rung in the listbox. """ selected_index_list = self.getAuxListboxSelection() if not selected_index_list: return selected_index = selected_index_list[0] if selected_index == 0: return # can't go any higher. selected_prop = self.displayed_aux_proplist[selected_index] old_index = self.displayed_aux_proplist.index(selected_prop) property_name = self.displayed_aux_proplist.pop(old_index) self.displayed_aux_proplist.insert(old_index - 1, property_name) self.showAuxProperties(self.displayed_aux_proplist, [old_index - 1]) return
[docs] def addToAuxListbox(self): """ Adds items selected in self.listbox to self.aux_listbox. """ new_sel_indexes = [ self.proplist.index(prop) for prop in self.displayed_aux_proplist ] for i in self.getListboxSelection(): prop = self.displayed_proplist[i] i = self.proplist.index(prop) if i not in new_sel_indexes: new_sel_indexes.append(i) # Add to aux list (and possibly remove from main list): self.displayProperties(self.proplist, new_sel_indexes) return
[docs] def delFromAuxListbox(self): """ Removes selected item from self.aux_listbox. """ selected = self.getAuxListboxSelection() # Must remove from the end, so need to sort first: selected.sort(reverse=True) for index in selected: self.displayed_aux_proplist.pop(index) # Modify selection: self.displayProperties(self.proplist, self.displayed_aux_proplist) return
[docs] def showAuxProperties(self, proplist, selected_indexes=None): """ Display the auxilary listbox items to the structure.PropertyName.user_name attributes for the PropertyName instances in proplist. proplist (list) List of structure.PropertyName instances. selected_indexes (list) List of indexes of <proplist> that should be selected. """ aux_usernames = [prop.user_name for prop in proplist] self.aux_listbox.clear() self.aux_listbox.insertItems(0, aux_usernames) if selected_indexes: for i in selected_indexes: item = self.aux_listbox.item(i) item.setSelected(True) self.displayed_aux_proplist = proplist return
# FIXME combine with PropertySelector
[docs]class PropertySelectorMenu: """ Pull-down menu style property selected widget. Used by VSW and Generate Phase DB GUIs """
[docs] def __init__(self, optionmenu, command=None, proplist=None): self.optionmenu = optionmenu if command: self._command = command optionmenu.currentIndexChanged.connect(self._callCommand) self.field_lookup = {} # key: menu text; value: PropertyName # List of PropertyName objects. First item is None. self.property_list = [] # Initialize the property list: if proplist: self.setProperties(proplist)
def _callCommand(self, selection_ignored): """ Calls the user-specified "command" with the name of the property """ if self._command: self._command(self.getSelected())
[docs] def getSelected(self): """ Return PropertyName object for the selected property, or None. """ selected = self.optionmenu.currentText() if not selected: return None else: return self.field_lookup[str(selected)]
[docs] def select(self, property): """ Select the specified property. <property> must be a data name string or a PropertyName object. """ for i, prop in enumerate(self.property_list): if prop == property or prop.dataName() == property: self.optionmenu.setCurrentIndex(i) return msg = "PropertySelectorMenu: property %s not in list!" % str(property) raise Exception(msg)
[docs] def setProperties(self, proplist): """ Change the list of properties. 'proplist' must be a list of PropertyName objects (from the 'structure.py' module) or data names :param proplist: A list of properties to show. :type proplist: list of str (data names) or `structure.PropertyName`. """ fields = [] self.field_lookup = {} self.property_list = [] for prop in proplist: if isinstance(prop, str): prop = structure.PropertyName(prop) field_text = prop.userNameWithFamily() self.field_lookup[field_text] = prop self.property_list.append(prop) fields.append(field_text) # Check that the <family>_<user> combinations are unique # FIXME replace with code that makes the names unique: for prop1 in list(self.field_lookup.values()): for prop2 in self.field_lookup.values(): if prop1 == prop2: continue if prop1.userNameWithFamily() == prop2.userNameWithFamily(): raise RuntimeError( "Error: Properties '%s' and '%s' share the same family/owner and user name" % (prop1.dataName(), prop2.dataName())) self.optionmenu.clear() self.optionmenu.addItems(fields)
[docs] def clear(self): self.optionmenu.clear()
[docs]class PropertySelectorDialog(QtWidgets.QDialog): """ A popup window that allows the user to grab a property. Use chooseFromPT() or chooseFromList() method to execute/show the dialog. """
[docs] def __init__(self, parent, type_filter=None, allow_empty=False, title='Choose Property', accept_text='Select', **kwargs): """ Create a dialog that allows the user to choose property(ies). :param parent: the parent widget for this dialog :type parent: QWidget :param type_filter: List of property types (first character of property name) to accept. Should be one or more of i, r, b and s. :type type_filter: list :param allow_empty: Whether to allow the user to accept the dialog when no property is selected. By default a warning dialog will appear in such case. :param title: Title to use for the dialog window :type title: str All keyword arguments supported by PropertySelector class can also be used. For example, set "multi=True" to allow selection of more than one property. """ if type_filter: # TODO: Move the type_filter option into the main PropertySelector # class. for type_char in type_filter: if type_char not in ('s', 'r', 'b', 'i'): raise ValueError('Unrecognized type: "%s"' % type_char) self.type_filter = type_filter self.allow_empty = allow_empty super(PropertySelectorDialog, self).__init__(parent) self.setWindowTitle(title) dialog_layout = QtWidgets.QVBoxLayout(self) dialog_layout.setContentsMargins(1, 1, 1, 1) # Property Selector selector_widget = QtWidgets.QWidget() dialog_layout.addWidget(selector_widget) self.selector = PropertySelector(selector_widget, **kwargs) # Button area swidgets.STwoButtonBox(layout=dialog_layout, accept_text=accept_text, close_text='Cancel', accept_command=self.accept, close_command=self.reject)
[docs] def accept(self): if not self.selector.getSelected() and not self.allow_empty: msg = 'Please select a property or press Cancel' QtWidgets.QMessageBox.warning(self, 'Warning', msg) else: super(PropertySelectorDialog, self).accept()
[docs] def chooseFromPT(self): """ Ask the user to choose from the available Project Table properties. :return list of PropertyName objects for properties that were selected. Returns None if the user cancelled. NOTE: If allow_empty is True, the returned list may be empty. """ proplist = maestro.project_table_get().getPropertyNames() if self.type_filter: proplist = [ prop for prop in proplist if prop[0] in self.type_filter ] if not proplist: raise ValueError( "No properties of specified type exist in the Project Table" ) return self.chooseFromList(proplist)
[docs] def chooseFromList(self, proplist, selected_indexes=None): """ Ask the user to choose from the given list of properties. :param proplist: A list of properties to show. :type proplist: list of str (data names) or `structure.PropertyName`. :param selected_indexes: List of indexes of <proplist> that should be initially selected, if desired. :type selected_indexes: list of int :return list of PropertyName objects for properties that were selected. Returns None if the user cancelled. NOTE: If allow_empty is True, the returned list may be empty. """ if not proplist: raise ValueError("proplist is empty") if self.type_filter: proplist = [ prop for prop in proplist if prop[0] in self.type_filter ] if not proplist: raise ValueError( "None of the properties matched the specified type filter.") self.selector.setProperties(proplist, selected_indexes) ret = self.exec() if ret: # OK was pressed; return selected properties. NOTE: This list may # be empty is allow_empty is True. return self.selector.getSelected() else: # Cancel pressed return None
[docs]class MyWindow(QtWidgets.QWidget):
[docs] def __init__(self, *args): QtWidgets.QWidget.__init__(self, *args) # Create a check button for each option: layout = QtWidgets.QVBoxLayout() self.multi_toggle = QtWidgets.QCheckBox("Multi", self) self.multi_toggle.setChecked(True) layout.addWidget(self.multi_toggle) self.presort_toggle = QtWidgets.QCheckBox("Presort", self) self.presort_toggle.setChecked(True) layout.addWidget(self.presort_toggle) self.show_family_toggle = QtWidgets.QCheckBox("Show family menu", self) self.show_family_toggle.setChecked(True) layout.addWidget(self.show_family_toggle) self.show_alpha_toggle = QtWidgets.QCheckBox("Show alpha option", self) self.show_alpha_toggle.setChecked(True) layout.addWidget(self.show_alpha_toggle) self.show_filter_toggle = QtWidgets.QCheckBox("Show filter option", self) self.show_filter_toggle.setChecked(True) layout.addWidget(self.show_filter_toggle) self.show_aux_toggle = QtWidgets.QCheckBox("Show aux table", self) self.show_aux_toggle.setChecked(True) layout.addWidget(self.show_aux_toggle) self.move_to_aux_toggle = QtWidgets.QCheckBox("Move to aux table", self) self.move_to_aux_toggle.setChecked(True) layout.addWidget(self.move_to_aux_toggle) self.display_button = QtWidgets.QPushButton("Display", self) layout.addWidget(self.display_button) self.display_button.clicked.connect(self.propsDialog) self.close_button = QtWidgets.QPushButton("Close", self) layout.addWidget(self.close_button) self.close_button.clicked.connect(self.close) self.setLayout(layout)
[docs] def propsDialog(self): all_props = ['s_m_title', 's_user_foo', 'i_valid_selected'] prop_objects = [structure.PropertyName(prop) for prop in all_props] selected_indexes = [2] dialog = QtWidgets.QDialog() dialog.setWindowTitle("PropertySelector") dialog_layout = QtWidgets.QVBoxLayout(dialog) interior_widget = QtWidgets.QWidget() property_selector = PropertySelector( interior_widget, multi=self.multi_toggle.isChecked(), presort=self.presort_toggle.isChecked(), show_aux_listbox=self.show_aux_toggle.isChecked(), show_family_menu=self.show_family_toggle.isChecked(), show_alpha_toggle=self.show_alpha_toggle.isChecked(), show_filter_field=self.show_filter_toggle.isChecked(), move_to_aux=self.move_to_aux_toggle.isChecked(), ) property_selector.setProperties(prop_objects, selected_indexes) dialog_layout.addWidget(interior_widget) self.buttonBox = QtWidgets.QDialogButtonBox(dialog) self.buttonBox.setGeometry(QtCore.QRect(30, 240, 341, 32)) self.buttonBox.setOrientation(QtCore.Qt.Horizontal) self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Ok) dialog_layout.addWidget(self.buttonBox) self.buttonBox.accepted.connect(dialog.accept) result = dialog.exec() if result: # OK pressed selected_props = property_selector.getSelected() print('SELECTED PROPS:', selected_props)
[docs]class PropertySelectorList(QtWidgets.QFrame): """ A widget for showing properties in a QListWidget, and letting the user select a property (or multiple properties) in it. Above the list widget there is a field for searching for properties. """
[docs] def __init__( self, parent=None, proplist=None, multi=False, reorderable=False, ): """ :type parent: QObject :param parent: The parent of this widget. :type proplist: list :param proplist: list of property data names to show. :type multi: bool :param multi: Whether multiple properties can be selected. Default is False. :type reorderable: bool :param reorderable: True if items can be dragged around to reorder. """ super().__init__(parent) self.main_layout = swidgets.SVBoxLayout(self) list_widget = QtWidgets.QListWidget(self) self.list_widget = list_widget if proplist: self.setProperties(proplist) list_widget.setSelectionMode( QtWidgets.QListWidget.ExtendedSelection if multi else QtWidgets. QListWidget.SingleSelection) # Internal dragging if reorderable: list_widget.setDragEnabled(True) list_widget.setDropIndicatorShown(True) list_widget.setDragDropMode(self.InternalMove) self.filter_field = swidgets.SLineEdit(self, show_search_icon=True) self.filter_field.setPlaceholderText('Search') self.filter_field.textChanged[str].connect(self.filterList) self.main_layout.addWidget(self.filter_field) self.main_layout.addWidget(list_widget)
[docs] def selectProperty(self, prop): """ Selects the given property data name. """ for row in range(self.list_widget.count()): item = self.list_widget.item(row) if item.data(Qt.UserRole) == prop: item.setSelected(True) return raise ValueError("No such property: %s" % prop)
[docs] def getSelected(self): """ Return the list of selected property data names. """ return [x.data(Qt.UserRole) for x in self.list_widget.selectedItems()]
[docs] def filterList(self, filtervalue): """ Filter the items in the list widget according to the contents of the LineEdit and our filter rules :type filtervalue: str :param filtervalue: The current value to filter the ListWidget by """ matched_items = self.list_widget.findItems(filtervalue, Qt.MatchFlag.MatchContains) for row in range(self.list_widget.count()): item = self.list_widget.item(row) matches = item in matched_items item.setHidden(not matches) if not matches: item.setSelected(False)
[docs] def setProperties(self, proplist): """ Add specified properties to the list widget. :param proplist: A list of properties to show. :type proplist: list of str (data names) or `structure.PropertyName`. """ # Make a copy, casted to str: self.list_widget.clear() for prop in proplist: prop = structure.PropertyName(prop) item = QtWidgets.QListWidgetItem(prop.userNameWithFamily()) item.setData(Qt.UserRole, prop.dataName()) self.list_widget.addItem(item)
[docs] def clear(self): """ Removes all items from the ListWidget """ self.filter_field.clear() self.list_widget.clear()
if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) w = MyWindow() w.show() sys.exit(app.exec()) # EOF