bauiv1lib.fileselector
UI functionality for selecting files.
1# Released under the MIT License. See LICENSE for details. 2# 3"""UI functionality for selecting files.""" 4 5from __future__ import annotations 6 7import os 8import time 9import logging 10from threading import Thread 11from typing import TYPE_CHECKING, override 12 13import bauiv1 as bui 14 15if TYPE_CHECKING: 16 from typing import Any, Callable, Sequence 17 18 19class FileSelectorWindow(bui.Window): 20 """Window for selecting files.""" 21 22 def __init__( 23 self, 24 path: str, 25 callback: Callable[[str | None], Any] | None = None, 26 show_base_path: bool = True, 27 valid_file_extensions: Sequence[str] | None = None, 28 allow_folders: bool = False, 29 ): 30 if valid_file_extensions is None: 31 valid_file_extensions = [] 32 assert bui.app.classic is not None 33 uiscale = bui.app.ui_v1.uiscale 34 self._width = 700 if uiscale is bui.UIScale.SMALL else 600 35 self._x_inset = x_inset = 50 if uiscale is bui.UIScale.SMALL else 0 36 self._height = 365 if uiscale is bui.UIScale.SMALL else 418 37 self._callback = callback 38 self._base_path = path 39 self._path: str | None = None 40 self._recent_paths: list[str] = [] 41 self._show_base_path = show_base_path 42 self._valid_file_extensions = [ 43 '.' + ext for ext in valid_file_extensions 44 ] 45 self._allow_folders = allow_folders 46 self._subcontainer: bui.Widget | None = None 47 self._subcontainerheight: float | None = None 48 self._scroll_width = self._width - (80 + 2 * x_inset) 49 self._scroll_height = self._height - 170 50 self._r = 'fileSelectorWindow' 51 super().__init__( 52 root_widget=bui.containerwidget( 53 size=(self._width, self._height), 54 transition='in_right', 55 scale=( 56 2.23 57 if uiscale is bui.UIScale.SMALL 58 else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0 59 ), 60 stack_offset=( 61 (0, -35) if uiscale is bui.UIScale.SMALL else (0, 0) 62 ), 63 ) 64 ) 65 bui.textwidget( 66 parent=self._root_widget, 67 position=(self._width * 0.5, self._height - 42), 68 size=(0, 0), 69 color=bui.app.ui_v1.title_color, 70 h_align='center', 71 v_align='center', 72 text=( 73 bui.Lstr(resource=self._r + '.titleFolderText') 74 if (allow_folders and not valid_file_extensions) 75 else ( 76 bui.Lstr(resource=self._r + '.titleFileText') 77 if not allow_folders 78 else bui.Lstr(resource=self._r + '.titleFileFolderText') 79 ) 80 ), 81 maxwidth=210, 82 ) 83 84 self._button_width = 146 85 self._cancel_button = bui.buttonwidget( 86 parent=self._root_widget, 87 position=(35 + x_inset, self._height - 67), 88 autoselect=True, 89 size=(self._button_width, 50), 90 label=bui.Lstr(resource='cancelText'), 91 on_activate_call=self._cancel, 92 ) 93 bui.widget(edit=self._cancel_button, left_widget=self._cancel_button) 94 95 b_color = (0.6, 0.53, 0.63) 96 97 self._back_button = bui.buttonwidget( 98 parent=self._root_widget, 99 button_type='square', 100 position=(43 + x_inset, self._height - 113), 101 color=b_color, 102 textcolor=(0.75, 0.7, 0.8), 103 enable_sound=False, 104 size=(55, 35), 105 label=bui.charstr(bui.SpecialChar.LEFT_ARROW), 106 on_activate_call=self._on_back_press, 107 ) 108 109 self._folder_tex = bui.gettexture('folder') 110 self._folder_color = (1.1, 0.8, 0.2) 111 self._file_tex = bui.gettexture('file') 112 self._file_color = (1, 1, 1) 113 self._use_folder_button: bui.Widget | None = None 114 self._folder_center = self._width * 0.5 + 15 115 self._folder_icon = bui.imagewidget( 116 parent=self._root_widget, 117 size=(40, 40), 118 position=(40, self._height - 117), 119 texture=self._folder_tex, 120 color=self._folder_color, 121 ) 122 self._path_text = bui.textwidget( 123 parent=self._root_widget, 124 position=(self._folder_center, self._height - 98), 125 size=(0, 0), 126 color=bui.app.ui_v1.title_color, 127 h_align='center', 128 v_align='center', 129 text=self._path, 130 maxwidth=self._width * 0.9, 131 ) 132 self._scrollwidget: bui.Widget | None = None 133 bui.containerwidget( 134 edit=self._root_widget, cancel_button=self._cancel_button 135 ) 136 self._set_path(path) 137 138 def _on_up_press(self) -> None: 139 self._on_entry_activated('..') 140 141 def _on_back_press(self) -> None: 142 if len(self._recent_paths) > 1: 143 bui.getsound('swish').play() 144 self._recent_paths.pop() 145 self._set_path(self._recent_paths.pop()) 146 else: 147 bui.getsound('error').play() 148 149 def _on_folder_entry_activated(self) -> None: 150 bui.containerwidget(edit=self._root_widget, transition='out_right') 151 if self._callback is not None: 152 assert self._path is not None 153 self._callback(self._path) 154 155 def _on_entry_activated(self, entry: str) -> None: 156 # pylint: disable=too-many-branches 157 new_path = None 158 try: 159 assert self._path is not None 160 if entry == '..': 161 chunks = self._path.split('/') 162 if len(chunks) > 1: 163 new_path = '/'.join(chunks[:-1]) 164 if new_path == '': 165 new_path = '/' 166 else: 167 bui.getsound('error').play() 168 else: 169 if self._path == '/': 170 test_path = self._path + entry 171 else: 172 test_path = self._path + '/' + entry 173 if os.path.isdir(test_path): 174 bui.getsound('swish').play() 175 new_path = test_path 176 elif os.path.isfile(test_path): 177 if self._is_valid_file_path(test_path): 178 bui.getsound('swish').play() 179 bui.containerwidget( 180 edit=self._root_widget, transition='out_right' 181 ) 182 if self._callback is not None: 183 self._callback(test_path) 184 else: 185 bui.getsound('error').play() 186 else: 187 print( 188 ( 189 'Error: FileSelectorWindow found non-file/dir:', 190 test_path, 191 ) 192 ) 193 except Exception: 194 logging.exception( 195 'Error in FileSelectorWindow._on_entry_activated().' 196 ) 197 198 if new_path is not None: 199 self._set_path(new_path) 200 201 class _RefreshThread(Thread): 202 def __init__( 203 self, path: str, callback: Callable[[list[str], str | None], Any] 204 ): 205 super().__init__() 206 self._callback = callback 207 self._path = path 208 209 @override 210 def run(self) -> None: 211 try: 212 starttime = time.time() 213 files = os.listdir(self._path) 214 duration = time.time() - starttime 215 min_time = 0.1 216 217 # Make sure this takes at least 1/10 second so the user 218 # has time to see the selection highlight. 219 if duration < min_time: 220 time.sleep(min_time - duration) 221 bui.pushcall( 222 bui.Call(self._callback, files, None), 223 from_other_thread=True, 224 ) 225 except Exception as exc: 226 # Ignore permission-denied. 227 if 'Errno 13' not in str(exc): 228 logging.exception('Error in fileselector refresh thread.') 229 nofiles: list[str] = [] 230 bui.pushcall( 231 bui.Call(self._callback, nofiles, str(exc)), 232 from_other_thread=True, 233 ) 234 235 def _set_path(self, path: str, add_to_recent: bool = True) -> None: 236 self._path = path 237 if add_to_recent: 238 self._recent_paths.append(path) 239 self._RefreshThread(path, self._refresh).start() 240 241 def _refresh(self, file_names: list[str], error: str | None) -> None: 242 # pylint: disable=too-many-statements 243 # pylint: disable=too-many-branches 244 # pylint: disable=too-many-locals 245 if not self._root_widget: 246 return 247 248 scrollwidget_selected = ( 249 self._scrollwidget is None 250 or self._root_widget.get_selected_child() == self._scrollwidget 251 ) 252 253 in_top_folder = self._path == self._base_path 254 hide_top_folder = in_top_folder and self._show_base_path is False 255 256 if hide_top_folder: 257 folder_name = '' 258 elif self._path == '/': 259 folder_name = '/' 260 else: 261 assert self._path is not None 262 folder_name = os.path.basename(self._path) 263 264 b_color = (0.6, 0.53, 0.63) 265 b_color_disabled = (0.65, 0.65, 0.65) 266 267 if len(self._recent_paths) < 2: 268 bui.buttonwidget( 269 edit=self._back_button, 270 color=b_color_disabled, 271 textcolor=(0.5, 0.5, 0.5), 272 ) 273 else: 274 bui.buttonwidget( 275 edit=self._back_button, 276 color=b_color, 277 textcolor=(0.75, 0.7, 0.8), 278 ) 279 280 max_str_width = 300.0 281 str_width = min( 282 max_str_width, 283 bui.get_string_width(folder_name, suppress_warning=True), 284 ) 285 bui.textwidget( 286 edit=self._path_text, text=folder_name, maxwidth=max_str_width 287 ) 288 bui.imagewidget( 289 edit=self._folder_icon, 290 position=( 291 self._folder_center - str_width * 0.5 - 40, 292 self._height - 117, 293 ), 294 opacity=0.0 if hide_top_folder else 1.0, 295 ) 296 297 if self._scrollwidget is not None: 298 self._scrollwidget.delete() 299 300 if self._use_folder_button is not None: 301 self._use_folder_button.delete() 302 bui.widget(edit=self._cancel_button, right_widget=self._back_button) 303 304 self._scrollwidget = bui.scrollwidget( 305 parent=self._root_widget, 306 position=( 307 (self._width - self._scroll_width) * 0.5, 308 self._height - self._scroll_height - 119, 309 ), 310 size=(self._scroll_width, self._scroll_height), 311 ) 312 313 if scrollwidget_selected: 314 bui.containerwidget( 315 edit=self._root_widget, selected_child=self._scrollwidget 316 ) 317 318 # show error case.. 319 if error is not None: 320 self._subcontainer = bui.containerwidget( 321 parent=self._scrollwidget, 322 size=(self._scroll_width, self._scroll_height), 323 background=False, 324 ) 325 bui.textwidget( 326 parent=self._subcontainer, 327 color=(1, 1, 0, 1), 328 text=error, 329 maxwidth=self._scroll_width * 0.9, 330 position=( 331 self._scroll_width * 0.48, 332 self._scroll_height * 0.57, 333 ), 334 size=(0, 0), 335 h_align='center', 336 v_align='center', 337 ) 338 339 else: 340 file_names = [f for f in file_names if not f.startswith('.')] 341 file_names.sort(key=lambda x: x[0].lower()) 342 343 entries = file_names 344 entry_height = 35 345 folder_entry_height = 100 346 show_folder_entry = False 347 show_use_folder_button = self._allow_folders and not in_top_folder 348 349 self._subcontainerheight = entry_height * len(entries) + ( 350 folder_entry_height if show_folder_entry else 0 351 ) 352 v = self._subcontainerheight - ( 353 folder_entry_height if show_folder_entry else 0 354 ) 355 356 self._subcontainer = bui.containerwidget( 357 parent=self._scrollwidget, 358 size=(self._scroll_width, self._subcontainerheight), 359 background=False, 360 ) 361 362 bui.containerwidget( 363 edit=self._scrollwidget, 364 claims_left_right=False, 365 claims_tab=False, 366 ) 367 bui.containerwidget( 368 edit=self._subcontainer, 369 claims_left_right=False, 370 claims_tab=False, 371 selection_loops=False, 372 print_list_exit_instructions=False, 373 ) 374 bui.widget(edit=self._subcontainer, up_widget=self._back_button) 375 376 if show_use_folder_button: 377 self._use_folder_button = btn = bui.buttonwidget( 378 parent=self._root_widget, 379 position=( 380 self._width - self._button_width - 35 - self._x_inset, 381 self._height - 67, 382 ), 383 size=(self._button_width, 50), 384 label=bui.Lstr( 385 resource=self._r + '.useThisFolderButtonText' 386 ), 387 on_activate_call=self._on_folder_entry_activated, 388 ) 389 bui.widget( 390 edit=btn, 391 left_widget=self._cancel_button, 392 down_widget=self._scrollwidget, 393 ) 394 bui.widget(edit=self._cancel_button, right_widget=btn) 395 bui.containerwidget(edit=self._root_widget, start_button=btn) 396 397 folder_icon_size = 35 398 for num, entry in enumerate(entries): 399 cnt = bui.containerwidget( 400 parent=self._subcontainer, 401 position=(0, v - entry_height), 402 size=(self._scroll_width, entry_height), 403 root_selectable=True, 404 background=False, 405 click_activate=True, 406 on_activate_call=bui.Call(self._on_entry_activated, entry), 407 ) 408 if num == 0: 409 bui.widget(edit=cnt, up_widget=self._back_button) 410 is_valid_file_path = self._is_valid_file_path(entry) 411 assert self._path is not None 412 is_dir = os.path.isdir(self._path + '/' + entry) 413 if is_dir: 414 bui.imagewidget( 415 parent=cnt, 416 size=(folder_icon_size, folder_icon_size), 417 position=( 418 10, 419 0.5 * entry_height - folder_icon_size * 0.5, 420 ), 421 draw_controller=cnt, 422 texture=self._folder_tex, 423 color=self._folder_color, 424 ) 425 else: 426 bui.imagewidget( 427 parent=cnt, 428 size=(folder_icon_size, folder_icon_size), 429 position=( 430 10, 431 0.5 * entry_height - folder_icon_size * 0.5, 432 ), 433 opacity=1.0 if is_valid_file_path else 0.5, 434 draw_controller=cnt, 435 texture=self._file_tex, 436 color=self._file_color, 437 ) 438 bui.textwidget( 439 parent=cnt, 440 draw_controller=cnt, 441 text=entry, 442 h_align='left', 443 v_align='center', 444 position=(10 + folder_icon_size * 1.05, entry_height * 0.5), 445 size=(0, 0), 446 maxwidth=self._scroll_width * 0.93 - 50, 447 color=( 448 (1, 1, 1, 1) 449 if (is_valid_file_path or is_dir) 450 else (0.5, 0.5, 0.5, 1) 451 ), 452 ) 453 v -= entry_height 454 455 def _is_valid_file_path(self, path: str) -> bool: 456 return any( 457 path.lower().endswith(ext) for ext in self._valid_file_extensions 458 ) 459 460 def _cancel(self) -> None: 461 bui.containerwidget(edit=self._root_widget, transition='out_right') 462 if self._callback is not None: 463 self._callback(None)
class
FileSelectorWindow(bauiv1._uitypes.Window):
20class FileSelectorWindow(bui.Window): 21 """Window for selecting files.""" 22 23 def __init__( 24 self, 25 path: str, 26 callback: Callable[[str | None], Any] | None = None, 27 show_base_path: bool = True, 28 valid_file_extensions: Sequence[str] | None = None, 29 allow_folders: bool = False, 30 ): 31 if valid_file_extensions is None: 32 valid_file_extensions = [] 33 assert bui.app.classic is not None 34 uiscale = bui.app.ui_v1.uiscale 35 self._width = 700 if uiscale is bui.UIScale.SMALL else 600 36 self._x_inset = x_inset = 50 if uiscale is bui.UIScale.SMALL else 0 37 self._height = 365 if uiscale is bui.UIScale.SMALL else 418 38 self._callback = callback 39 self._base_path = path 40 self._path: str | None = None 41 self._recent_paths: list[str] = [] 42 self._show_base_path = show_base_path 43 self._valid_file_extensions = [ 44 '.' + ext for ext in valid_file_extensions 45 ] 46 self._allow_folders = allow_folders 47 self._subcontainer: bui.Widget | None = None 48 self._subcontainerheight: float | None = None 49 self._scroll_width = self._width - (80 + 2 * x_inset) 50 self._scroll_height = self._height - 170 51 self._r = 'fileSelectorWindow' 52 super().__init__( 53 root_widget=bui.containerwidget( 54 size=(self._width, self._height), 55 transition='in_right', 56 scale=( 57 2.23 58 if uiscale is bui.UIScale.SMALL 59 else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0 60 ), 61 stack_offset=( 62 (0, -35) if uiscale is bui.UIScale.SMALL else (0, 0) 63 ), 64 ) 65 ) 66 bui.textwidget( 67 parent=self._root_widget, 68 position=(self._width * 0.5, self._height - 42), 69 size=(0, 0), 70 color=bui.app.ui_v1.title_color, 71 h_align='center', 72 v_align='center', 73 text=( 74 bui.Lstr(resource=self._r + '.titleFolderText') 75 if (allow_folders and not valid_file_extensions) 76 else ( 77 bui.Lstr(resource=self._r + '.titleFileText') 78 if not allow_folders 79 else bui.Lstr(resource=self._r + '.titleFileFolderText') 80 ) 81 ), 82 maxwidth=210, 83 ) 84 85 self._button_width = 146 86 self._cancel_button = bui.buttonwidget( 87 parent=self._root_widget, 88 position=(35 + x_inset, self._height - 67), 89 autoselect=True, 90 size=(self._button_width, 50), 91 label=bui.Lstr(resource='cancelText'), 92 on_activate_call=self._cancel, 93 ) 94 bui.widget(edit=self._cancel_button, left_widget=self._cancel_button) 95 96 b_color = (0.6, 0.53, 0.63) 97 98 self._back_button = bui.buttonwidget( 99 parent=self._root_widget, 100 button_type='square', 101 position=(43 + x_inset, self._height - 113), 102 color=b_color, 103 textcolor=(0.75, 0.7, 0.8), 104 enable_sound=False, 105 size=(55, 35), 106 label=bui.charstr(bui.SpecialChar.LEFT_ARROW), 107 on_activate_call=self._on_back_press, 108 ) 109 110 self._folder_tex = bui.gettexture('folder') 111 self._folder_color = (1.1, 0.8, 0.2) 112 self._file_tex = bui.gettexture('file') 113 self._file_color = (1, 1, 1) 114 self._use_folder_button: bui.Widget | None = None 115 self._folder_center = self._width * 0.5 + 15 116 self._folder_icon = bui.imagewidget( 117 parent=self._root_widget, 118 size=(40, 40), 119 position=(40, self._height - 117), 120 texture=self._folder_tex, 121 color=self._folder_color, 122 ) 123 self._path_text = bui.textwidget( 124 parent=self._root_widget, 125 position=(self._folder_center, self._height - 98), 126 size=(0, 0), 127 color=bui.app.ui_v1.title_color, 128 h_align='center', 129 v_align='center', 130 text=self._path, 131 maxwidth=self._width * 0.9, 132 ) 133 self._scrollwidget: bui.Widget | None = None 134 bui.containerwidget( 135 edit=self._root_widget, cancel_button=self._cancel_button 136 ) 137 self._set_path(path) 138 139 def _on_up_press(self) -> None: 140 self._on_entry_activated('..') 141 142 def _on_back_press(self) -> None: 143 if len(self._recent_paths) > 1: 144 bui.getsound('swish').play() 145 self._recent_paths.pop() 146 self._set_path(self._recent_paths.pop()) 147 else: 148 bui.getsound('error').play() 149 150 def _on_folder_entry_activated(self) -> None: 151 bui.containerwidget(edit=self._root_widget, transition='out_right') 152 if self._callback is not None: 153 assert self._path is not None 154 self._callback(self._path) 155 156 def _on_entry_activated(self, entry: str) -> None: 157 # pylint: disable=too-many-branches 158 new_path = None 159 try: 160 assert self._path is not None 161 if entry == '..': 162 chunks = self._path.split('/') 163 if len(chunks) > 1: 164 new_path = '/'.join(chunks[:-1]) 165 if new_path == '': 166 new_path = '/' 167 else: 168 bui.getsound('error').play() 169 else: 170 if self._path == '/': 171 test_path = self._path + entry 172 else: 173 test_path = self._path + '/' + entry 174 if os.path.isdir(test_path): 175 bui.getsound('swish').play() 176 new_path = test_path 177 elif os.path.isfile(test_path): 178 if self._is_valid_file_path(test_path): 179 bui.getsound('swish').play() 180 bui.containerwidget( 181 edit=self._root_widget, transition='out_right' 182 ) 183 if self._callback is not None: 184 self._callback(test_path) 185 else: 186 bui.getsound('error').play() 187 else: 188 print( 189 ( 190 'Error: FileSelectorWindow found non-file/dir:', 191 test_path, 192 ) 193 ) 194 except Exception: 195 logging.exception( 196 'Error in FileSelectorWindow._on_entry_activated().' 197 ) 198 199 if new_path is not None: 200 self._set_path(new_path) 201 202 class _RefreshThread(Thread): 203 def __init__( 204 self, path: str, callback: Callable[[list[str], str | None], Any] 205 ): 206 super().__init__() 207 self._callback = callback 208 self._path = path 209 210 @override 211 def run(self) -> None: 212 try: 213 starttime = time.time() 214 files = os.listdir(self._path) 215 duration = time.time() - starttime 216 min_time = 0.1 217 218 # Make sure this takes at least 1/10 second so the user 219 # has time to see the selection highlight. 220 if duration < min_time: 221 time.sleep(min_time - duration) 222 bui.pushcall( 223 bui.Call(self._callback, files, None), 224 from_other_thread=True, 225 ) 226 except Exception as exc: 227 # Ignore permission-denied. 228 if 'Errno 13' not in str(exc): 229 logging.exception('Error in fileselector refresh thread.') 230 nofiles: list[str] = [] 231 bui.pushcall( 232 bui.Call(self._callback, nofiles, str(exc)), 233 from_other_thread=True, 234 ) 235 236 def _set_path(self, path: str, add_to_recent: bool = True) -> None: 237 self._path = path 238 if add_to_recent: 239 self._recent_paths.append(path) 240 self._RefreshThread(path, self._refresh).start() 241 242 def _refresh(self, file_names: list[str], error: str | None) -> None: 243 # pylint: disable=too-many-statements 244 # pylint: disable=too-many-branches 245 # pylint: disable=too-many-locals 246 if not self._root_widget: 247 return 248 249 scrollwidget_selected = ( 250 self._scrollwidget is None 251 or self._root_widget.get_selected_child() == self._scrollwidget 252 ) 253 254 in_top_folder = self._path == self._base_path 255 hide_top_folder = in_top_folder and self._show_base_path is False 256 257 if hide_top_folder: 258 folder_name = '' 259 elif self._path == '/': 260 folder_name = '/' 261 else: 262 assert self._path is not None 263 folder_name = os.path.basename(self._path) 264 265 b_color = (0.6, 0.53, 0.63) 266 b_color_disabled = (0.65, 0.65, 0.65) 267 268 if len(self._recent_paths) < 2: 269 bui.buttonwidget( 270 edit=self._back_button, 271 color=b_color_disabled, 272 textcolor=(0.5, 0.5, 0.5), 273 ) 274 else: 275 bui.buttonwidget( 276 edit=self._back_button, 277 color=b_color, 278 textcolor=(0.75, 0.7, 0.8), 279 ) 280 281 max_str_width = 300.0 282 str_width = min( 283 max_str_width, 284 bui.get_string_width(folder_name, suppress_warning=True), 285 ) 286 bui.textwidget( 287 edit=self._path_text, text=folder_name, maxwidth=max_str_width 288 ) 289 bui.imagewidget( 290 edit=self._folder_icon, 291 position=( 292 self._folder_center - str_width * 0.5 - 40, 293 self._height - 117, 294 ), 295 opacity=0.0 if hide_top_folder else 1.0, 296 ) 297 298 if self._scrollwidget is not None: 299 self._scrollwidget.delete() 300 301 if self._use_folder_button is not None: 302 self._use_folder_button.delete() 303 bui.widget(edit=self._cancel_button, right_widget=self._back_button) 304 305 self._scrollwidget = bui.scrollwidget( 306 parent=self._root_widget, 307 position=( 308 (self._width - self._scroll_width) * 0.5, 309 self._height - self._scroll_height - 119, 310 ), 311 size=(self._scroll_width, self._scroll_height), 312 ) 313 314 if scrollwidget_selected: 315 bui.containerwidget( 316 edit=self._root_widget, selected_child=self._scrollwidget 317 ) 318 319 # show error case.. 320 if error is not None: 321 self._subcontainer = bui.containerwidget( 322 parent=self._scrollwidget, 323 size=(self._scroll_width, self._scroll_height), 324 background=False, 325 ) 326 bui.textwidget( 327 parent=self._subcontainer, 328 color=(1, 1, 0, 1), 329 text=error, 330 maxwidth=self._scroll_width * 0.9, 331 position=( 332 self._scroll_width * 0.48, 333 self._scroll_height * 0.57, 334 ), 335 size=(0, 0), 336 h_align='center', 337 v_align='center', 338 ) 339 340 else: 341 file_names = [f for f in file_names if not f.startswith('.')] 342 file_names.sort(key=lambda x: x[0].lower()) 343 344 entries = file_names 345 entry_height = 35 346 folder_entry_height = 100 347 show_folder_entry = False 348 show_use_folder_button = self._allow_folders and not in_top_folder 349 350 self._subcontainerheight = entry_height * len(entries) + ( 351 folder_entry_height if show_folder_entry else 0 352 ) 353 v = self._subcontainerheight - ( 354 folder_entry_height if show_folder_entry else 0 355 ) 356 357 self._subcontainer = bui.containerwidget( 358 parent=self._scrollwidget, 359 size=(self._scroll_width, self._subcontainerheight), 360 background=False, 361 ) 362 363 bui.containerwidget( 364 edit=self._scrollwidget, 365 claims_left_right=False, 366 claims_tab=False, 367 ) 368 bui.containerwidget( 369 edit=self._subcontainer, 370 claims_left_right=False, 371 claims_tab=False, 372 selection_loops=False, 373 print_list_exit_instructions=False, 374 ) 375 bui.widget(edit=self._subcontainer, up_widget=self._back_button) 376 377 if show_use_folder_button: 378 self._use_folder_button = btn = bui.buttonwidget( 379 parent=self._root_widget, 380 position=( 381 self._width - self._button_width - 35 - self._x_inset, 382 self._height - 67, 383 ), 384 size=(self._button_width, 50), 385 label=bui.Lstr( 386 resource=self._r + '.useThisFolderButtonText' 387 ), 388 on_activate_call=self._on_folder_entry_activated, 389 ) 390 bui.widget( 391 edit=btn, 392 left_widget=self._cancel_button, 393 down_widget=self._scrollwidget, 394 ) 395 bui.widget(edit=self._cancel_button, right_widget=btn) 396 bui.containerwidget(edit=self._root_widget, start_button=btn) 397 398 folder_icon_size = 35 399 for num, entry in enumerate(entries): 400 cnt = bui.containerwidget( 401 parent=self._subcontainer, 402 position=(0, v - entry_height), 403 size=(self._scroll_width, entry_height), 404 root_selectable=True, 405 background=False, 406 click_activate=True, 407 on_activate_call=bui.Call(self._on_entry_activated, entry), 408 ) 409 if num == 0: 410 bui.widget(edit=cnt, up_widget=self._back_button) 411 is_valid_file_path = self._is_valid_file_path(entry) 412 assert self._path is not None 413 is_dir = os.path.isdir(self._path + '/' + entry) 414 if is_dir: 415 bui.imagewidget( 416 parent=cnt, 417 size=(folder_icon_size, folder_icon_size), 418 position=( 419 10, 420 0.5 * entry_height - folder_icon_size * 0.5, 421 ), 422 draw_controller=cnt, 423 texture=self._folder_tex, 424 color=self._folder_color, 425 ) 426 else: 427 bui.imagewidget( 428 parent=cnt, 429 size=(folder_icon_size, folder_icon_size), 430 position=( 431 10, 432 0.5 * entry_height - folder_icon_size * 0.5, 433 ), 434 opacity=1.0 if is_valid_file_path else 0.5, 435 draw_controller=cnt, 436 texture=self._file_tex, 437 color=self._file_color, 438 ) 439 bui.textwidget( 440 parent=cnt, 441 draw_controller=cnt, 442 text=entry, 443 h_align='left', 444 v_align='center', 445 position=(10 + folder_icon_size * 1.05, entry_height * 0.5), 446 size=(0, 0), 447 maxwidth=self._scroll_width * 0.93 - 50, 448 color=( 449 (1, 1, 1, 1) 450 if (is_valid_file_path or is_dir) 451 else (0.5, 0.5, 0.5, 1) 452 ), 453 ) 454 v -= entry_height 455 456 def _is_valid_file_path(self, path: str) -> bool: 457 return any( 458 path.lower().endswith(ext) for ext in self._valid_file_extensions 459 ) 460 461 def _cancel(self) -> None: 462 bui.containerwidget(edit=self._root_widget, transition='out_right') 463 if self._callback is not None: 464 self._callback(None)
Window for selecting files.
FileSelectorWindow( path: str, callback: Optional[Callable[[str | None], Any]] = None, show_base_path: bool = True, valid_file_extensions: Optional[Sequence[str]] = None, allow_folders: bool = False)
23 def __init__( 24 self, 25 path: str, 26 callback: Callable[[str | None], Any] | None = None, 27 show_base_path: bool = True, 28 valid_file_extensions: Sequence[str] | None = None, 29 allow_folders: bool = False, 30 ): 31 if valid_file_extensions is None: 32 valid_file_extensions = [] 33 assert bui.app.classic is not None 34 uiscale = bui.app.ui_v1.uiscale 35 self._width = 700 if uiscale is bui.UIScale.SMALL else 600 36 self._x_inset = x_inset = 50 if uiscale is bui.UIScale.SMALL else 0 37 self._height = 365 if uiscale is bui.UIScale.SMALL else 418 38 self._callback = callback 39 self._base_path = path 40 self._path: str | None = None 41 self._recent_paths: list[str] = [] 42 self._show_base_path = show_base_path 43 self._valid_file_extensions = [ 44 '.' + ext for ext in valid_file_extensions 45 ] 46 self._allow_folders = allow_folders 47 self._subcontainer: bui.Widget | None = None 48 self._subcontainerheight: float | None = None 49 self._scroll_width = self._width - (80 + 2 * x_inset) 50 self._scroll_height = self._height - 170 51 self._r = 'fileSelectorWindow' 52 super().__init__( 53 root_widget=bui.containerwidget( 54 size=(self._width, self._height), 55 transition='in_right', 56 scale=( 57 2.23 58 if uiscale is bui.UIScale.SMALL 59 else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0 60 ), 61 stack_offset=( 62 (0, -35) if uiscale is bui.UIScale.SMALL else (0, 0) 63 ), 64 ) 65 ) 66 bui.textwidget( 67 parent=self._root_widget, 68 position=(self._width * 0.5, self._height - 42), 69 size=(0, 0), 70 color=bui.app.ui_v1.title_color, 71 h_align='center', 72 v_align='center', 73 text=( 74 bui.Lstr(resource=self._r + '.titleFolderText') 75 if (allow_folders and not valid_file_extensions) 76 else ( 77 bui.Lstr(resource=self._r + '.titleFileText') 78 if not allow_folders 79 else bui.Lstr(resource=self._r + '.titleFileFolderText') 80 ) 81 ), 82 maxwidth=210, 83 ) 84 85 self._button_width = 146 86 self._cancel_button = bui.buttonwidget( 87 parent=self._root_widget, 88 position=(35 + x_inset, self._height - 67), 89 autoselect=True, 90 size=(self._button_width, 50), 91 label=bui.Lstr(resource='cancelText'), 92 on_activate_call=self._cancel, 93 ) 94 bui.widget(edit=self._cancel_button, left_widget=self._cancel_button) 95 96 b_color = (0.6, 0.53, 0.63) 97 98 self._back_button = bui.buttonwidget( 99 parent=self._root_widget, 100 button_type='square', 101 position=(43 + x_inset, self._height - 113), 102 color=b_color, 103 textcolor=(0.75, 0.7, 0.8), 104 enable_sound=False, 105 size=(55, 35), 106 label=bui.charstr(bui.SpecialChar.LEFT_ARROW), 107 on_activate_call=self._on_back_press, 108 ) 109 110 self._folder_tex = bui.gettexture('folder') 111 self._folder_color = (1.1, 0.8, 0.2) 112 self._file_tex = bui.gettexture('file') 113 self._file_color = (1, 1, 1) 114 self._use_folder_button: bui.Widget | None = None 115 self._folder_center = self._width * 0.5 + 15 116 self._folder_icon = bui.imagewidget( 117 parent=self._root_widget, 118 size=(40, 40), 119 position=(40, self._height - 117), 120 texture=self._folder_tex, 121 color=self._folder_color, 122 ) 123 self._path_text = bui.textwidget( 124 parent=self._root_widget, 125 position=(self._folder_center, self._height - 98), 126 size=(0, 0), 127 color=bui.app.ui_v1.title_color, 128 h_align='center', 129 v_align='center', 130 text=self._path, 131 maxwidth=self._width * 0.9, 132 ) 133 self._scrollwidget: bui.Widget | None = None 134 bui.containerwidget( 135 edit=self._root_widget, cancel_button=self._cancel_button 136 ) 137 self._set_path(path)
Inherited Members
- bauiv1._uitypes.Window
- get_root_widget