Source code for schrodinger.ui.qt.filter_list

"""
Module for widgets that allow for dynamic filtering of a list.

Copyright Schrodinger, LLC. All rights reserved.
"""
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 icons_rc  # noqa: F401
from schrodinger.ui.qt import pop_up_widgets
from schrodinger.ui.qt import swidgets
from schrodinger.ui.qt.appframework2.settings import SettingsMixin
from schrodinger.ui.qt.standard.colors import LightModeColors

GROUP_KEY = 'filter_group'
FILTER_KEY_BASE = 'filter_cb_'


[docs]class FilterCheckBox(QtWidgets.QCheckBox): """ A checkbox that applies a predefined filter to items in a QtWidgets.QListWidget. """
[docs] def __init__(self, label_text, filter_func, parent=None, on_by_default=False): """ :param label_text: Label text to be shown for this checkbox. :type label_text: str :param filter_func: Filter function to be applied to the list items. :type filter_func: callable :param parent: Parent widget. :type parent: QtWidgets.QWidget :param on_by_default: Whether the filter is on by default. :type on_by_default: bool """ super().__init__(label_text, parent) self._filter_func = filter_func self._on_by_default = on_by_default self.setDefault()
[docs] def setDefault(self): """ Reset the checkbox to it's default state. """ self.setChecked(self._on_by_default)
[docs] def applyFilter(self, list_items): """ Apply the filter for this checkbox to the specified list items. Note that items will not be unhidden, so any allowed items should be explicitly unhidden before being passed here. :param list_items: List items to be filtered :type list_items: iterable(QtWidgets.QListWidgetItem) """ for item in list_items: if not self._filter_func(item): item.setHidden(True) item.setSelected(False)
[docs]class PredefinedFiltersPopUp(SettingsMixin, pop_up_widgets.PopUp): """ Class for a popup containing predefined filter checkboxes. """
[docs] def __init__(self, parent, filter_cbs, toggle_filtering_text=None, match_msg=None, no_match_msg=None): """ :param parent: Parent widget. :type parent: QtWidgets.QWidget :param filter_cbs: tuple of filter checkboxes for this popup. :type filter_cbs: iterable(FilterCheckBox) :param toggle_filtering_text: Text to show for a group box checkbox to turn filtering on and off. If not specified, no group box checkbox will be shown. :type toggle_filtering_text: str :param match_msg: Label message to display when items match :type match_msg: str :param no_match_msg: Text to show when no items match filters :type no_match_msg: str """ self._filter_cbs = tuple(filter_cbs) self._toggle_filtering_text = toggle_filtering_text self._match_msg = match_msg or 'Items must match all selected filters.' self._no_match_msg = no_match_msg if no_match_msg is not None else 'No items match current filters.' super().__init__(parent)
[docs] def setup(self): """ Set up the filters and lay them out in the pop up widget. """ self.main_layout = swidgets.SVBoxLayout(self) self.filter_group_box = QtWidgets.QGroupBox(self) checkable = self._toggle_filtering_text is not None if checkable: self.filter_group_box.setCheckable(True) self.filter_group_box.setChecked(True) self.filter_group_box.setTitle(self._toggle_filtering_text) self.filter_group_box.toggled.connect( lambda: self.dataChanged.emit('')) filters_layout = swidgets.SVBoxLayout(self.filter_group_box) self.main_layout.addWidget(self.filter_group_box) for filter_cb in self._filter_cbs: filter_cb.toggled.connect(lambda: self.dataChanged.emit('')) filters_layout.addWidget(filter_cb) self._match_label = QtWidgets.QLabel(self._match_msg) self.main_layout.addWidget(self._match_label)
[docs] def hasActiveFilters(self): """ Return whether there are currently active filters. :return: Whether there are active filters. :rtype: bool """ if (self.filter_group_box.isCheckable() and not self.filter_group_box.isChecked()): return False return any(cb.isChecked() for cb in self._filter_cbs)
[docs] def applyFilters(self, list_items): """ Apply all active filters to the specified list items. Note that this function will not unhide any items, so all allowed items should be explicitly shown before being passed here. :param list_items: List items to be filtered. :type list_items: iterable(QtWidgets.QListWidgetItem) """ self._match_label.setText(self._match_msg) self._match_label.setStyleSheet( f'QLabel {{color: {LightModeColors.STANDARD_TEXT};}}') if not self.hasActiveFilters(): return for filter_cb in self._filter_cbs: if filter_cb.isChecked(): filter_cb.applyFilter(list_items) if all(i.isHidden() for i in list_items): self._match_label.setText(self._no_match_msg) self._match_label.setStyleSheet( f'QLabel {{color: {LightModeColors.ERROR_TEXT};}}')
[docs] def clearFilters(self): """ Clear all predefined filters. """ for filter_cb in self._filter_cbs: filter_cb.setChecked(False)
[docs] def getSettings(self, **kwargs): """ Get the current filter settings. :return: Map of the current filter settings :rtype: dict """ settings = super().getSettings(**kwargs) settings[GROUP_KEY] = self.filter_group_box.isChecked() for idx, cb in enumerate(self._filter_cbs): settings[FILTER_KEY_BASE + f'{idx}'] = cb.isChecked() return settings
[docs] def applySettings(self, settings): """ Apply the specified settings to the filters. :param settings: Settings to be applied. :type settings: dict """ super().applySettings(settings) if self.filter_group_box.isCheckable(): self.filter_group_box.setChecked(settings.get(GROUP_KEY, False)) for idx, cb in enumerate(self._filter_cbs): cb.setChecked(settings.get(FILTER_KEY_BASE + F'{idx}', False))
[docs]class FilterListPopUp(SettingsMixin, pop_up_widgets.PopUp): """ This class implements a group of widgets that can be used to dynamically filter the contents of a list in a popup that can be associated with another widget. :ivar filtersChanged: Signal emitted when filters are toggled. emits a dict of current filter settings. :type filtersChanged: QtCore.pyQtSignal(dict) """ filtersChanged = QtCore.pyqtSignal(dict)
[docs] def __init__(self, parent, list_items=(), filter_cbs=(), toggle_filtering_text=None, match_msg=None, no_match_msg=None): """ :param parent: Parent widget. :type parent: QtWidgets.QWidget :param list_items: Items to populate in the filtered list :type list_items: iterable(QtWidget.QListWidgetItem) :param filter_cbs: tuple of filter checkboxes for this popup. :type filter_cbs: tuple(FilterCheckBox) :param toggle_filtering_text: Text to show for a group box checkbox to turn filtering on and off. If not specified, no group box checkbox will be shown. :type toggle_filtering_text: str :param match_msg: Label message to display when items match :type match_msg: str :param no_match_msg: Text to show when no items match filters :type no_match_msg: str """ self._list_items = list_items self._filter_cbs = filter_cbs self._toggle_filtering_text = toggle_filtering_text self._match_msg = match_msg self._no_match_msg = no_match_msg super().__init__(parent)
[docs] def setup(self): """ Sets up the widget's UI and connections. """ self.main_layout = swidgets.SVBoxLayout(self) self._setUpFilterByText() self._setUpFilteredList() self.applyFilters()
def _setUpFilterByText(self): """ Set up the widgets for dynamic filtering by user-entered text. """ parent = self.parent() pop_up_parent = parent.window() if parent else self self._predefined_filters_pop_up = PredefinedFiltersPopUp( pop_up_parent, self._filter_cbs, self._toggle_filtering_text, self._match_msg, self._no_match_msg) self._predefined_filters_pop_up.dataChanged.connect(self.applyFilters) self._filter_le = swidgets.SLineEdit(self, show_search_icon=True) self._filter_le.textChanged.connect(self.applyFilters) self._filter_le.returnPressed.connect(self.onReturnPressed) self._show_filters_btn = pop_up_widgets.ToolButtonWithPopUp( self, arrow_type=Qt.NoArrow) self._show_filters_btn.setPopUp(self._predefined_filters_pop_up) self._show_filters_btn.setPopupValign( self._show_filters_btn.ALIGN_BOTTOM) self._filters_active_icon = QtGui.QIcon( ':/schrodinger/ui/qt/icons_dir/filter-menu-icon-active.png') self._filters_inactive_icon = QtGui.QIcon( ':/schrodinger/ui/qt/icons_dir/filter-menu-icon.png') self._updateFilterButtonIcon() text_search_layout = swidgets.SHBoxLayout() text_search_layout.addWidget(self._filter_le) text_search_layout.addWidget(self._show_filters_btn) self.main_layout.addLayout(text_search_layout) def _setUpFilteredList(self): """ Set up the list that can be dynamically filtered by the user. """ self._list_widget = QtWidgets.QListWidget(self) for item in self._list_items: self._list_widget.addItem(item) self._list_widget.currentItemChanged.connect(self.onDataChanged) self._list_widget.itemClicked.connect(self.onReturnPressed) self.main_layout.addWidget(self._list_widget)
[docs] def applyFilters(self): """ Apply the current filter text and predefined filters to the list contents. """ filter_str = self._filter_le.text() search_items = self._list_widget.findItems(filter_str, Qt.MatchFlag.MatchContains) exact_item = self._list_widget.findItems(filter_str, Qt.MatchFlag.MatchFixedString) if len(exact_item) == 1: exact_item[0].setSelected(True) for idx in range(self._list_widget.count()): item = self._list_widget.item(idx) matches = item in search_items item.setHidden(not matches) if not matches: item.setSelected(False) self._predefined_filters_pop_up.applyFilters(search_items) self._updateFilterButtonIcon() self.filtersChanged.emit(self.getSettings())
[docs] def clearFilters(self): """ Clear all item filters. """ self._filter_le.setText('') self._predefined_filters_pop_up.clearFilters() for idx in range(self._list_widget.count()): self._list_widget.item(idx).setHidden(False)
[docs] def isItemHidden(self, item_txt): """ :return: True if the item with the specified text is in the list and hidden, False otherwise. :rtype: bool """ items = self._list_widget.findItems(item_txt, Qt.MatchFlag.MatchExactly) if len(items) != 1: return False return items[0].isHidden()
[docs] def onDataChanged(self): """ Emit the dataChanged signal when a new item is selected. """ current_item = self._list_widget.currentItem() data = current_item.text() if current_item else '' self.dataChanged.emit(data)
def _updateFilterButtonIcon(self): """ Update the icon for the filter button depending on the current filter state. """ active = self._predefined_filters_pop_up.hasActiveFilters() icon = self._filters_active_icon if active else self._filters_inactive_icon self._show_filters_btn.setIcon(icon)
[docs] def onReturnPressed(self): """ Check if an item has been selected and, if so, set that item as the current item. If a current item has been set, hide the popup. """ selected_item = self._list_widget.selectedItems() if len(selected_item) == 1: self._list_widget.setCurrentItem(selected_item[0]) current_item = self._list_widget.currentItem() if current_item: self.hide()
[docs] def keyPressEvent(self, event): """ Close the popup if enter/return is pressed. See Qt documentation for argument documentation. """ # FIXME: Should find out why this is needed to get Enter/Return functionality working since the pop_up_widgets module has events filters that # should achieve this functionality... super().keyPressEvent(event) if event.key() in (Qt.Key_Return, Qt.Key_Enter): self.onReturnPressed()
[docs] def subWidgetHasFocus(self): # See parent class for method documentation # The PopUp implementation of this method will return False when the # filtering pop up has focus since that pop up is parented to the # window, not to this pop up. To get around this and make sure that we # don't close this pop up while the user is interacting with the # filtering pop up, we explicitly check the filtering pop up for focus. return (super().subWidgetHasFocus() or self._predefined_filters_pop_up.subWidgetHasFocus())
[docs]class ToolButtonWithFilterListPopUp(pop_up_widgets.ToolButtonWithPopUp): """ Custom toolbutton with a filter list popup. :ivar filtersChanged: Signal emitted when filters are toggled. emits a dict of current filter settings. :type filtersChanged: QtCore.pyQtSignal(dict) """ POP_UP_CLASS = FilterListPopUp filtersChanged = QtCore.pyqtSignal(dict)
[docs] def __init__(self, parent): super().__init__(parent, self.POP_UP_CLASS, arrow_type=Qt.NoArrow) self.setIcon(QtGui.QIcon(":/schrodinger/ui/qt/icons_dir/list.png")) self._pop_up.filtersChanged.connect(self.filtersChanged.emit)
[docs] def isItemHidden(self, item_txt): """ Return whether the specified item text is hidden. :param item_txt: Item text to check :type item_txt: str :return: True if item is n list and is hidden, False otherwise :rtype: bool """ return self._pop_up.isItemHidden(item_txt)
[docs] def clearFilters(self): """ Clear all pop up filters """ self._pop_up.clearFilters()
[docs] def getSettings(self, **kwargs): """ Get the current filter settings. :return: Map of the current filter settings :rtype: dict """ return self._predefined_filters_pop_up.getSettings(**kwargs)
[docs] def applySettings(self, settings): """ Apply the specified settings to the filters. :param settings: Settings to be applied. :type settings: dict """ self._predefined_filters_pop_up.applySettings(settings)