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