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