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 = 700 if uiscale is bui.UIScale.SMALL else 600 37 self._x_inset = x_inset = 50 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 2.23 58 if uiscale is bui.UIScale.SMALL 59 else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0 60 ), 61 stack_offset=( 62 (0, -35) if uiscale is bui.UIScale.SMALL else (0, 0) 63 ), 64 ), 65 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 bui.containerwidget(edit=self._root_widget, transition='out_right') 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 bui.containerwidget( 208 edit=self._root_widget, transition='out_right' 209 ) 210 if self._callback is not None: 211 self._callback(test_path) 212 else: 213 bui.getsound('error').play() 214 else: 215 print( 216 ( 217 'Error: FileSelectorWindow found non-file/dir:', 218 test_path, 219 ) 220 ) 221 except Exception: 222 logging.exception( 223 'Error in FileSelectorWindow._on_entry_activated().' 224 ) 225 226 if new_path is not None: 227 self._set_path(new_path) 228 229 class _RefreshThread(Thread): 230 def __init__( 231 self, path: str, callback: Callable[[list[str], str | None], Any] 232 ): 233 super().__init__() 234 self._callback = callback 235 self._path = path 236 237 @override 238 def run(self) -> None: 239 try: 240 starttime = time.time() 241 files = os.listdir(self._path) 242 duration = time.time() - starttime 243 min_time = 0.1 244 245 # Make sure this takes at least 1/10 second so the user 246 # has time to see the selection highlight. 247 if duration < min_time: 248 time.sleep(min_time - duration) 249 bui.pushcall( 250 bui.Call(self._callback, files, None), 251 from_other_thread=True, 252 ) 253 except Exception as exc: 254 # Ignore permission-denied. 255 if 'Errno 13' not in str(exc): 256 logging.exception('Error in fileselector refresh thread.') 257 nofiles: list[str] = [] 258 bui.pushcall( 259 bui.Call(self._callback, nofiles, str(exc)), 260 from_other_thread=True, 261 ) 262 263 def _set_path(self, path: str, add_to_recent: bool = True) -> None: 264 self._path = path 265 if add_to_recent: 266 self._recent_paths.append(path) 267 self._RefreshThread(path, self._refresh).start() 268 269 def _refresh(self, file_names: list[str], error: str | None) -> None: 270 # pylint: disable=too-many-statements 271 # pylint: disable=too-many-branches 272 # pylint: disable=too-many-locals 273 if not self._root_widget: 274 return 275 276 scrollwidget_selected = ( 277 self._scrollwidget is None 278 or self._root_widget.get_selected_child() == self._scrollwidget 279 ) 280 281 in_top_folder = self._path == self._base_path 282 hide_top_folder = in_top_folder and self._show_base_path is False 283 284 if hide_top_folder: 285 folder_name = '' 286 elif self._path == '/': 287 folder_name = '/' 288 else: 289 assert self._path is not None 290 folder_name = os.path.basename(self._path) 291 292 b_color = (0.6, 0.53, 0.63) 293 b_color_disabled = (0.65, 0.65, 0.65) 294 295 if len(self._recent_paths) < 2: 296 bui.buttonwidget( 297 edit=self._back_button, 298 color=b_color_disabled, 299 textcolor=(0.5, 0.5, 0.5), 300 ) 301 else: 302 bui.buttonwidget( 303 edit=self._back_button, 304 color=b_color, 305 textcolor=(0.75, 0.7, 0.8), 306 ) 307 308 max_str_width = 300.0 309 str_width = min( 310 max_str_width, 311 bui.get_string_width(folder_name, suppress_warning=True), 312 ) 313 bui.textwidget( 314 edit=self._path_text, text=folder_name, maxwidth=max_str_width 315 ) 316 bui.imagewidget( 317 edit=self._folder_icon, 318 position=( 319 self._folder_center - str_width * 0.5 - 40, 320 self._height - 117, 321 ), 322 opacity=0.0 if hide_top_folder else 1.0, 323 ) 324 325 if self._scrollwidget is not None: 326 self._scrollwidget.delete() 327 328 if self._use_folder_button is not None: 329 self._use_folder_button.delete() 330 bui.widget(edit=self._cancel_button, right_widget=self._back_button) 331 332 self._scrollwidget = bui.scrollwidget( 333 parent=self._root_widget, 334 position=( 335 (self._width - self._scroll_width) * 0.5, 336 self._height - self._scroll_height - 119, 337 ), 338 size=(self._scroll_width, self._scroll_height), 339 ) 340 341 if scrollwidget_selected: 342 bui.containerwidget( 343 edit=self._root_widget, selected_child=self._scrollwidget 344 ) 345 346 # show error case.. 347 if error is not None: 348 self._subcontainer = bui.containerwidget( 349 parent=self._scrollwidget, 350 size=(self._scroll_width, self._scroll_height), 351 background=False, 352 ) 353 bui.textwidget( 354 parent=self._subcontainer, 355 color=(1, 1, 0, 1), 356 text=error, 357 maxwidth=self._scroll_width * 0.9, 358 position=( 359 self._scroll_width * 0.48, 360 self._scroll_height * 0.57, 361 ), 362 size=(0, 0), 363 h_align='center', 364 v_align='center', 365 ) 366 367 else: 368 file_names = [f for f in file_names if not f.startswith('.')] 369 file_names.sort(key=lambda x: x[0].lower()) 370 371 entries = file_names 372 entry_height = 35 373 folder_entry_height = 100 374 show_folder_entry = False 375 show_use_folder_button = self._allow_folders and not in_top_folder 376 377 self._subcontainerheight = entry_height * len(entries) + ( 378 folder_entry_height if show_folder_entry else 0 379 ) 380 v = self._subcontainerheight - ( 381 folder_entry_height if show_folder_entry else 0 382 ) 383 384 self._subcontainer = bui.containerwidget( 385 parent=self._scrollwidget, 386 size=(self._scroll_width, self._subcontainerheight), 387 background=False, 388 ) 389 390 bui.containerwidget( 391 edit=self._scrollwidget, 392 claims_left_right=False, 393 claims_tab=False, 394 ) 395 bui.containerwidget( 396 edit=self._subcontainer, 397 claims_left_right=False, 398 claims_tab=False, 399 selection_loops=False, 400 print_list_exit_instructions=False, 401 ) 402 bui.widget(edit=self._subcontainer, up_widget=self._back_button) 403 404 if show_use_folder_button: 405 self._use_folder_button = btn = bui.buttonwidget( 406 parent=self._root_widget, 407 position=( 408 self._width - self._button_width - 35 - self._x_inset, 409 self._height - 67, 410 ), 411 size=(self._button_width, 50), 412 label=bui.Lstr( 413 resource=f'{self._r}.useThisFolderButtonText' 414 ), 415 on_activate_call=self._on_folder_entry_activated, 416 ) 417 bui.widget( 418 edit=btn, 419 left_widget=self._cancel_button, 420 down_widget=self._scrollwidget, 421 ) 422 bui.widget(edit=self._cancel_button, right_widget=btn) 423 bui.containerwidget(edit=self._root_widget, start_button=btn) 424 425 folder_icon_size = 35 426 for num, entry in enumerate(entries): 427 cnt = bui.containerwidget( 428 parent=self._subcontainer, 429 position=(0, v - entry_height), 430 size=(self._scroll_width, entry_height), 431 root_selectable=True, 432 background=False, 433 click_activate=True, 434 on_activate_call=bui.Call(self._on_entry_activated, entry), 435 ) 436 if num == 0: 437 bui.widget(edit=cnt, up_widget=self._back_button) 438 is_valid_file_path = self._is_valid_file_path(entry) 439 assert self._path is not None 440 is_dir = os.path.isdir(self._path + '/' + entry) 441 if is_dir: 442 bui.imagewidget( 443 parent=cnt, 444 size=(folder_icon_size, folder_icon_size), 445 position=( 446 10, 447 0.5 * entry_height - folder_icon_size * 0.5, 448 ), 449 draw_controller=cnt, 450 texture=self._folder_tex, 451 color=self._folder_color, 452 ) 453 else: 454 bui.imagewidget( 455 parent=cnt, 456 size=(folder_icon_size, folder_icon_size), 457 position=( 458 10, 459 0.5 * entry_height - folder_icon_size * 0.5, 460 ), 461 opacity=1.0 if is_valid_file_path else 0.5, 462 draw_controller=cnt, 463 texture=self._file_tex, 464 color=self._file_color, 465 ) 466 bui.textwidget( 467 parent=cnt, 468 draw_controller=cnt, 469 text=entry, 470 h_align='left', 471 v_align='center', 472 position=(10 + folder_icon_size * 1.05, entry_height * 0.5), 473 size=(0, 0), 474 maxwidth=self._scroll_width * 0.93 - 50, 475 color=( 476 (1, 1, 1, 1) 477 if (is_valid_file_path or is_dir) 478 else (0.5, 0.5, 0.5, 1) 479 ), 480 ) 481 v -= entry_height 482 483 def _is_valid_file_path(self, path: str) -> bool: 484 return any( 485 path.lower().endswith(ext) for ext in self._valid_file_extensions 486 ) 487 488 def _cancel(self) -> None: 489 # bui.containerwidget(edit=self._root_widget, transition='out_right') 490 self.main_window_back() 491 if self._callback is not None: 492 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 = 700 if uiscale is bui.UIScale.SMALL else 600 38 self._x_inset = x_inset = 50 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 2.23 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 bui.containerwidget(edit=self._root_widget, transition='out_right') 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 bui.containerwidget( 209 edit=self._root_widget, transition='out_right' 210 ) 211 if self._callback is not None: 212 self._callback(test_path) 213 else: 214 bui.getsound('error').play() 215 else: 216 print( 217 ( 218 'Error: FileSelectorWindow found non-file/dir:', 219 test_path, 220 ) 221 ) 222 except Exception: 223 logging.exception( 224 'Error in FileSelectorWindow._on_entry_activated().' 225 ) 226 227 if new_path is not None: 228 self._set_path(new_path) 229 230 class _RefreshThread(Thread): 231 def __init__( 232 self, path: str, callback: Callable[[list[str], str | None], Any] 233 ): 234 super().__init__() 235 self._callback = callback 236 self._path = path 237 238 @override 239 def run(self) -> None: 240 try: 241 starttime = time.time() 242 files = os.listdir(self._path) 243 duration = time.time() - starttime 244 min_time = 0.1 245 246 # Make sure this takes at least 1/10 second so the user 247 # has time to see the selection highlight. 248 if duration < min_time: 249 time.sleep(min_time - duration) 250 bui.pushcall( 251 bui.Call(self._callback, files, None), 252 from_other_thread=True, 253 ) 254 except Exception as exc: 255 # Ignore permission-denied. 256 if 'Errno 13' not in str(exc): 257 logging.exception('Error in fileselector refresh thread.') 258 nofiles: list[str] = [] 259 bui.pushcall( 260 bui.Call(self._callback, nofiles, str(exc)), 261 from_other_thread=True, 262 ) 263 264 def _set_path(self, path: str, add_to_recent: bool = True) -> None: 265 self._path = path 266 if add_to_recent: 267 self._recent_paths.append(path) 268 self._RefreshThread(path, self._refresh).start() 269 270 def _refresh(self, file_names: list[str], error: str | None) -> None: 271 # pylint: disable=too-many-statements 272 # pylint: disable=too-many-branches 273 # pylint: disable=too-many-locals 274 if not self._root_widget: 275 return 276 277 scrollwidget_selected = ( 278 self._scrollwidget is None 279 or self._root_widget.get_selected_child() == self._scrollwidget 280 ) 281 282 in_top_folder = self._path == self._base_path 283 hide_top_folder = in_top_folder and self._show_base_path is False 284 285 if hide_top_folder: 286 folder_name = '' 287 elif self._path == '/': 288 folder_name = '/' 289 else: 290 assert self._path is not None 291 folder_name = os.path.basename(self._path) 292 293 b_color = (0.6, 0.53, 0.63) 294 b_color_disabled = (0.65, 0.65, 0.65) 295 296 if len(self._recent_paths) < 2: 297 bui.buttonwidget( 298 edit=self._back_button, 299 color=b_color_disabled, 300 textcolor=(0.5, 0.5, 0.5), 301 ) 302 else: 303 bui.buttonwidget( 304 edit=self._back_button, 305 color=b_color, 306 textcolor=(0.75, 0.7, 0.8), 307 ) 308 309 max_str_width = 300.0 310 str_width = min( 311 max_str_width, 312 bui.get_string_width(folder_name, suppress_warning=True), 313 ) 314 bui.textwidget( 315 edit=self._path_text, text=folder_name, maxwidth=max_str_width 316 ) 317 bui.imagewidget( 318 edit=self._folder_icon, 319 position=( 320 self._folder_center - str_width * 0.5 - 40, 321 self._height - 117, 322 ), 323 opacity=0.0 if hide_top_folder else 1.0, 324 ) 325 326 if self._scrollwidget is not None: 327 self._scrollwidget.delete() 328 329 if self._use_folder_button is not None: 330 self._use_folder_button.delete() 331 bui.widget(edit=self._cancel_button, right_widget=self._back_button) 332 333 self._scrollwidget = bui.scrollwidget( 334 parent=self._root_widget, 335 position=( 336 (self._width - self._scroll_width) * 0.5, 337 self._height - self._scroll_height - 119, 338 ), 339 size=(self._scroll_width, self._scroll_height), 340 ) 341 342 if scrollwidget_selected: 343 bui.containerwidget( 344 edit=self._root_widget, selected_child=self._scrollwidget 345 ) 346 347 # show error case.. 348 if error is not None: 349 self._subcontainer = bui.containerwidget( 350 parent=self._scrollwidget, 351 size=(self._scroll_width, self._scroll_height), 352 background=False, 353 ) 354 bui.textwidget( 355 parent=self._subcontainer, 356 color=(1, 1, 0, 1), 357 text=error, 358 maxwidth=self._scroll_width * 0.9, 359 position=( 360 self._scroll_width * 0.48, 361 self._scroll_height * 0.57, 362 ), 363 size=(0, 0), 364 h_align='center', 365 v_align='center', 366 ) 367 368 else: 369 file_names = [f for f in file_names if not f.startswith('.')] 370 file_names.sort(key=lambda x: x[0].lower()) 371 372 entries = file_names 373 entry_height = 35 374 folder_entry_height = 100 375 show_folder_entry = False 376 show_use_folder_button = self._allow_folders and not in_top_folder 377 378 self._subcontainerheight = entry_height * len(entries) + ( 379 folder_entry_height if show_folder_entry else 0 380 ) 381 v = self._subcontainerheight - ( 382 folder_entry_height if show_folder_entry else 0 383 ) 384 385 self._subcontainer = bui.containerwidget( 386 parent=self._scrollwidget, 387 size=(self._scroll_width, self._subcontainerheight), 388 background=False, 389 ) 390 391 bui.containerwidget( 392 edit=self._scrollwidget, 393 claims_left_right=False, 394 claims_tab=False, 395 ) 396 bui.containerwidget( 397 edit=self._subcontainer, 398 claims_left_right=False, 399 claims_tab=False, 400 selection_loops=False, 401 print_list_exit_instructions=False, 402 ) 403 bui.widget(edit=self._subcontainer, up_widget=self._back_button) 404 405 if show_use_folder_button: 406 self._use_folder_button = btn = bui.buttonwidget( 407 parent=self._root_widget, 408 position=( 409 self._width - self._button_width - 35 - self._x_inset, 410 self._height - 67, 411 ), 412 size=(self._button_width, 50), 413 label=bui.Lstr( 414 resource=f'{self._r}.useThisFolderButtonText' 415 ), 416 on_activate_call=self._on_folder_entry_activated, 417 ) 418 bui.widget( 419 edit=btn, 420 left_widget=self._cancel_button, 421 down_widget=self._scrollwidget, 422 ) 423 bui.widget(edit=self._cancel_button, right_widget=btn) 424 bui.containerwidget(edit=self._root_widget, start_button=btn) 425 426 folder_icon_size = 35 427 for num, entry in enumerate(entries): 428 cnt = bui.containerwidget( 429 parent=self._subcontainer, 430 position=(0, v - entry_height), 431 size=(self._scroll_width, entry_height), 432 root_selectable=True, 433 background=False, 434 click_activate=True, 435 on_activate_call=bui.Call(self._on_entry_activated, entry), 436 ) 437 if num == 0: 438 bui.widget(edit=cnt, up_widget=self._back_button) 439 is_valid_file_path = self._is_valid_file_path(entry) 440 assert self._path is not None 441 is_dir = os.path.isdir(self._path + '/' + entry) 442 if is_dir: 443 bui.imagewidget( 444 parent=cnt, 445 size=(folder_icon_size, folder_icon_size), 446 position=( 447 10, 448 0.5 * entry_height - folder_icon_size * 0.5, 449 ), 450 draw_controller=cnt, 451 texture=self._folder_tex, 452 color=self._folder_color, 453 ) 454 else: 455 bui.imagewidget( 456 parent=cnt, 457 size=(folder_icon_size, folder_icon_size), 458 position=( 459 10, 460 0.5 * entry_height - folder_icon_size * 0.5, 461 ), 462 opacity=1.0 if is_valid_file_path else 0.5, 463 draw_controller=cnt, 464 texture=self._file_tex, 465 color=self._file_color, 466 ) 467 bui.textwidget( 468 parent=cnt, 469 draw_controller=cnt, 470 text=entry, 471 h_align='left', 472 v_align='center', 473 position=(10 + folder_icon_size * 1.05, entry_height * 0.5), 474 size=(0, 0), 475 maxwidth=self._scroll_width * 0.93 - 50, 476 color=( 477 (1, 1, 1, 1) 478 if (is_valid_file_path or is_dir) 479 else (0.5, 0.5, 0.5, 1) 480 ), 481 ) 482 v -= entry_height 483 484 def _is_valid_file_path(self, path: str) -> bool: 485 return any( 486 path.lower().endswith(ext) for ext in self._valid_file_extensions 487 ) 488 489 def _cancel(self) -> None: 490 # bui.containerwidget(edit=self._root_widget, transition='out_right') 491 self.main_window_back() 492 if self._callback is not None: 493 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 = 700 if uiscale is bui.UIScale.SMALL else 600 38 self._x_inset = x_inset = 50 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 2.23 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_close
- can_change_main_window
- main_window_back
- main_window_replace
- on_main_window_close
- bauiv1._uitypes.Window
- get_root_widget