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