magicgui icon indicating copy to clipboard operation
magicgui copied to clipboard

QTreeWidget from list of path like strings

Open sofroniewn opened this issue 5 years ago • 16 comments

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 avatar Apr 26 '20 04:04 sofroniewn

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

sofroniewn avatar Apr 26 '20 19:04 sofroniewn

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?

tlambert03 avatar Apr 26 '20 19:04 tlambert03

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

sofroniewn avatar Apr 26 '20 19:04 sofroniewn

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

tlambert03 avatar Apr 26 '20 19:04 tlambert03

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

# 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)
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])
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 avatar Apr 26 '20 20:04 sofroniewn

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

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)

sofroniewn avatar Apr 27 '20 06:04 sofroniewn

😍🤯

tlambert03 avatar Apr 27 '20 09:04 tlambert03

@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

sofroniewn avatar May 03 '21 14:05 sofroniewn

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

tlambert03 avatar May 03 '21 14:05 tlambert03

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.

sofroniewn avatar May 03 '21 14:05 sofroniewn

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)

tlambert03 avatar May 03 '21 14:05 tlambert03

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?

jni avatar May 03 '21 22:05 jni

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

sofroniewn avatar May 03 '21 23:05 sofroniewn

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:

evamaxfield avatar May 04 '21 00:05 evamaxfield

@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.

jni avatar May 04 '21 00:05 jni

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

evamaxfield avatar May 04 '21 00:05 evamaxfield