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