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