mirror of
https://github.com/pyapp-kit/superqt.git
synced 2025-12-13 09:50:05 +01:00
feat: Add QSearchableListWidget and QSearchableComboBox widgets (#80)
* implement widgets * add basic documentation * Add examples * try version without packaging Co-authored-by: Talley Lambert <talley.lambert@gmail.com>
This commit is contained in:
@@ -61,3 +61,8 @@ combo.setEnumClass(SampleEnum, allow_none=True)
|
||||
```
|
||||
|
||||
In this case there is added option `----` and `currentEnum` will return `None` for it.
|
||||
|
||||
## QSearchableComboBox
|
||||
|
||||
`QSearchableComboBox` is a variant of [`QComboBox`](https://doc.qt.io/qt-5/qcombobox.html) that
|
||||
allow to filter list of options by enter part of text. It could be drop in replacement for `QComboBox`.
|
||||
|
||||
8
docs/listwidgets.md
Normal file
8
docs/listwidgets.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# ListWidget
|
||||
|
||||
## QSearchableListWidget
|
||||
|
||||
`QSearchableListWidget` is a variant of [`QListWidget`](https://doc.qt.io/qt-5/qlistwidget.html) that add text entry above list widget that allow to filter list
|
||||
of available options.
|
||||
|
||||
Because of implementation it does not inherit directly from `QListWidget` but satisfy it all api. The only limitation is that it cannot be used as argument of `QListWidgetItem` constructor.
|
||||
11
examples/searchable_combo_box.py
Normal file
11
examples/searchable_combo_box.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QSearchableComboBox
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
slider = QSearchableComboBox()
|
||||
slider.addItems(["foo", "bar", "baz", "foobar", "foobaz", "barbaz"])
|
||||
slider.show()
|
||||
|
||||
app.exec_()
|
||||
11
examples/searchable_list_widget.py
Normal file
11
examples/searchable_list_widget.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QSearchableListWidget
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
slider = QSearchableListWidget()
|
||||
slider.addItems(["foo", "bar", "baz", "foobar", "foobaz", "barbaz"])
|
||||
slider.show()
|
||||
|
||||
app.exec_()
|
||||
@@ -35,6 +35,7 @@ project_urls =
|
||||
[options]
|
||||
packages = find:
|
||||
install_requires =
|
||||
packaging
|
||||
qtpy>=1.1.0
|
||||
typing-extensions>=3.10.0.0
|
||||
python_requires = >=3.7
|
||||
|
||||
@@ -7,7 +7,8 @@ except ImportError:
|
||||
|
||||
from ._eliding_label import QElidingLabel
|
||||
from .collapsible import QCollapsible
|
||||
from .combobox import QEnumComboBox
|
||||
from .combobox import QEnumComboBox, QSearchableComboBox
|
||||
from .selection import QSearchableListWidget
|
||||
from .sliders import (
|
||||
QDoubleRangeSlider,
|
||||
QDoubleSlider,
|
||||
@@ -26,13 +27,15 @@ __all__ = [
|
||||
"QDoubleRangeSlider",
|
||||
"QDoubleSlider",
|
||||
"QElidingLabel",
|
||||
"QEnumComboBox",
|
||||
"QLabeledDoubleRangeSlider",
|
||||
"QLabeledDoubleSlider",
|
||||
"QLabeledRangeSlider",
|
||||
"QLabeledSlider",
|
||||
"QLargeIntSpinBox",
|
||||
"QMessageHandler",
|
||||
"QSearchableComboBox",
|
||||
"QSearchableListWidget",
|
||||
"QRangeSlider",
|
||||
"QEnumComboBox",
|
||||
"QCollapsible",
|
||||
]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from ._enum_combobox import QEnumComboBox
|
||||
from ._searchable_combo_box import QSearchableComboBox
|
||||
|
||||
__all__ = ("QEnumComboBox",)
|
||||
__all__ = ("QEnumComboBox", "QSearchableComboBox")
|
||||
|
||||
48
src/superqt/combobox/_searchable_combo_box.py
Normal file
48
src/superqt/combobox/_searchable_combo_box.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from qtpy import QT_VERSION
|
||||
from qtpy.QtCore import Qt, Signal
|
||||
from qtpy.QtWidgets import QComboBox, QCompleter
|
||||
|
||||
try:
|
||||
is_qt_bellow_5_14 = tuple(int(x) for x in QT_VERSION.split(".")[:2]) < (5, 14)
|
||||
except ValueError:
|
||||
is_qt_bellow_5_14 = False
|
||||
|
||||
|
||||
class QSearchableComboBox(QComboBox):
|
||||
"""
|
||||
ComboCox with completer for fast search in multiple options
|
||||
"""
|
||||
|
||||
if is_qt_bellow_5_14:
|
||||
textActivated = Signal(str) # pragma: no cover
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setEditable(True)
|
||||
self.completer_object = QCompleter()
|
||||
self.completer_object.setCaseSensitivity(Qt.CaseInsensitive)
|
||||
self.completer_object.setCompletionMode(QCompleter.PopupCompletion)
|
||||
self.completer_object.setFilterMode(Qt.MatchContains)
|
||||
self.setCompleter(self.completer_object)
|
||||
self.setInsertPolicy(QComboBox.NoInsert)
|
||||
if is_qt_bellow_5_14: # pragma: no cover
|
||||
self.currentIndexChanged.connect(self._text_activated)
|
||||
|
||||
def _text_activated(self): # pragma: no cover
|
||||
self.textActivated.emit(self.currentText())
|
||||
|
||||
def addItem(self, *args):
|
||||
super().addItem(*args)
|
||||
self.completer_object.setModel(self.model())
|
||||
|
||||
def addItems(self, *args):
|
||||
super().addItems(*args)
|
||||
self.completer_object.setModel(self.model())
|
||||
|
||||
def insertItem(self, *args) -> None:
|
||||
super().insertItem(*args)
|
||||
self.completer_object.setModel(self.model())
|
||||
|
||||
def insertItems(self, *args) -> None:
|
||||
super().insertItems(*args)
|
||||
self.completer_object.setModel(self.model())
|
||||
3
src/superqt/selection/__init__.py
Normal file
3
src/superqt/selection/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from ._searchable_list_widget import QSearchableListWidget
|
||||
|
||||
__all__ = ("QSearchableListWidget",)
|
||||
46
src/superqt/selection/_searchable_list_widget.py
Normal file
46
src/superqt/selection/_searchable_list_widget.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QLineEdit, QListWidget, QVBoxLayout, QWidget
|
||||
|
||||
|
||||
class QSearchableListWidget(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.list_widget = QListWidget()
|
||||
|
||||
self.filter_widget = QLineEdit()
|
||||
self.filter_widget.textChanged.connect(self.update_visible)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(self.filter_widget)
|
||||
layout.addWidget(self.list_widget)
|
||||
self.setLayout(layout)
|
||||
|
||||
def __getattr__(self, item):
|
||||
if hasattr(self.list_widget, item):
|
||||
return getattr(self.list_widget, item)
|
||||
return super().__getattr__(item)
|
||||
|
||||
def update_visible(self, text):
|
||||
items_text = [
|
||||
x.text() for x in self.list_widget.findItems(text, Qt.MatchContains)
|
||||
]
|
||||
for index in range(self.list_widget.count()):
|
||||
item = self.item(index)
|
||||
item.setHidden(item.text() not in items_text)
|
||||
|
||||
def addItems(self, *args):
|
||||
self.list_widget.addItems(*args)
|
||||
self.update_visible(self.filter_widget.text())
|
||||
|
||||
def addItem(self, *args):
|
||||
self.list_widget.addItem(*args)
|
||||
self.update_visible(self.filter_widget.text())
|
||||
|
||||
def insertItems(self, *args):
|
||||
self.list_widget.insertItems(*args)
|
||||
self.update_visible(self.filter_widget.text())
|
||||
|
||||
def insertItem(self, *args):
|
||||
self.list_widget.insertItem(*args)
|
||||
self.update_visible(self.filter_widget.text())
|
||||
35
tests/test_searchable_combobox.py
Normal file
35
tests/test_searchable_combobox.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from superqt import QSearchableComboBox
|
||||
|
||||
|
||||
class TestSearchableComboBox:
|
||||
def test_constructor(self, qtbot):
|
||||
widget = QSearchableComboBox()
|
||||
qtbot.addWidget(widget)
|
||||
|
||||
def test_add_items(self, qtbot):
|
||||
widget = QSearchableComboBox()
|
||||
qtbot.addWidget(widget)
|
||||
widget.addItems(["foo", "bar"])
|
||||
assert widget.completer_object.model().rowCount() == 2
|
||||
widget.addItem("foobar")
|
||||
assert widget.completer_object.model().rowCount() == 3
|
||||
widget.insertItem(1, "baz")
|
||||
assert widget.completer_object.model().rowCount() == 4
|
||||
widget.insertItems(2, ["bazbar", "foobaz"])
|
||||
assert widget.completer_object.model().rowCount() == 6
|
||||
assert widget.itemText(0) == "foo"
|
||||
assert widget.itemText(1) == "baz"
|
||||
assert widget.itemText(2) == "bazbar"
|
||||
|
||||
def test_completion(self, qtbot):
|
||||
widget = QSearchableComboBox()
|
||||
qtbot.addWidget(widget)
|
||||
widget.addItems(["foo", "bar", "foobar", "baz", "bazbar", "foobaz"])
|
||||
|
||||
widget.completer_object.setCompletionPrefix("fo")
|
||||
assert widget.completer_object.completionCount() == 3
|
||||
assert widget.completer_object.currentCompletion() == "foo"
|
||||
widget.completer_object.setCurrentRow(1)
|
||||
assert widget.completer_object.currentCompletion() == "foobar"
|
||||
widget.completer_object.setCurrentRow(2)
|
||||
assert widget.completer_object.currentCompletion() == "foobaz"
|
||||
34
tests/test_searchable_list.py
Normal file
34
tests/test_searchable_list.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from superqt import QSearchableListWidget
|
||||
|
||||
|
||||
class TestSearchableListWidget:
|
||||
def test_create(self, qtbot):
|
||||
widget = QSearchableListWidget()
|
||||
qtbot.addWidget(widget)
|
||||
widget.addItem("aaa")
|
||||
assert widget.count() == 1
|
||||
|
||||
def test_add_items(self, qtbot):
|
||||
widget = QSearchableListWidget()
|
||||
qtbot.addWidget(widget)
|
||||
widget.addItems(["foo", "bar"])
|
||||
assert widget.count() == 2
|
||||
widget.insertItems(1, ["baz", "foobaz"])
|
||||
widget.insertItem(2, "foobar")
|
||||
assert widget.count() == 5
|
||||
assert widget.item(0).text() == "foo"
|
||||
assert widget.item(1).text() == "baz"
|
||||
assert widget.item(2).text() == "foobar"
|
||||
|
||||
def test_completion(self, qtbot):
|
||||
widget = QSearchableListWidget()
|
||||
qtbot.addWidget(widget)
|
||||
widget.show()
|
||||
widget.addItems(["foo", "bar", "foobar", "baz", "bazbar", "foobaz"])
|
||||
widget.filter_widget.setText("fo")
|
||||
assert widget.count() == 6
|
||||
for i in range(widget.count()):
|
||||
item = widget.item(i)
|
||||
assert item.isHidden() == ("fo" not in item.text())
|
||||
|
||||
widget.hide()
|
||||
Reference in New Issue
Block a user