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

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:
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        )

Return a WindowState to recreate this window, if supported.

Inherited Members
bauiv1._uitypes.MainWindow
main_window_back_state
main_window_close
can_change_main_window
main_window_back
main_window_replace
on_main_window_close
bauiv1._uitypes.Window
get_root_widget