pyapp-kit / magicgui

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

QTreeWidget from list of path like strings #9

Open sofroniewn opened 4 years ago

sofroniewn commented 4 years ago

I was about to code something up as a one off (and probably still wise for me to start there) but then I thought it might be ultimately a nice re-usable piece of Qt code we'd want to provide to plugin devs. I have a list of paths (they happen to come from inside a zip file from a url, but really they could also come from anywhere) - a small snapshot looks like

['RxRx19a/',
 'RxRx19a/LICENSE',
 'RxRx19a/images/',
 'RxRx19a/images/VERO-1/',
 'RxRx19a/images/VERO-1/Plate1/',
 'RxRx19a/images/VERO-1/Plate1/AC41_s4_w3.png',
 'RxRx19a/images/VERO-1/Plate1/G38_s3_w1.png',....]

and I'd like to use a QTreeWidget to browse them as described in posts like this one on stackoverflow.

When i select a path with a png I'd then like to trigger some calls that will lead to a new image being displayed in the viewer - i.e. turning napari into a little file browser for this remote zip. It might also be nice to have similar browser capability for local files too.

Curious what you think of this @tlambert03 or if you have any tips before I got going? I'd say this is more of a hobby use of napari rather than a top development priority, but always fun to be trying to push it in new ways :-)

sofroniewn commented 4 years ago

Here is my code snippet which kind of works to create the tree widget

treeWidget = QTreeWidget()

# Determine at set number of columns
n_columns = np.max([f.count('/') for f in filenames]) + 1
treeWidget.setColumnCount(n_columns)

# Create tree
tree_root = (None, {})
for f in filenames:
    parts = f.split('/')
    node = tree_root
    for i, p in enumerate(parts):
        if p != '':
            if p not in node[1]:
                node[1][p] = (QTreeWidgetItem(node[0]), {})
                node[1][p][0].setText(i, p)
            node = node[1][p]

# Add top level items
for node in tree_root[1].values():
    treeWidget.addTopLevelItem(node[0])

Doesn't look too bad ... but I imagine there is a better/ more standard way to do this

file_browser

tlambert03 commented 4 years ago

awesome! I definitely think this is useful. need some time to look a little closer, but already looks cool :)

i wonder whether QFileSystemModel could be useful here? Or does it not work for your list of strings if you don't necessarily want every file?

sofroniewn commented 4 years ago

i wonder whether QFileSystemModel could be useful here? Or does it not work for your list of strings if you don't necessarily want every file?

ooo that sounds promising!! will check it out

tlambert03 commented 4 years ago

another potentially useful section here: https://doc.qt.io/qt-5/model-view-programming.html#using-views-with-an-existing-model

sofroniewn commented 4 years ago

Thanks! Here is my "browser" for this recent 450GB data release from Recursion - It's a little slow because the remote zip stuff is slow, see my image.sc post, but it does work!!!

Screen Shot 2020-04-26 at 1 41 01 PM

Here are a couple key pieces

```python # decorate your function with the ``@magicgui`` decorator @magicgui(call_button="fetch") def fetch_image(): viewer.status = 'Button Clicked!' print(viewer.status) item = treeWidget.currentItem() if item.text(6).endswith('.png') or item.text(5).endswith('_w'): file_split = item.full_path.split('/') file = '' for f in file_split[:-3]: file += f + '/' for f in file_split[-3:]: file += f viewer.status = 'Fetching: ' + file print(viewer.status) images = get_image_stack(path, file) viewer.status = 'Recieved!' print(viewer.status) viewer.layers.select_all() viewer.layers.remove_selected() viewer.add_image(images, channel_axis=0) ``` ```python treeWidget = QTreeWidget() # Determine at set number of columns n_columns = np.max([f.count('/') for f in adj_filenames]) + 1 treeWidget.setColumnCount(n_columns) # Create tree tree_root = (None, {}) for f in adj_filenames: parts = f.split('/') node = tree_root for i, p in enumerate(parts): if p != '': if p not in node[1]: node[1][p] = (QTreeWidgetItem(node[0]), {}) node[1][p][0].setText(i, p) node[1][p][0].full_path = f node = node[1][p] # Add top level items for node in tree_root[1].values(): treeWidget.addTopLevelItem(node[0]) ``` ```python def get_image_stack(path, file): with RemoteZip(path) as zip: image_data = [] for c in list(range(1, 6)): adj_file = f'{file[:-5]}{c}.png' image_data.append(zip.read(adj_file)) return np.array([imread(imd) for imd in image_data]) ```

Once we have it nicely figured out I can write up a tutorial - it's pretty fun to use (though would be better if faster and call was not blocking the UI)

sofroniewn commented 4 years ago

Following up I made a new cool demo doing a similar thing but with my local file browser.

Clears and loads new image data via viewer.open_path / viewer.open when selected. Makes for fun browsing!!! Should probably remove the open/ cancel buttons

local_file_browser

```python import os from qtpy.QtWidgets import QFileDialog from qtpy.QtCore import Qt file_dialog = QFileDialog() file_dialog.setWindowFlags(Qt.Widget) file_dialog.setModal(False) file_dialog.setOption(QFileDialog.DontUseNativeDialog) def open_path(path): if os.path.isfile(path): viewer.layers.select_all() viewer.layers.remove_selected() viewer.open_path(path) file_dialog.currentChanged.connect(open_path) ```
tlambert03 commented 4 years ago

😍🤯

sofroniewn commented 3 years ago

@jni just asked me about turning this into a plugin (or contribution to magicgui?). It's been a while since the last comments on here, curious @tlambert03 if things have advanced in this area/ if you have any new recommendations here

tlambert03 commented 3 years ago

I think I might need a refresher. It kinda seems like the best example here was just using the Qt file browser connected to an event that loads and shows the image, yeah? If the main use case here was file-tree related, I think this is better as a napari example with a direct Qt file widget. but if I'm missing a more general type-to-widget opportunity, let me know

sofroniewn commented 3 years ago

I think napari example/ napari plugin for file browsing is all @jni wanted, but maybe we could build it using pieces from the magicgui file dialog? https://github.com/napari/magicgui/blob/master/examples/file_dialog.py

Direct Qt file widget approach might be simpler though. Maybe we'll poke around later and then update this thread.

tlambert03 commented 3 years ago

yeah, the magicgui file dialog is just a line edit with a button that opens up a "pick a file" dialog. This is a full-blown QFileDialog (which i'm struggling to fit into my mental model of magicgui at the moment)

jni commented 3 years ago

Yeah it would be awesome if we could specify a QFileDialog widget for files, just like you can specify RadioButton instead of combo boxes for enums. Am I missing some difficulty @tlambert03 ? I guess not all front ends have such a widget but there's not full parity on everything is there?

Then you could make an auto-run file -> layers plug-in with this and things would just work, no?

sofroniewn commented 3 years ago

Code from above https://github.com/sofroniewn/image-demos/blob/master/examples/rxrx19_browser.py

evamaxfield commented 3 years ago

Just saw this thread from napari and would ask that instead of the "Look in" directory dropdown to have just a text box that the user can pass a string. Reason being I was also in the process of working on a general "ObjectTreeModel" for napari image loading backed by fsspec to allow object browsing of remote as well:

Very minimal but working tree for local or remote: https://github.com/JacksonMaxfield/napari-aicsimageio/blob/feature/object-tree/napari_aicsimageio/widgets/object_tree.py

Doesn't have all the fancy bells and whistles though so maybe not :shrug:

jni commented 3 years ago

@JacksonMaxfield this is a different use case I think. Both a file browser and a world browser can live side by side in the napari plugin ecosystem. 😊 In the case of magicgui, I think we mostly want to support standard or quasi-standard widgets. Note that above the whole thing is a QFileDialog, so we can't really go in and move things around.

evamaxfield commented 3 years ago

Yep makes sense to me. Love the progress either way :slightly_smiling_face: