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 ) 391 bui.containerwidget( 392 edit=self._subcontainer, 393 claims_left_right=False, 394 selection_loops=False, 395 print_list_exit_instructions=False, 396 ) 397 bui.widget(edit=self._subcontainer, up_widget=self._back_button) 398 399 if show_use_folder_button: 400 self._use_folder_button = btn = bui.buttonwidget( 401 parent=self._root_widget, 402 position=( 403 self._width - self._button_width - 35 - self._x_inset, 404 self._height - 67, 405 ), 406 size=(self._button_width, 50), 407 label=bui.Lstr( 408 resource=f'{self._r}.useThisFolderButtonText' 409 ), 410 on_activate_call=self._on_folder_entry_activated, 411 ) 412 bui.widget( 413 edit=btn, 414 left_widget=self._cancel_button, 415 down_widget=self._scrollwidget, 416 ) 417 bui.widget(edit=self._cancel_button, right_widget=btn) 418 bui.containerwidget(edit=self._root_widget, start_button=btn) 419 420 folder_icon_size = 35 421 for num, entry in enumerate(entries): 422 cnt = bui.containerwidget( 423 parent=self._subcontainer, 424 position=(0, v - entry_height), 425 size=(self._scroll_width, entry_height), 426 root_selectable=True, 427 background=False, 428 click_activate=True, 429 on_activate_call=bui.Call(self._on_entry_activated, entry), 430 ) 431 if num == 0: 432 bui.widget(edit=cnt, up_widget=self._back_button) 433 is_valid_file_path = self._is_valid_file_path(entry) 434 assert self._path is not None 435 is_dir = os.path.isdir(self._path + '/' + entry) 436 if is_dir: 437 bui.imagewidget( 438 parent=cnt, 439 size=(folder_icon_size, folder_icon_size), 440 position=( 441 10, 442 0.5 * entry_height - folder_icon_size * 0.5, 443 ), 444 draw_controller=cnt, 445 texture=self._folder_tex, 446 color=self._folder_color, 447 ) 448 else: 449 bui.imagewidget( 450 parent=cnt, 451 size=(folder_icon_size, folder_icon_size), 452 position=( 453 10, 454 0.5 * entry_height - folder_icon_size * 0.5, 455 ), 456 opacity=1.0 if is_valid_file_path else 0.5, 457 draw_controller=cnt, 458 texture=self._file_tex, 459 color=self._file_color, 460 ) 461 bui.textwidget( 462 parent=cnt, 463 draw_controller=cnt, 464 text=entry, 465 h_align='left', 466 v_align='center', 467 position=(10 + folder_icon_size * 1.05, entry_height * 0.5), 468 size=(0, 0), 469 maxwidth=self._scroll_width * 0.93 - 50, 470 color=( 471 (1, 1, 1, 1) 472 if (is_valid_file_path or is_dir) 473 else (0.5, 0.5, 0.5, 1) 474 ), 475 ) 476 v -= entry_height 477 478 def _is_valid_file_path(self, path: str) -> bool: 479 return any( 480 path.lower().endswith(ext) for ext in self._valid_file_extensions 481 ) 482 483 def _cancel(self) -> None: 484 self.main_window_back() 485 if self._callback is not None: 486 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 ) 392 bui.containerwidget( 393 edit=self._subcontainer, 394 claims_left_right=False, 395 selection_loops=False, 396 print_list_exit_instructions=False, 397 ) 398 bui.widget(edit=self._subcontainer, up_widget=self._back_button) 399 400 if show_use_folder_button: 401 self._use_folder_button = btn = bui.buttonwidget( 402 parent=self._root_widget, 403 position=( 404 self._width - self._button_width - 35 - self._x_inset, 405 self._height - 67, 406 ), 407 size=(self._button_width, 50), 408 label=bui.Lstr( 409 resource=f'{self._r}.useThisFolderButtonText' 410 ), 411 on_activate_call=self._on_folder_entry_activated, 412 ) 413 bui.widget( 414 edit=btn, 415 left_widget=self._cancel_button, 416 down_widget=self._scrollwidget, 417 ) 418 bui.widget(edit=self._cancel_button, right_widget=btn) 419 bui.containerwidget(edit=self._root_widget, start_button=btn) 420 421 folder_icon_size = 35 422 for num, entry in enumerate(entries): 423 cnt = bui.containerwidget( 424 parent=self._subcontainer, 425 position=(0, v - entry_height), 426 size=(self._scroll_width, entry_height), 427 root_selectable=True, 428 background=False, 429 click_activate=True, 430 on_activate_call=bui.Call(self._on_entry_activated, entry), 431 ) 432 if num == 0: 433 bui.widget(edit=cnt, up_widget=self._back_button) 434 is_valid_file_path = self._is_valid_file_path(entry) 435 assert self._path is not None 436 is_dir = os.path.isdir(self._path + '/' + entry) 437 if is_dir: 438 bui.imagewidget( 439 parent=cnt, 440 size=(folder_icon_size, folder_icon_size), 441 position=( 442 10, 443 0.5 * entry_height - folder_icon_size * 0.5, 444 ), 445 draw_controller=cnt, 446 texture=self._folder_tex, 447 color=self._folder_color, 448 ) 449 else: 450 bui.imagewidget( 451 parent=cnt, 452 size=(folder_icon_size, folder_icon_size), 453 position=( 454 10, 455 0.5 * entry_height - folder_icon_size * 0.5, 456 ), 457 opacity=1.0 if is_valid_file_path else 0.5, 458 draw_controller=cnt, 459 texture=self._file_tex, 460 color=self._file_color, 461 ) 462 bui.textwidget( 463 parent=cnt, 464 draw_controller=cnt, 465 text=entry, 466 h_align='left', 467 v_align='center', 468 position=(10 + folder_icon_size * 1.05, entry_height * 0.5), 469 size=(0, 0), 470 maxwidth=self._scroll_width * 0.93 - 50, 471 color=( 472 (1, 1, 1, 1) 473 if (is_valid_file_path or is_dir) 474 else (0.5, 0.5, 0.5, 1) 475 ), 476 ) 477 v -= entry_height 478 479 def _is_valid_file_path(self, path: str) -> bool: 480 return any( 481 path.lower().endswith(ext) for ext in self._valid_file_extensions 482 ) 483 484 def _cancel(self) -> None: 485 self.main_window_back() 486 if self._callback is not None: 487 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.