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 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)