11import os
22from os .path import isfile , join
33from pathlib import Path
4- from typing import Callable , Dict , List , Optional , Union , cast
4+ from typing import Callable , Dict , List , Optional , TypeVar , Union , cast
5+ import logging
56
67import humanize
78import ipyvuetify as vy
1011import solara
1112from solara .components import Div
1213
14+ T = TypeVar ("T" )
15+ logger = logging .getLogger (__name__ )
16+
1317
1418def list_dir (path , filter : Callable [[Path ], bool ] = lambda x : True , directory_first : bool = False ) -> List [dict ]:
1519 def mk_item (n ):
@@ -48,9 +52,30 @@ def __contains__(self, name):
4852 return name in [k ["name" ] for k in self .files ]
4953
5054
55+ def use_reactive_or_value (
56+ value : Union [T , solara .Reactive [T ]], on_value : Optional [Callable [[T ], None ]] = None , value_name = "value" , on_value_name = "on_value" , use_internal_value = False
57+ ):
58+ def hookup_on_value ():
59+ if isinstance (value , solara .Reactive ) and on_value :
60+ return value .subscribe (on_value )
61+
62+ solara .use_effect (hookup_on_value , [isinstance (value , solara .Reactive ), on_value ])
63+ internal_value , set_internal_value = solara .use_state (value .value if isinstance (value , solara .Reactive ) else value )
64+ if use_internal_value :
65+ return internal_value , set_internal_value
66+ if isinstance (value , solara .Reactive ):
67+ return value .value , value .set
68+ elif on_value :
69+ return value , on_value
70+ else :
71+ logger .warning ("You should provide an %s callback if you are not using a reactive value, otherwise %s input will not update" , on_value_name , value_name )
72+ return value , lambda x : None
73+
74+
5175@solara .component
5276def FileBrowser (
5377 directory : Union [None , str , Path , solara .Reactive [Path ]] = None ,
78+ selected : Union [None , Path , solara .Reactive [Optional [Path ]]] = None ,
5479 on_directory_change : Optional [Callable [[Path ], None ]] = None ,
5580 on_path_select : Optional [Callable [[Optional [Path ]], None ]] = None ,
5681 on_file_open : Optional [Callable [[Path ], None ]] = None ,
@@ -75,7 +100,8 @@ def FileBrowser(
75100
76101 ## Arguments
77102
78- * `directory`: The directory to start in. If `None` the current working directory is used.
103+ * `directory`: The directory to start in. If `None`, the current working directory is used.
104+ * `selected`: The selected file or directory. If `None`, no file or directory is selected (requires `can_select=True`).
79105 * `on_directory_change`: Depends on mode, see above.
80106 * `on_path_select`: Depends on mode, see above.
81107 * `on_file_open`: Depends on mode, see above.
@@ -90,13 +116,30 @@ def FileBrowser(
90116 directory = os .getcwd () # pragma: no cover
91117 if isinstance (directory , str ):
92118 directory = Path (directory )
119+ # directory = directory.resolve()
93120 current_dir = solara .use_reactive (directory )
94- selected , set_selected = solara .use_state (None )
95121 double_clicked , set_double_clicked = solara .use_state (None )
96122 warning , set_warning = solara .use_state (cast (Optional [str ], None ))
97123 scroll_pos_stack , set_scroll_pos_stack = solara .use_state (cast (List [int ], []))
98124 scroll_pos , set_scroll_pos = solara .use_state (0 )
99- selected , set_selected = solara .use_state (None )
125+ selected_private , set_selected_private = use_reactive_or_value (
126+ selected ,
127+ on_value = on_path_select if can_select else lambda x : None ,
128+ value_name = "selected" ,
129+ on_value_name = "on_path_select" ,
130+ use_internal_value = not can_select ,
131+ )
132+ # remove so we don't accidentally use it
133+ del selected
134+
135+ def sync_directory_from_selected ():
136+ if selected_private is not None :
137+ # if we select a file, we need to make sure the directory is correct
138+ # NOTE: although we expect a Path, abuse might make it a string
139+ if isinstance (selected_private , Path ):
140+ current_dir .value = selected_private .resolve ().parent
141+
142+ solara .use_effect (sync_directory_from_selected , [selected_private ])
100143
101144 def change_dir (new_dir : Path ):
102145 if os .access (new_dir , os .R_OK ):
@@ -121,7 +164,7 @@ def on_item(item, double_click):
121164 last_pos = scroll_pos_stack [- 1 ]
122165 set_scroll_pos_stack (scroll_pos_stack [:- 1 ])
123166 set_scroll_pos (last_pos )
124- set_selected (None )
167+ set_selected_private (None )
125168 set_double_clicked (None )
126169 if on_path_select and can_select :
127170 on_path_select (None )
@@ -142,7 +185,7 @@ def on_item(item, double_click):
142185 if change_dir (path ):
143186 set_scroll_pos_stack (scroll_pos_stack + [scroll_pos ])
144187 set_scroll_pos (0 )
145- set_selected (None )
188+ set_selected_private (None )
146189 set_double_clicked (None )
147190 if on_path_select and can_select :
148191 on_path_select (None )
@@ -153,7 +196,7 @@ def on_item(item, double_click):
153196 raise RuntimeError ("Combination should not happen" ) # pragma: no cover
154197
155198 def on_click (item ):
156- set_selected (item )
199+ set_selected_private (item [ "name" ] if item else None )
157200 on_item (item , False )
158201
159202 def on_double_click (item ):
@@ -163,12 +206,20 @@ def on_double_click(item):
163206 # otherwise we can ignore it, single click will handle it
164207
165208 files = [{"name" : ".." , "is_file" : False }] + list_dir (current_dir .value , filter = filter , directory_first = directory_first )
209+ clicked = (
210+ {
211+ "name" : selected_private .name if isinstance (selected_private , Path ) else selected_private ,
212+ "is_file" : isinstance (selected_private , Path ),
213+ "size" : None ,
214+ }
215+ if selected_private is not None
216+ else None
217+ )
166218 with Div (class_ = "solara-file-browser" ) as main :
167219 Div (children = [str (current_dir .value .resolve ())])
168220 FileListWidget .element (
169221 files = files ,
170- selected = selected ,
171- clicked = selected ,
222+ clicked = clicked ,
172223 on_clicked = on_click ,
173224 double_clicked = double_clicked ,
174225 on_double_clicked = on_double_click ,
0 commit comments