pyapp-kit / magicgui

build GUIs from type annotations
https://pyapp-kit.github.io/magicgui/
MIT License
374 stars 49 forks source link

Can not select tracks layer by create_widget #333

Closed pmcesky closed 2 years ago

pmcesky commented 2 years ago

Describe the bug I was trying to load the tracks layer to a widget, using the code like below:

from napari.layers import Image, Labels, Tracks from magicgui.widgets import create_widget from qtpy.QtWidgets import QWidget, QPushButton, QLabel, QHBoxLayout, QVBoxLayout, QLineEdit

self.setLayout(QVBoxLayout()) self.tracks_select = create_widget(annotation=Tracks, label="tracks_layer") tracks_layer_selection_container = QWidget() tracks_layer_selection_container.setLayout(QHBoxLayout()) tracks_layer_selection_container.layout().addWidget(QLabel("Tracks layer")) tracks_layer_selection_container.layout().addWidget(self.tracks_select.native) self.layout().addWidget(tracks_layer_selection_container)

The code works for image and labels layer, as in https://github.com/BiAPoL/napari-clusters-plotter/blob/main/napari_clusters_plotter/_measure.py but fails for tracks layer. The running result is like this: image If use "self.tracks_select = create_widget(label="tracks_layer")" instead, the widget would be like: image

As you can see, the image and labels layer could be selected, but the tracks layer could not be selected in both cases. What's wrong? Is tracks layer supported by create_widget? What is the right way to do it?

Environment (please complete the following information):

tlambert03 commented 2 years ago

This isn't a difference between tracks or layers, it has more to do with using magicgui widgets inside of QWidgets and expecting all the magic to work the same :)

The way magicgui can update those comboboxes to show the current layers is by keeping an eye on any reparenting events, so that if you, for example, create a widget not in a viewer, and then add it to a viewer with add_dock_widget, it knows to go look for layers.

If you want to combine Qt and magicgui like this, you'll need to recreate what magicgui was doing for you. Take this example, and note the commented out parts. Without it, neither tracks nor points work. With it, they both work.

import numpy as np
from magicgui.widgets import create_widget
from qtpy.QtWidgets import QFormLayout, QWidget

import napari
import napari.layers

class W(QWidget):
    def __init__(self):
        super().__init__()
        self.setLayout(QFormLayout())
        self._pnts = create_widget(annotation=napari.layers.Points)
        self._trx = create_widget(annotation=napari.layers.Tracks)
        self.layout().addRow('points', self._pnts.native)
        self.layout().addRow('tracks', self._trx.native)

v = napari.Viewer()
v.add_points(None)
v.add_tracks(np.arange(20).reshape(5, 4))

wdg = W()
v.window.add_dock_widget(wdg)

# # Notify that the parent has changed.
# # NOTE: without these two lines, *neither* layer type works.
# wdg._trx._emit_parent()
# wdg._pnts._emit_parent()

napari.run()

The trick is knowing when the parent changes. You can see some advice for doing that in a response I gave here: https://github.com/napari/napari/issues/3659

pmcesky commented 2 years ago

Hi @tlambert03, thank you very much! This is my first time encountering and learning about this, your explanation and code example are really helpful. Thanks a lot!

One question, like your code above, the last two lines let the QWidget instance know they have been docked to the napari window. This works when you explicitly add the QWidget to the napari window by napari.viewer.window.add_dock_widget(). In another case, when building a plugin by:

@napari_hook_implementation 
def napari_experimental_provide_dock_widget(): 
    return [W]

What should we do to let it happen like in your code above?

tlambert03 commented 2 years ago

As mentioned above You can see some advice for doing that in a response I gave here: napari/napari#3659

Let me know, after reading that issue, if it's not clear

pmcesky commented 2 years ago

Hi Talley, I checked the advice you give on napari/napari#3659. Though I'm not clear about the code and method you use, but it seems it uses event filter to catch the reparent event and let the QWidget know it happens. And I borrowed the codes about the eventfilter part there and now it works! image I'm surprised when the QWidget knows the event, it starts to look for the layers. That's absolutely magic.

Thanks very much!

One question, in your code example above:

# # Notify that the parent has changed.
# # NOTE: without these two lines, *neither* layer type works.
wdg._trx._emit_parent()
wdg._pnts._emit_parent()

What is the ._emit_parent(), where is it from? I did not find it in magicgui and pyqt5.

tlambert03 commented 2 years ago

What is the ._emit_parent(), where is it from? I did not find it in magicgui and pyqt5.

here ya go: https://github.com/napari/magicgui/blob/4eafd75bb8dc99af28e96ddc589fa548651d2e6b/magicgui/widgets/_bases/widget.py#L349-L350

tlambert03 commented 2 years ago

(actually, you might be better off using the non-private method: self.parent_changed.emit(self.parent))

pmcesky commented 2 years ago

Thanks so much!

imagesc-bot commented 2 years ago

This issue has been mentioned on Image.sc Forum. There might be relevant details there:

https://forum.image.sc/t/composing-workflows-in-napari/61222/2

imagesc-bot commented 2 years ago

This issue has been mentioned on Image.sc Forum. There might be relevant details there:

https://forum.image.sc/t/access-viewer-object-within-magicgui-widget-wrapper/61366/6