Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collapsible Widget API v1.1 #23

Merged
merged 8 commits into from
Jan 8, 2024
74 changes: 52 additions & 22 deletions brainglobe_utils/qtpy/collapsible_widget.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import List, Optional, Union
from typing import List, Optional

from qtpy.QtCore import Qt, Signal
from qtpy.QtWidgets import QGroupBox, QVBoxLayout, QWidget
from qtpy.QtWidgets import QVBoxLayout, QWidget
from superqt.collapsible import QCollapsible


Expand Down Expand Up @@ -59,62 +59,92 @@
self.toggled_signal_with_self.emit(self, state)


class CollapsibleWidgetContainer(QGroupBox):
class CollapsibleWidgetContainer(QWidget):
"""
Container for multiple CollapsibleWidgets with the ability to add,
remove, and synchronize their states.
remove, and synchronize their states. Non-CollapsibleWidgets can also
be added.

Methods
-------
add_widget(QWidget or CollapsibleWidget)
add_widget(QWidget)
Adds a widget to the CollapsibleWidgetContainer.
remove_drawer(QWidget or CollapsibleWidget)
Removes a widget from the CollapsibleWidgetContainer.
_update_drawers(signalling_drawer, state)
Private method to synchronize drawer states.
"""

def __init__(self):
def __init__(self, parent=None):
"""
Initializes a new CollapsibleWidgetContainer instance.
"""
super().__init__()
super().__init__(parent)
self.setLayout(QVBoxLayout())

self.layout().setAlignment(Qt.AlignTop)
self.layout().setSpacing(0)
self.layout().setContentsMargins(0, 0, 0, 0)
self.collapsible_widgets: List[CollapsibleWidget] = []

def add_widget(self, widget: Union[QWidget, CollapsibleWidget]):
def add_widget(
self, widget: QWidget, collapsible: bool = True, widget_title: str = ""
):
"""
Adds a QWidget or a CollapsibleWidget to the chest.

Parameters
----------
widget : QWidget or CollapsibleWidget
widget : QWidget
The widget instance to be added.
collapsible : bool, optional
Whether the widget should be collapsible.
widget_title : str, optional
The title of the CollapsibleWidget.
IgorTatarnikov marked this conversation as resolved.
Show resolved Hide resolved
"""
if isinstance(widget, CollapsibleWidget):
self.collapsible_widgets.append(widget)
widget.toggled_signal_with_self.connect(self._update_drawers)

self.layout().addWidget(widget, 0, Qt.AlignTop)

def remove_widget(self, widget: Union[QWidget, CollapsibleWidget]):
if collapsible:
collapsible_widget = CollapsibleWidget(widget_title, parent=self)
collapsible_widget.setContent(widget)
collapsible_widget.toggled_signal_with_self.connect(
self._update_drawers
)
collapsible_widget.collapse(animate=False)
self.collapsible_widgets.append(collapsible_widget)
self.layout().addWidget(collapsible_widget, 0, Qt.AlignTop)
else:
self.layout().addWidget(widget, 0, Qt.AlignTop)

def remove_widget(self, widget: QWidget):
"""
Removes a widget from the chest.

Parameters
----------
widget : QWidget or CollapsibleWidget
widget : QWidget
The widget instance to be removed.
"""
if isinstance(widget, CollapsibleWidget):
self.collapsible_widgets.remove(widget)
widget.toggled_signal_with_self.disconnect(self._update_drawers)

self.layout().removeWidget(widget)
Raises
------
ValueError
If the widget is not found.
"""
for i in range(self.layout().count()):
child_widget = self.layout().itemAt(i).widget()
if (
isinstance(child_widget, CollapsibleWidget)
and child_widget.content() is widget
):
self.layout().removeWidget(child_widget)
self.collapsible_widgets.remove(child_widget)
child_widget.toggled_signal_with_self.disconnect(
self._update_drawers
)
return
elif child_widget is widget:
self.layout().removeWidget(widget)
return

raise ValueError("Widget not found")

Check warning on line 147 in brainglobe_utils/qtpy/collapsible_widget.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_utils/qtpy/collapsible_widget.py#L147

Added line #L147 was not covered by tests

def _update_drawers(
self, signalling_widget: CollapsibleWidget, state: bool
Expand Down
141 changes: 104 additions & 37 deletions tests/tests/test_qtpy/test_collapsible_widget.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import pytest
from qtpy.QtWidgets import QLabel, QPushButton
from qtpy.QtWidgets import (
QFormLayout,
QHBoxLayout,
QLabel,
QPushButton,
QVBoxLayout,
QWidget,
)

from brainglobe_utils.qtpy.collapsible_widget import (
CollapsibleWidget,
Expand All @@ -8,6 +15,15 @@

WIDGET_TITLE = "Title"

## TODO Add tests for different layouts
## TODO Add tests for removing widgets not in the container
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need issues for these, so we keep track of TODO's in one place?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say always have issues rather than TODOs

Copy link
Member Author

@IgorTatarnikov IgorTatarnikov Jan 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the following covers the first TODO.

@pytest.mark.parametrize(
"layout, collapsible",
[
(QVBoxLayout(), True),
(QHBoxLayout(), True),
(QFormLayout(), True),
(QVBoxLayout(), False),
(QHBoxLayout(), False),
(QFormLayout(), False),
],
)
def test_collapsible_widget_container_add_remove_widgets(
qtbot, collapsible_widget_container, generic_widget, layout, collapsible
):
qtbot.addWidget(collapsible_widget_container)
generic_widget.setLayout(layout)
generic_widget.layout().addWidget(QLabel("test"))
generic_widget.layout().addWidget(QPushButton("test"))
collapsible_widget_container.add_widget(
generic_widget, collapsible=collapsible, widget_title=WIDGET_TITLE
)
assert collapsible_widget_container.layout().count() == 1
# Convert collapsible to int (False -> 0, True -> 1)
# to check if collapsible_widgets is empty
assert len(collapsible_widget_container.collapsible_widgets) == int(
collapsible
)
collapsible_widget_container.remove_widget(generic_widget)
assert collapsible_widget_container.layout().count() == 0
assert len(collapsible_widget_container.collapsible_widgets) == 0

And I've just added a test to check an exception is thrown when trying to remove a widget that hasn't been added to the container.



@pytest.fixture(scope="class")
def generic_widget() -> QWidget:
widget = QWidget()
return widget


@pytest.fixture(scope="class")
def collapsible_widget() -> CollapsibleWidget:
Expand Down Expand Up @@ -86,61 +102,115 @@ def test_collapsible_widget_container(qtbot, collapsible_widget_container):
assert len(collapsible_widget_container.collapsible_widgets) == 0


@pytest.mark.parametrize(
"layout", [QVBoxLayout(), QHBoxLayout(), QFormLayout()]
)
def test_collapsible_widget_container_add_collapsible_widget(
qtbot, collapsible_widget_container, collapsible_widget
qtbot, collapsible_widget_container, generic_widget, layout
):
qtbot.addWidget(collapsible_widget_container)

collapsible_widget_container.add_widget(collapsible_widget)
generic_widget.setLayout(layout)
generic_widget.layout().addWidget(QLabel("test"))
generic_widget.layout().addWidget(QPushButton("test"))

collapsible_widget_container.add_widget(
generic_widget, widget_title=WIDGET_TITLE
IgorTatarnikov marked this conversation as resolved.
Show resolved Hide resolved
)

# Check that the widget was added and is a CollapsibleWidget
assert collapsible_widget_container.layout().count() == 1
assert isinstance(
collapsible_widget_container.layout().itemAt(0).widget(),
CollapsibleWidget,
)
# Check that the widget is collapsed and contains the generic widget
assert (
collapsible_widget_container.collapsible_widgets[0].content()
is generic_widget
)
assert (
collapsible_widget_container.collapsible_widgets[0]
== collapsible_widget
collapsible_widget_container.layout().itemAt(0).widget().text()
== WIDGET_TITLE
)
assert (
not collapsible_widget_container.layout()
.itemAt(0)
.widget()
.isExpanded()
)
assert len(collapsible_widget_container.collapsible_widgets) == 1


def test_collapsible_widget_container_add_other_widget(
qtbot, collapsible_widget_container
@pytest.mark.parametrize("widget_type", [QLabel, QPushButton])
def test_collapsible_widget_container_add_not_collapsible_widget(
qtbot, collapsible_widget_container, widget_type
):
qtbot.addWidget(collapsible_widget_container)

collapsible_widget_container.add_widget(QLabel("test"))
collapsible_widget_container.add_widget(
widget_type(WIDGET_TITLE), collapsible=False
)

assert collapsible_widget_container.layout().count() == 1
assert isinstance(
collapsible_widget_container.layout().itemAt(0).widget(), widget_type
)
assert len(collapsible_widget_container.collapsible_widgets) == 0


@pytest.mark.parametrize(
"layout, collapsible",
[
(QVBoxLayout(), True),
(QHBoxLayout(), True),
(QFormLayout(), True),
(QVBoxLayout(), False),
(QHBoxLayout(), False),
(QFormLayout(), False),
],
)
def test_collapsible_widget_container_add_remove_widgets(
qtbot, collapsible_widget, collapsible_widget_container
qtbot, collapsible_widget_container, generic_widget, layout, collapsible
):
qtbot.addWidget(collapsible_widget_container)

collapsible_widget_container.add_widget(collapsible_widget)
generic_widget.setLayout(layout)
generic_widget.layout().addWidget(QLabel("test"))
generic_widget.layout().addWidget(QPushButton("test"))

collapsible_widget_container.add_widget(
generic_widget, collapsible=collapsible, widget_title=WIDGET_TITLE
)

assert collapsible_widget_container.layout().count() == 1
assert len(collapsible_widget_container.collapsible_widgets) == 1
# Convert collapsible to int (False -> 0, True -> 1)
# to check if collapsible_widgets is empty
assert len(collapsible_widget_container.collapsible_widgets) == int(
collapsible
)

collapsible_widget_container.remove_widget(collapsible_widget)
collapsible_widget_container.remove_widget(generic_widget)

assert collapsible_widget_container.layout().count() == 0
assert len(collapsible_widget_container.collapsible_widgets) == 0


def test_collapsible_widget_container_add_remove_diff_widgets(
qtbot, collapsible_widget, collapsible_widget_container
qtbot, generic_widget, collapsible_widget_container
):
qtbot.addWidget(collapsible_widget_container)
other_widget = QLabel("test")

collapsible_widget_container.add_widget(collapsible_widget)
collapsible_widget_container.add_widget(other_widget)
collapsible_widget_container.add_widget(
generic_widget, widget_title=WIDGET_TITLE
)
collapsible_widget_container.add_widget(other_widget, collapsible=False)

assert collapsible_widget_container.layout().count() == 2
assert len(collapsible_widget_container.collapsible_widgets) == 1

collapsible_widget_container.remove_widget(collapsible_widget)
collapsible_widget_container.remove_widget(generic_widget)

assert collapsible_widget_container.layout().count() == 1
assert len(collapsible_widget_container.collapsible_widgets) == 0
Expand Down Expand Up @@ -180,28 +250,25 @@ def test_collapsible_widget_container_update_drawers(
# Add collapsible widgets and other widgets to the container alternating
# until the correct number of each type of widget has been added
for i in range(num_collapsible_widgets + num_other_widgets):
if i % 2 == 0:
if len(collapsible_widgets) == num_collapsible_widgets:
non_collapsible_widgets.append(QLabel("test"))
collapsible_widget_container.add_widget(
non_collapsible_widgets[-1]
)
else:
collapsible_widgets.append(CollapsibleWidget(WIDGET_TITLE))
collapsible_widget_container.add_widget(
collapsible_widgets[-1]
)
if i % 2 == 0 and len(non_collapsible_widgets) < num_other_widgets:
collapsible_widget_container.add_widget(
QLabel("test"), collapsible=False
)
non_collapsible_widgets.append(
collapsible_widget_container.layout().itemAt(i).widget()
)
elif len(collapsible_widgets) < num_collapsible_widgets:
collapsible_widget_container.add_widget(QLabel("test"))
collapsible_widgets.append(
collapsible_widget_container.layout().itemAt(i).widget()
)
else:
if len(non_collapsible_widgets) == num_other_widgets:
collapsible_widgets.append(CollapsibleWidget(WIDGET_TITLE))
collapsible_widget_container.add_widget(
collapsible_widgets[-1]
)
else:
non_collapsible_widgets.append(QLabel("test"))
collapsible_widget_container.add_widget(
non_collapsible_widgets[-1]
)
collapsible_widget_container.add_widget(
QLabel("test"), collapsible=False
)
non_collapsible_widgets.append(
collapsible_widget_container.layout().itemAt(i).widget()
)

for _ in range(num_collapsible_widgets):
collapsible_widgets[index_expanded]._toggle_btn.click()
Expand Down