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

Conversation

gatoniel
Copy link
Contributor

@gatoniel gatoniel commented Dec 12, 2022

Description

Adds to the documentation how to use annotations with magicgui.widgets.create_widget in a plugin subclass of QtWidgets.QWidget.

Type of change

  • Fixes or improves existing content

References

closes napari/napari#5340

uses information from https://github.com/PartSeg/PartSeg_bioimageio/blob/2afcbd558e52d6af6a13f0b844e80d280cd6ba9d/src/PartSeg_bioimageio/_widget.py#L421

Final checklist:

  • My PR is the minimum possible work for the desired functionality
  • I have commented my code, particularly in hard-to-understand areas
  • I have added alt text to new images included in this PR

@github-actions github-actions bot added the documentation Improvements or additions to documentation label Dec 12, 2022
Copy link
Member

@melissawm melissawm left a comment

Choose a reason for hiding this comment

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

Hi @gatoniel - welcome and thanks for your PR! I left one minor comment (to sctually create a link to the magicgui-and-type-annotations section) but will leave the actual example to be reviewed by one of the core maintainers. Cheers!

docs/guides/magicgui.md Outdated Show resolved Hide resolved
@psobolewskiPhD psobolewskiPhD requested a review from Czaki December 12, 2022 21:40
@Czaki
Copy link
Contributor

Czaki commented Dec 12, 2022

This is a much more generous mechanism. magicgui.widgets.create_widget allows you to get magicgui widget for any type registered in magicgui.

So getting layer selector should be mentioned as an example of usage, not primary usage.

Maybe it could also be nice to add an example using magicgui.widgets.Container for complex examples that do not require direct Qt usage.

@melissawm
Copy link
Member

Hi folks - I'd love to see this one go in. I will link to it in zulip to maybe get more eyes on it 😄

Co-authored-by: Melissa Weber Mendonça <melissawm@gmail.com>
@melissawm
Copy link
Member

Hi @gatoniel - do you need help with updating your branch, or moving this forward? Thanks!

@psobolewskiPhD
Copy link
Member

@lucyleeow @DragaDoncila
This fell through the cracks -- how does this fit in with the current state of extending napari docs?

@lucyleeow
Copy link
Collaborator

This looks useful but I think it is missing some context. In what situations would you not be able to use the @magicgui decorator?

I think this would be a better example than what is used here: https://napari.org/dev/howtos/extending/magicgui.html#qtwidgets-qwidget and we can over-write it. Could we add some more detail about how create_widget gives you more control over the selection widget created (i.e. we can specify the widget type (e.g., slider, RangeEdit etc)). I would also make it more general and not specifically about layer selection, though we can use that as an example.

@gatoniel are you still interested in continuing this PR?

@melissawm
Copy link
Member

Gentle ping, @gatoniel ! Let us know if you need help 😄

@lucyleeow
Copy link
Collaborator

I think this would be a better example than what is used here: https://napari.org/dev/howtos/extending/magicgui.html#qtwidgets-qwidget and we can over-write it.

Reading again, I think it would be a good addition to the existing example. We could explain why/how create_widget makes things easier?

@gatoniel
Copy link
Contributor Author

Hi, sorry for the late reply. I was / am busy with other stuff. Maybe I will have time this weekend.

@gatoniel
Copy link
Contributor Author

gatoniel commented Apr 2, 2024

This looks useful but I think it is missing some context. In what situations would you not be able to use the @magicgui decorator?

I do not know whether this question requires an answer as long as the information is available at https://napari.org/dev/howtos/extending/magicgui.html#qtwidgets-qwidget. For now, I have added the example as a special case there as people using QtWidgets.QWidget might be interested in still using the type annotated widgets with automatic synchronization.

Could we add some more detail about how create_widget gives you more control over the selection widget created (i.e. we can specify the widget type (e.g., slider, RangeEdit etc)). I would also make it more general and not specifically about layer selection, though we can use that as an example.

Unfortunately, I do not know enough about the other possibilities of create_widget.

Maybe it could also be nice to add an example using magicgui.widgets.Container for complex examples that do not require direct Qt usage.

However, in the example under https://napari.org/dev/howtos/extending/magicgui.html#magicgui-widgets-container create_widget is already used. Hence, I would actualy propose to close this PR without merging as now it becomes clear how to use create_widget to get for example layer selections without use of @magicgui.

@lucyleeow
Copy link
Collaborator

I think there is very useful and important information here (e.g., regarding refreshing the widget) that is not documented anywhere else. Fitting it in with existing documentation is more tricky and probably more work than originally intended. How about I try and add this example, along with some more info, and you can review if you are interested @gatoniel ?

@gatoniel
Copy link
Contributor Author

gatoniel commented Apr 3, 2024

Sounds good. Otherwise, I could also add some more description about the refreshing into the surrounding text.
Additionally, I guess it would be good to have a link from https://napari.org/dev/howtos/extending/magicgui.html#parameter-annotations to the availability of create_widget.

@gatoniel
Copy link
Contributor Author

gatoniel commented Apr 3, 2024

I have now added some more description about the refreshing and a note box in the #parameter-annotations section to direct the reader directly to this example if she is not concerned about @magicgui.

@lucyleeow
Copy link
Collaborator

Ah that is interesting. @DragaDoncila and I were talking about whether the reset_choices method in your example somehow overrides the parent CategoricalWidgets reset_choices method or if it is just called manually. If it is the latter, then technically it doesn't matter what this method is called right? Its just something we manually call in ShowEvent ?

@lucyleeow
Copy link
Collaborator

Now that I understand better, I think we could just have one QWidget example. The first example widget doesn't really do anything useful, but it does show how to connect a function to an event. I would maybe amend your example to add a function to execute when a layer is selected.

I think some more details about what is happening in the 2 methods would be useful too. And highlight that we need to update ourselves as magicgui is not doing this for us.

There are also several improvements I would like to make to that page, but this can be separate to this addition.

Overall, maybe this is too much extra work for you?


You might want to add a layer selection as [shown above](#parameter-annotations) into your highly customizable `QtWidgets.QWidget` that is always synchronized with the available {attr}`napari.types.ImageData` layers in your viewer. For this you can use the {func}`create_widget <magicgui.widgets.create_widget>` function as described in the following example.

To synchronize the information between the napari viewer (i.e. the available layers) the function `reset_choices` of the dropdown widget needs to be manually called whenever the main widget is shown. In the following example, this is done by defining two class methods `showEvent` and `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.

I would have thought it needs to be called when the input selector widget is shown? If the main widget is open. then layers are changed, you'd expect the input selector widget to update?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tested it. Napari somehow internally calls reset_choices of the main widget. So showEvent and reset_choices need to be named exactly like that. I updated it in the PR

@gatoniel
Copy link
Contributor Author

gatoniel commented Apr 3, 2024

Ah that is interesting. @DragaDoncila and I were talking about whether the reset_choices method in your example somehow overrides the parent CategoricalWidgets #68 (comment) method or if it is just called manually. If it is the latter, then technically it doesn't matter what this method is called right? Its just something we manually call in ShowEvent?

Yes, technically it doesn't matter what this method is called. One could also call the self.layer_select.reset_choices(event) in the showEvent method.

Overall, maybe this is too much extra work for you?

Depends. If I get rather precise orders, I can try :D

@gatoniel
Copy link
Contributor Author

gatoniel commented Apr 3, 2024

Yes, technically it doesn't matter what this method is called. One could also call the self.layer_select.reset_choices(event) in the showEvent method.

This was wrong. I tested the available options. It works only smoothly, when the function is exactly called reset_choices. It seems that napari calls this function of the docked widgets whenever something changed within napari. But I do not know where in the source code this call comes from. I've updated the PR accordingly. showEvent should just simply also call reset_choices as the choices should be reset whenever the widget is being shown.

@DragaDoncila
Copy link
Contributor

It works only smoothly, when the function is exactly called reset_choices

Yeah I found this also but I couldn't for the life of me figure out where the connection is made.

I can see that for magicgui widgets we have a signal native_parent_changed, that calls the CategoricalWidget's reset_choices method - so I guess this would call layer_select.reset_choices when the containing widget, ExampleLayerListWidget changes.

What I don't understand is how having a method named reset_choices on a parent widget of a magicgui widget (in this case ExampleLayerListWidget) ensures that this method gets connected to layerlist events. There's no inheritance going on since ExampleLayerListWidget just inherits from QWidget, so magicgui must be reaching up from the layer_select widget to the parent widget somehow.

Pinging @tlambert03 who can hopefully unravel some of the magic in magicgui.

@tlambert03
Copy link
Contributor

tlambert03 commented Apr 4, 2024

What I don't understand is how having a method named reset_choices on a parent widget of a magicgui widget (in this case ExampleLayerListWidget) ensures that this method gets connected to layerlist events.

a grep of napari for reset_choices suggests its probably this:

https://github.com/napari/napari/blob/3cb47b5fc393427a09f3b98d641b3086ff93405e/napari/_qt/qt_main_window.py#L1093-L1100

(i.e. it's not magicgui magic, it's napari magic... i'm not sure if you want to "codify" that as a first class API here, but that's why it's working 😄 )

@DragaDoncila
Copy link
Contributor

@gatoniel @lucyleeow I would say to move this PR forward while we think about how/if we want to maintain this reset_choices behavior, we should update the example to explicitly connect the layer_select.reset_choices method to viewer.layers events. It's a little more boilerplate, but a lot less magic, and I would prefer to have that documented rather than this brittle kinda weird thing we're doing in napari specifically for CategoricalWidgets.

@lucyleeow
Copy link
Collaborator

@gatoniel thanks for uncovering all of this, sorry this PR has gotten a bit derailed.

@gatoniel
Copy link
Contributor Author

gatoniel commented Apr 4, 2024

I changed the example. Now the reset_choices is directly connected to the three events in viewer.layers.events. I agree, this makes the example more straightforward and less magic :D Thanks @tlambert03 and @DragaDoncila for finding the mechanism...

@lucyleeow
Copy link
Collaborator

Thanks @gatoniel.

I still think that we only need one QWidget example. I would connect your widget to a function that does something vaguely useful with an input layer. Also mention that we need to connect to the layer events because magicgui is not doing this automatically for us, as with the above examples.

Could you also split your lines so max line length is 79 characters (this is what we've set for napari (see: here) but the docs repo has no linting). Thank you

Comment on lines 181 to 183
```{note}
If you're interested in getting synchronized information from the napari viewer into widgets without `@magicgui`, see the [`create_widget` example](#magicgui-widgets-create_widget) below.
```
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think this is necessary. I think we automatically update for all magicgui registered napari types; layer, layer subclasses and layer type data (e.g., napari.types.ImageData).
I am not sure if there would be any cases where you would want an input that is not in the above list.

If there is, we could mention that for non registered types you need to manually synchronize, see QWidget example for more...

cc @DragaDoncila

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have made the note more precise. I added it, because I was aware that one can easily create these synchronized dropdown lists of the available layers with @magicgui. But then, they were part of a bigger widget, and I was not able to figure out to create only these widgets which is possible with create_widget.

@gatoniel
Copy link
Contributor Author

gatoniel commented Apr 5, 2024

Could you also split your lines so max line length is 79 characters (this is what we've set for napari (see: here) but the docs repo has no linting). Thank you

Done. Is it ok now?

I still think that we only need one QWidget example. I would connect your widget to a function that does something vaguely useful with an input layer. Also mention that we need to connect to the layer events because magicgui is not doing this automatically for us, as with the above examples.

It is only one widget now. Clicking on the button prints the shape of the currently selected Image Layer. I mentioned that it needs manual connection to the layers events.

@psobolewskiPhD psobolewskiPhD added this to the 0.5.0 milestone Apr 5, 2024
Copy link
Collaborator

@lucyleeow lucyleeow left a comment

Choose a reason for hiding this comment

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

Looks good, just some nits, thanks!

docs/howtos/extending/magicgui.md Outdated Show resolved Hide resolved
docs/howtos/extending/magicgui.md Outdated Show resolved Hide resolved
docs/howtos/extending/magicgui.md Outdated Show resolved Hide resolved
gatoniel and others added 2 commits April 9, 2024 15:18
Co-authored-by: Lucy Liu <jliu176@gmail.com>
Co-authored-by: Lucy Liu <jliu176@gmail.com>
Copy link
Contributor

@DragaDoncila DragaDoncila left a comment

Choose a reason for hiding this comment

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

Thanks for your work and patience on this @gatoniel! I suggested a more explicit comment about why we have to manually connect to reset_choices now, but am approving otherwise. Let's get this in!

docs/howtos/extending/magicgui.md Outdated Show resolved Hide resolved
gatoniel and others added 3 commits April 10, 2024 10:35
Co-authored-by: Draga Doncila Pop <17995243+DragaDoncila@users.noreply.github.com>
Co-authored-by: Draga Doncila Pop <17995243+DragaDoncila@users.noreply.github.com>
Copy link
Contributor

@Czaki Czaki left a comment

Choose a reason for hiding this comment

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

If a widget has reset_choices method, then it will be called on all required events.

Fell free to reformat my text.

Comment on lines +773 to +775
manually connect the `reset_choices` of the created widget with the
`viewer.layers.events` so that the available choices are synchronized
with the current layers of the viewer:
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
manually connect the `reset_choices` of the created widget with the
`viewer.layers.events` so that the available choices are synchronized
with the current layers of the viewer:
define `reset_choices` method that will need to call `reset_choices`
of magicgui widgets.

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

Comment on lines +799 to +802
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
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.

@jni
Copy link
Member

jni commented Apr 11, 2024

Hi folks,

I'm with Draga and Lucy here — I find the napari magic um, concerning, 😂, insomuch as it introduces a way-too-tight coupling between napari and magicgui, so I would rather we use the explicit connections in the docs. Yes @Czaki, the events might change, but they are part of the public API so in that case we would use a deprecation warningemitter cycle and so on and we would be able to update. (Of course, keeping the docs in sync is still a challenge, but that is the case either way — we might also remove the totally-private reset_choices magic and then the example would stop working — with no warning.)

@gatoniel thanks for offering to document both options — that is appreciated, but even if we decide to go that route, in my opinion the example as it is now is good and should go in, and we can expand on the magic in a subsequent PR.

I'll mark this as ready-to-merge and if @Czaki agrees with my rationale above, he can merge. 🙏 🚀

Thanks all for the discussion! I had actually not seen this page @lucyleeow, it is friggin GOLD. 🥇

@Czaki
Copy link
Contributor

Czaki commented Apr 11, 2024

I still do not understand why you call this magic.
This is only the interface.

@jni
Copy link
Member

jni commented Apr 11, 2024

it is magical to connect events behind the scenes based on a method name. (Not the signature, nor what the method does.) This is especially true if the interface is not annotated anywhere, specifically in the function that is adding the widget.

@jni
Copy link
Member

jni commented Apr 11, 2024

At any rate @Czaki I don't think you can argue that layer events are public and therefore can be considered at least as stable as this internal connection, whether or not you call it magic? And that the docs are improved by this PR. I'd be happy to move this discussion to a new PR that discusses both options and their tradeoffs, as proposed by @gatoniel.

@psobolewskiPhD psobolewskiPhD merged commit 79485ff into napari:main Apr 15, 2024
7 checks passed
DragaDoncila pushed a commit that referenced this pull request Apr 16, 2024
# References and relevant issues
Came about after #68

Details `magicgui` type registration, in particular the `reset_choices`
connection to layers we do for all widgets that have that method.
psobolewskiPhD pushed a commit that referenced this pull request Apr 21, 2024
# References and relevant issues
Follow on from #68

# Description

* adds more info on when you would want to choose each type of widget,
clarifying that widgets with access to the native QWidget are equally
extensible.
* clarify you can use `create_widget` in any widget class
* Move the "Avoid imports with forward references" info to plugin best
practices, where we already have a section about avoiding unnecessary
imports
* Move '`magicgui` function widgets as plugin contributions' to be next
to the intro to magicgui function widgets
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ComboBox automatically filled with viewer.layers for QWidget class plugins
8 participants