66
77Replace code below according to your needs.
88"""
9+ import collections
10+ from datetime import date , datetime
11+ import functools
12+ import json
913
1014from napari .utils .notifications import show_info
15+ from napari .resources ._icons import ICONS
1116from qtpy .QtCore import Qt , Signal
17+ from qtpy .QtGui import QIcon , QPixmap
1218from qtpy .QtWidgets import (
1319 QAbstractItemView ,
1420 QComboBox ,
1723 QLineEdit ,
1824 QPushButton ,
1925 QSplitter ,
26+ QStyle ,
2027 QTableWidget ,
2128 QTableWidgetItem ,
29+ QTextEdit ,
2230 QVBoxLayout ,
2331 QWidget ,
2432)
2533from tiled .client import from_uri
34+ from tiled .client .array import DaskArrayClient
35+ from tiled .client .node import Node
2636from tiled .structures .core import StructureFamily
2737
2838
39+ def json_decode (obj ):
40+ if isinstance (obj , (datetime , date )):
41+ return obj .isoformat ()
42+ return str (obj )
43+
44+
45+ class DummyClient :
46+ "Placeholder for a structure family we cannot (yet) handle"
47+ def __init__ (self , * args , item , ** kwargs ):
48+ self .item = item
49+
50+ STRUCTURE_CLIENTS = collections .defaultdict (lambda : DummyClient )
51+ STRUCTURE_CLIENTS .update ({"array" : DaskArrayClient , "node" : Node })
52+
2953class TiledBrowser (QWidget ):
54+ NODE_ID_MAXLEN = 8
55+ SUPPORTED_TYPES = (StructureFamily .array , StructureFamily .node )
56+
3057 # your QWidget.__init__ can optionally request the napari viewer instance
3158 # in one of two ways:
3259 # 1. use a parameter called `napari_viewer`, as done here
3360 # 2. use a type annotation of 'napari.viewer.Viewer' for any parameter
3461 def __init__ (self , napari_viewer ):
3562 super ().__init__ ()
3663 self .viewer = napari_viewer
37- self . catalog = None
38- self ._current_page = 0 # Keep track of where in the catalog we are
64+
65+ self .set_root ( None )
3966
4067 # Connection elements
4168 self .url_entry = QLineEdit ()
@@ -55,7 +82,7 @@ def __init__(self, napari_viewer):
5582 # Navigation elements
5683 self .rows_per_page_label = QLabel ("Rows per page: " )
5784 self .rows_per_page_selector = QComboBox ()
58- self .rows_per_page_selector .addItems (["5" , "10" ])
85+ self .rows_per_page_selector .addItems (["5" , "10" , "25" ])
5986 self .rows_per_page_selector .setCurrentIndex (0 )
6087
6188 self .current_location_label = QLabel ()
@@ -74,20 +101,41 @@ def __init__(self, napari_viewer):
74101 navigation_layout .addWidget (self .next_page )
75102 self .navigation_widget .setLayout (navigation_layout )
76103
104+ # Current path layout
105+ self .current_path_label = QLabel ()
106+ self ._rebuild_current_path_label ()
107+
77108 # Catalog table elements
78109 self .catalog_table = QTableWidget (0 , 1 )
110+ self .catalog_table .horizontalHeader ().setStretchLastSection (True )
111+ self .catalog_table .setEditTriggers (QTableWidget .EditTrigger .NoEditTriggers ) # disable editing
79112 self .catalog_table .horizontalHeader ().hide () # remove header
80113 self .catalog_table .setSelectionMode (QAbstractItemView .SelectionMode .SingleSelection ) # disable multi-select
81-
82114 # disabled due to bad colour palette # self.catalog_table.setAlternatingRowColors(True)
83- self ._create_table_rows ()
84115 self .catalog_table .itemDoubleClicked .connect (self ._on_item_double_click )
116+ self .catalog_table .itemSelectionChanged .connect (self ._on_item_selected )
85117 self .catalog_table_widget = QWidget ()
118+ self .catalog_breadcrumbs = None
119+
120+ # Info layout
121+ self .info_box = QTextEdit ()
122+ self .info_box .setReadOnly (True )
123+ self .load_button = QPushButton ('Open' )
124+ self .load_button .setEnabled (False )
125+ self .load_button .clicked .connect (self ._on_load )
126+ catalog_info_layout = QHBoxLayout ()
127+ catalog_info_layout .addWidget (self .catalog_table )
128+ load_layout = QVBoxLayout ()
129+ load_layout .addWidget (self .info_box )
130+ load_layout .addWidget (self .load_button )
131+ catalog_info_layout .addLayout (load_layout )
86132
87133 # Catalog table layout
88134 catalog_table_layout = QVBoxLayout ()
89- catalog_table_layout .addWidget (self .catalog_table )
135+ catalog_table_layout .addWidget (self .current_path_label )
136+ catalog_table_layout .addLayout (catalog_info_layout )
90137 catalog_table_layout .addWidget (self .navigation_widget )
138+ catalog_table_layout .addStretch (1 )
91139 self .catalog_table_widget .setLayout (catalog_table_layout )
92140 self .catalog_table_widget .setVisible (False )
93141
@@ -111,97 +159,191 @@ def __init__(self, napari_viewer):
111159 self ._on_rows_per_page_changed
112160 )
113161
114- # def _on_connect_clicked(self):
115- # url = self.url_entry.displayText()
116- # if not url:
117- # show_info("Please specify a url.")
118- # return
119- # try:
120- # self.catalog = from_uri(url)
121- # except Exception:
122- # show_info("Could not connect. Please check the url.")
123- # return
124- # self.connection_label.setText(f"Connected to {url}")
125- # self.catalog_table_widget.setVisible(True)
126- # self._set_current_location_label()
127- # self._populate_table()
128-
129162 def _on_connect_clicked (self ):
130- url = self .url_entry .displayText ()
163+ url = self .url_entry .displayText (). strip ()
131164 # url = "https://tiled-demo.blueskyproject.io/api"
132165 if not url :
133166 show_info ("Please specify a url." )
134167 return
135168 try :
136- # self.catalog = from_uri(url)["bmm"]["raw"] # .keys()[:13]
137- self .catalog = from_uri (url )
169+ root = from_uri (url , STRUCTURE_CLIENTS )
170+ if isinstance (root , DummyClient ):
171+ show_info ("Unsupported tiled type detected" )
138172 except Exception :
139173 show_info ("Could not connect. Please check the url." )
174+ else :
175+ self .connection_label .setText (f"Connected to { url } " )
176+ self .set_root (root )
177+
178+ def set_root (self , root ):
179+ self .root = root
180+ self .node_path = ()
181+ self ._current_page = 0
182+ if root is not None :
183+ self .catalog_table_widget .setVisible (True )
184+ self ._rebuild ()
185+
186+ def get_current_node (self ):
187+ return self .get_node (self .node_path )
188+
189+ @functools .lru_cache (maxsize = 1 )
190+ def get_node (self , node_path ):
191+ if node_path :
192+ return self .root [node_path ]
193+ return self .root
194+
195+ def enter_node (self , node_id ):
196+ self .node_path += (node_id ,)
197+ self ._current_page = 0
198+ self ._rebuild ()
199+
200+ def exit_node (self ):
201+ self .node_path = self .node_path [:- 1 ]
202+ self ._current_page = 0
203+ self ._rebuild ()
204+
205+ def open_node (self , node_id ):
206+ node = self .get_current_node ()[node_id ]
207+ family = node .item ['attributes' ]['structure_family' ]
208+ if isinstance (node , DummyClient ):
209+ show_info (f"Cannot open type: '{ family } '" )
140210 return
211+ if family == StructureFamily .array :
212+ layer = self .viewer .add_image (node , name = node_id )
213+ layer .reset_contrast_limits ()
214+ elif family == StructureFamily .node :
215+ self .enter_node (node_id )
216+ else :
217+ show_info (f"Type not supported:'{ family } " )
141218
142- print (f"{ self .catalog = } " )
143- self .connection_label .setText (f"Connected to { url } " )
144- self .catalog_table_widget .setVisible (True )
145- self ._set_current_location_label ()
146- self ._populate_table ()
219+ def _on_load (self ):
220+ selected = self .catalog_table .selectedItems ()
221+ if not selected :
222+ return
223+ item = selected [0 ]
224+ if item is self .catalog_breadcrumbs :
225+ return
226+ self .open_node (item .text ())
147227
148228 def _on_rows_per_page_changed (self , value ):
149229 self ._rows_per_page = int (value )
150- self ._create_table_rows ()
151- self ._populate_table ()
230+ self ._current_page = 0
231+ self ._rebuild_table ()
152232 self ._set_current_location_label ()
153233
154- def _create_table_rows (self ):
234+ def _on_item_double_click (self , item ):
235+ if item is self .catalog_breadcrumbs :
236+ self .exit_node ()
237+ return
238+ self .open_node (item .text ())
239+
240+ def _on_item_selected (self ):
241+ selected = self .catalog_table .selectedItems ()
242+ if not selected or (item := selected [0 ]) is self .catalog_breadcrumbs :
243+ self ._clear_metadata ()
244+ return
245+
246+ name = item .text ()
247+ node_path = self .node_path + (name ,)
248+ node = self .get_node (node_path )
249+
250+ attrs = node .item ['attributes' ]
251+ family = attrs ['structure_family' ]
252+ metadata = json .dumps (attrs ['metadata' ], indent = 2 , default = json_decode )
253+
254+ info = f'<b>type:</b> { family } <br>'
255+ if family == StructureFamily .array :
256+ shape = attrs ['structure' ]['macro' ]['shape' ]
257+ info += f'<b>shape:</b> { tuple (shape )} <br>'
258+ info += f'<b>metadata:</b> { metadata } '
259+ self .info_box .setText (info )
260+
261+ if family in self .SUPPORTED_TYPES :
262+ self .load_button .setEnabled (True )
263+ else :
264+ self .load_button .setEnabled (False )
265+
266+ def _clear_metadata (self ):
267+ self .info_box .setText ('' )
268+ self .load_button .setEnabled (False )
269+
270+ def _rebuild_current_path_label (self ):
271+ path = ['root' ]
272+ for node_id in self .node_path :
273+ if len (node_id ) > self .NODE_ID_MAXLEN :
274+ node_id = node_id [:self .NODE_ID_MAXLEN - 3 ] + '...'
275+ path .append (node_id )
276+ path .append ('' )
277+
278+ self .current_path_label .setText (' / ' .join (path ))
279+
280+ def _rebuild_table (self ):
281+ prev_block = self .catalog_table .blockSignals (True )
155282 # Remove all rows first
156283 while self .catalog_table .rowCount () > 0 :
157284 self .catalog_table .removeRow (0 )
285+
286+ if self .node_path :
287+ # add breadcrumbs
288+ self .catalog_breadcrumbs = QTableWidgetItem ('..' )
289+ self .catalog_table .insertRow (0 )
290+ self .catalog_table .setItem (0 , 0 , self .catalog_breadcrumbs )
291+
158292 # Then add new rows
159293 for row in range (self ._rows_per_page ):
160294 last_row_position = self .catalog_table .rowCount ()
161295 self .catalog_table .insertRow (last_row_position )
162-
163- def _on_item_double_click (self , item ):
164- name = item .text ()
165- node = self .catalog [name ]
166- family = node .item ['attributes' ]['structure_family' ]
167- if family == StructureFamily .array :
168- self .viewer .add_image (node , name = name )
169- elif family == StructureFamily .node :
170- pass
171- # TBD... open sub-browser?
172-
173- def _populate_table (self ):
174296 node_offset = self ._rows_per_page * self ._current_page
175297 # Fetch a page of keys.
176- keys = self .catalog . keys ()[node_offset :node_offset + self ._rows_per_page ]
298+ items = self .get_current_node (). items ()[node_offset :node_offset + self ._rows_per_page ]
177299 # Loop over rows, filling in keys until we run out of keys.
178- for row_index , key in zip (range (self .catalog_table .rowCount ()), keys ):
179- item = QTableWidgetItem (key )
180- item .setFlags (item .flags () ^ Qt .ItemIsEditable )
181- self .catalog_table .setItem (row_index , 0 , item )
182- self .catalog_table .setVerticalHeaderLabels ([str (x + 1 ) for x in range (node_offset , node_offset + self .catalog_table .rowCount ())])
300+ start = 1 if self .node_path else 0
301+ for row_index , (key , value ) in zip (range (start , self .catalog_table .rowCount ()), items ):
302+ family = value .item ['attributes' ]['structure_family' ]
303+ if family == StructureFamily .node :
304+ icon = self .style ().standardIcon (QStyle .SP_DirHomeIcon )
305+ elif family == StructureFamily .array :
306+ icon = QIcon (QPixmap (ICONS ['new_image' ]))
307+ else :
308+ icon = self .style ().standardIcon (QStyle .SP_TitleBarContextHelpButton )
309+ self .catalog_table .setItem (row_index , 0 , QTableWidgetItem (icon ,key ))
310+
311+ # remove extra rows
312+ for row in range (self ._rows_per_page - len (items )):
313+ self .catalog_table .removeRow (self .catalog_table .rowCount () - 1 )
314+
315+ headers = [str (x + 1 ) for x in range (node_offset , node_offset + self .catalog_table .rowCount ())]
316+ if self .node_path :
317+ headers = ['' ] + headers
318+
319+ self .catalog_table .setVerticalHeaderLabels (headers )
320+ self ._clear_metadata ()
321+ self .catalog_table .blockSignals (prev_block )
322+
323+ def _rebuild (self ):
324+ self ._rebuild_table ()
325+ self ._rebuild_current_path_label ()
326+ self ._set_current_location_label ()
183327
184328 def _on_prev_page_clicked (self ):
185329 if self ._current_page != 0 :
186330 self ._current_page -= 1
187- self ._populate_table ()
188- self ._set_current_location_label ()
331+ self ._rebuild ()
189332
190333 def _on_next_page_clicked (self ):
191334 if (
192335 self ._current_page * self ._rows_per_page
193- ) + self ._rows_per_page < len (self .catalog ):
336+ ) + self ._rows_per_page < len (self .get_current_node () ):
194337 self ._current_page += 1
195- self ._populate_table ()
196- self ._set_current_location_label ()
338+ self ._rebuild ()
197339
198340 def _set_current_location_label (self ):
199341 starting_index = self ._current_page * self ._rows_per_page + 1
200342 ending_index = min (
201- self ._rows_per_page * (self ._current_page + 1 ), len (self .catalog )
343+ self ._rows_per_page * (self ._current_page + 1 ), len (self .get_current_node () )
202344 )
203345 current_location_text = (
204- f"{ starting_index } -{ ending_index } of { len (self .catalog )} "
346+ f"{ starting_index } -{ ending_index } of { len (self .get_current_node () )} "
205347 )
206348 self .current_location_label .setText (current_location_text )
207349
0 commit comments