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

how to use annotation with create_widget #68

Merged
merged 14 commits into from
Apr 15, 2024
50 changes: 46 additions & 4 deletions docs/howtos/extending/magicgui.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,14 @@ The following napari types may be used as *parameter* type annotations in
{attr}`napari.types.ImageData` or {attr}`napari.types.LabelsData`
- {class}`napari.Viewer`

```{note}
If you want to get the dropdown list widgets resulting from the layer
annotations (e.g., {class}`~napari.layers.Layer`,
{class}`~napari.layers.Image`, {attr}`napari.types.ImageData`)
without using the `@magicgui` decorator,
see the [`QWidget` example](#qtwidgets-qwidget) below.
gatoniel marked this conversation as resolved.
Show resolved Hide resolved
```

The consequence of each type annotation is described below:

#### Annotating as a `Layer` subclass
Expand Down Expand Up @@ -754,29 +762,63 @@ simply provide the class definition and add to the plugin manifest.
### `QtWidgets.QWidget`

For the most control over your widget, subclass
[`QtWidgets.QWidget`](https://doc.qt.io/qt-5/qwidget.html):
[`QtWidgets.QWidget`](https://doc.qt.io/qt-5/qwidget.html).
In the following example, we create a button and a dropdown list.
The available choices for the dropdown list are the
current layers in the viewer. For this, we use
{func}`create_widget <magicgui.widgets.create_widget>` with the annotation
{attr}`napari.types.ImageData`.
In opposition to
using `@magicgui` as [shown above](#parameter-annotations), we now need to
DragaDoncila marked this conversation as resolved.
Show resolved Hide resolved
manually connect the `reset_choices` of the list with the
DragaDoncila marked this conversation as resolved.
Show resolved Hide resolved
`viewer.layers.events` so that the available choices are synchronized
with the current layers of the viewer:

```python
import numpy
import napari
from magicgui.widgets import create_widget
from qtpy.QtWidgets import QHBoxLayout, QPushButton, QWidget

class ExampleQWidget(QWidget):
from napari.types import ImageData


class ExampleLayerListWidget(QWidget):
def __init__(self, viewer: "napari.viewer.Viewer"):
super().__init__()
self.viewer = viewer

# Create a button
btn = QPushButton("Click me!")
# Connect the click event to a function
btn.clicked.connect(self._on_click)

# create new widget with create_widget and type annotation
self.layer_select = create_widget(annotation=ImageData)
# The widget has to be connected to viewer.layers.events
gatoniel marked this conversation as resolved.
Show resolved Hide resolved
layers_events = self.viewer.layers.events
layers_events.inserted.connect(self.layer_select.reset_choices)
layers_events.removed.connect(self.layer_select.reset_choices)
layers_events.reordered.connect(self.layer_select.reset_choices)
Comment on lines +799 to +802
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
layers_events = self.viewer.layers.events
layers_events.inserted.connect(self.layer_select.reset_choices)
layers_events.removed.connect(self.layer_select.reset_choices)
layers_events.reordered.connect(self.layer_select.reset_choices)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe, we can explain that part of napari magic, too, and show both possibilities with a marker that one of them is potentially deprecated?

Copy link
Contributor

Choose a reason for hiding this comment

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

We are working on nested Layers (Layer Groups). It may change a list of events. So then people will need to update their plugins if they do not depend on napari calling reset_choices.

Copy link
Contributor

@DragaDoncila DragaDoncila Apr 11, 2024

Choose a reason for hiding this comment

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

So then people will need to update their plugins if they do not depend on napari calling reset_choices.

@Czaki I'm not happy to enshrine this behaviour in our docs right now (see original comment here). It's very brittle and in my opinion, quite an overreach that could lead to surprising effects on widgets totally unrelated to the layerlist. I would strongly prefer to keep the example as it currently is, because it involves no magic and no reliance on napari internals. If we one day change the layerlist events then yes, people may have to update their plugins, but I have no issue with that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could we explain the shortcut via reset_choices in the docs and also mention that there are plans that both viewer.layers.events and reset_choices might change in future, so that developers should check the corresponding source code once there plugins stop working as expected.

reset_choices https://github.com/napari/napari/blob/50b314f7ad2e7cd4ef21b5705b3c6b363c0857ed/napari/_qt/qt_main_window.py#L1093 shows all the relevant layers.events that would need to be connected individually.

viewer.layers.events currently is a instance subclassing https://github.com/napari/napari/blob/50b314f7ad2e7cd4ef21b5705b3c6b363c0857ed/napari/utils/events/containers/_selectable_list.py#L12

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the offer @gatonielif we decide to take you up on it or parts of it, I think expanding on the reset_choices magic should come in a separate PR. For now I think we are still internally deciding whether we want it in the docs, as you can see 😅, so I think this should go in as-is.

Regarding the future of the event connections: in my opinion, if we start worrying in the docs about all the things we are planning to deprecate, we will never document anything. 😂 I think in general the docs should be written with regard to the current public API, unless we really want to discourage use of a specific API. Which is not the case here.


self.setLayout(QHBoxLayout())
# add it to the layout
self.layout().addWidget(self.layer_select.native)
self.layout().addWidget(btn)

def _on_click(self):
print("napari has", len(self.viewer.layers), "layers")
print(
"Selected layer has shape: ",
self.layer_select.value.shape,
)

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
def reset_choices(self):
self.layer_select.reset_choices()


# Create a `viewer`
viewer = napari.Viewer()
viewer.add_image(numpy.random.rand(20, 20), name="Layer 1")
viewer.add_image(numpy.random.rand(40, 40), name="Layer 2")
# Instantiate your widget
my_widg = ExampleQWidget()
my_widg = ExampleLayerListWidget(viewer)
# Add widget to `viewer`
viewer.window.add_dock_widget(my_widg)
```
Expand Down
Loading