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