bauiv1lib.fileselector

UI functionality for selecting files.

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

Window for selecting files.

FileSelectorWindow( path: str, callback: Optional[Callable[[str | None], Any]] = None, *, show_base_path: bool = True, valid_file_extensions: Optional[Sequence[str]] = None, allow_folders: bool = False, transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None)
 23    def __init__(
 24        self,
 25        path: str,
 26        callback: Callable[[str | None], Any] | None = None,
 27        *,
 28        show_base_path: bool = True,
 29        valid_file_extensions: Sequence[str] | None = None,
 30        allow_folders: bool = False,
 31        transition: str | None = 'in_right',
 32        origin_widget: bui.Widget | None = None,
 33    ):
 34        if valid_file_extensions is None:
 35            valid_file_extensions = []
 36        assert bui.app.classic is not None
 37        uiscale = bui.app.ui_v1.uiscale
 38        self._width = 850 if uiscale is bui.UIScale.SMALL else 600
 39        self._x_inset = x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
 40        self._height = 365 if uiscale is bui.UIScale.SMALL else 418
 41        self._callback = callback
 42        self._base_path = path
 43        self._path: str | None = None
 44        self._recent_paths: list[str] = []
 45        self._show_base_path = show_base_path
 46        self._valid_file_extensions = [
 47            '.' + ext for ext in valid_file_extensions
 48        ]
 49        self._allow_folders = allow_folders
 50        self._subcontainer: bui.Widget | None = None
 51        self._subcontainerheight: float | None = None
 52        self._scroll_width = self._width - (80 + 2 * x_inset)
 53        self._scroll_height = self._height - 170
 54        self._r = 'fileSelectorWindow'
 55        super().__init__(
 56            root_widget=bui.containerwidget(
 57                size=(self._width, self._height),
 58                scale=(
 59                    1.93
 60                    if uiscale is bui.UIScale.SMALL
 61                    else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
 62                ),
 63                stack_offset=(
 64                    (0, -35) if uiscale is bui.UIScale.SMALL else (0, 0)
 65                ),
 66            ),
 67            transition=transition,
 68            origin_widget=origin_widget,
 69        )
 70        bui.textwidget(
 71            parent=self._root_widget,
 72            position=(self._width * 0.5, self._height - 42),
 73            size=(0, 0),
 74            color=bui.app.ui_v1.title_color,
 75            h_align='center',
 76            v_align='center',
 77            text=(
 78                bui.Lstr(resource=f'{self._r}.titleFolderText')
 79                if (allow_folders and not valid_file_extensions)
 80                else (
 81                    bui.Lstr(resource=f'{self._r}.titleFileText')
 82                    if not allow_folders
 83                    else bui.Lstr(resource=f'{self._r}.titleFileFolderText')
 84                )
 85            ),
 86            maxwidth=210,
 87        )
 88
 89        self._button_width = 146
 90        self._cancel_button = bui.buttonwidget(
 91            parent=self._root_widget,
 92            position=(35 + x_inset, self._height - 67),
 93            autoselect=True,
 94            size=(self._button_width, 50),
 95            label=bui.Lstr(resource='cancelText'),
 96            on_activate_call=self._cancel,
 97        )
 98        bui.widget(edit=self._cancel_button, left_widget=self._cancel_button)
 99
100        b_color = (0.6, 0.53, 0.63)
101
102        self._back_button = bui.buttonwidget(
103            parent=self._root_widget,
104            button_type='square',
105            position=(43 + x_inset, self._height - 113),
106            color=b_color,
107            textcolor=(0.75, 0.7, 0.8),
108            enable_sound=False,
109            size=(55, 35),
110            label=bui.charstr(bui.SpecialChar.LEFT_ARROW),
111            on_activate_call=self._on_back_press,
112        )
113
114        self._folder_tex = bui.gettexture('folder')
115        self._folder_color = (1.1, 0.8, 0.2)
116        self._file_tex = bui.gettexture('file')
117        self._file_color = (1, 1, 1)
118        self._use_folder_button: bui.Widget | None = None
119        self._folder_center = self._width * 0.5 + 15
120        self._folder_icon = bui.imagewidget(
121            parent=self._root_widget,
122            size=(40, 40),
123            position=(40, self._height - 117),
124            texture=self._folder_tex,
125            color=self._folder_color,
126        )
127        self._path_text = bui.textwidget(
128            parent=self._root_widget,
129            position=(self._folder_center, self._height - 98),
130            size=(0, 0),
131            color=bui.app.ui_v1.title_color,
132            h_align='center',
133            v_align='center',
134            text=self._path,
135            maxwidth=self._width * 0.9,
136        )
137        self._scrollwidget: bui.Widget | None = None
138        bui.containerwidget(
139            edit=self._root_widget, cancel_button=self._cancel_button
140        )
141        self._set_path(path)

Create a MainWindow given a root widget and transition info.

Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
143    @override
144    def get_main_window_state(self) -> bui.MainWindowState:
145        # Support recreating our window for back/refresh purposes.
146        cls = type(self)
147
148        # Pull everything out of self here. If we do it below in the lambda,
149        # we'll keep self alive which is bad.
150        path = self._base_path
151        callback = self._callback
152        show_base_path = self._show_base_path
153        valid_file_extensions = self._valid_file_extensions
154        allow_folders = self._allow_folders
155
156        return bui.BasicMainWindowState(
157            create_call=lambda transition, origin_widget: cls(
158                transition=transition,
159                origin_widget=origin_widget,
160                path=path,
161                callback=callback,
162                show_base_path=show_base_path,
163                valid_file_extensions=valid_file_extensions,
164                allow_folders=allow_folders,
165            )
166        )

Return a WindowState to recreate this window, if supported.