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 = 850 if uiscale is bui.UIScale.SMALL else 600
 37        self._x_inset = x_inset = 100 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                    1.93
 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        if self._callback is not None:
179            assert self._path is not None
180            self._callback(self._path)
181
182    def _on_entry_activated(self, entry: str) -> None:
183        # pylint: disable=too-many-branches
184        new_path = None
185        try:
186            assert self._path is not None
187            if entry == '..':
188                chunks = self._path.split('/')
189                if len(chunks) > 1:
190                    new_path = '/'.join(chunks[:-1])
191                    if new_path == '':
192                        new_path = '/'
193                else:
194                    bui.getsound('error').play()
195            else:
196                if self._path == '/':
197                    test_path = self._path + entry
198                else:
199                    test_path = self._path + '/' + entry
200                if os.path.isdir(test_path):
201                    bui.getsound('swish').play()
202                    new_path = test_path
203                elif os.path.isfile(test_path):
204                    if self._is_valid_file_path(test_path):
205                        bui.getsound('swish').play()
206                        if self._callback is not None:
207                            self._callback(test_path)
208                    else:
209                        bui.getsound('error').play()
210                else:
211                    print(
212                        (
213                            'Error: FileSelectorWindow found non-file/dir:',
214                            test_path,
215                        )
216                    )
217        except Exception:
218            logging.exception(
219                'Error in FileSelectorWindow._on_entry_activated().'
220            )
221
222        if new_path is not None:
223            self._set_path(new_path)
224
225    class _RefreshThread(Thread):
226        def __init__(
227            self, path: str, callback: Callable[[list[str], str | None], Any]
228        ):
229            super().__init__()
230            self._callback = callback
231            self._path = path
232
233        @override
234        def run(self) -> None:
235            try:
236                starttime = time.time()
237                files = os.listdir(self._path)
238                duration = time.time() - starttime
239                min_time = 0.1
240
241                # Make sure this takes at least 1/10 second so the user
242                # has time to see the selection highlight.
243                if duration < min_time:
244                    time.sleep(min_time - duration)
245                bui.pushcall(
246                    bui.Call(self._callback, files, None),
247                    from_other_thread=True,
248                )
249            except Exception as exc:
250                # Ignore permission-denied.
251                if 'Errno 13' not in str(exc):
252                    logging.exception('Error in fileselector refresh thread.')
253                nofiles: list[str] = []
254                bui.pushcall(
255                    bui.Call(self._callback, nofiles, str(exc)),
256                    from_other_thread=True,
257                )
258
259    def _set_path(self, path: str, add_to_recent: bool = True) -> None:
260        self._path = path
261        if add_to_recent:
262            self._recent_paths.append(path)
263        self._RefreshThread(path, self._refresh).start()
264
265    def _refresh(self, file_names: list[str], error: str | None) -> None:
266        # pylint: disable=too-many-statements
267        # pylint: disable=too-many-branches
268        # pylint: disable=too-many-locals
269        if not self._root_widget:
270            return
271
272        scrollwidget_selected = (
273            self._scrollwidget is None
274            or self._root_widget.get_selected_child() == self._scrollwidget
275        )
276
277        in_top_folder = self._path == self._base_path
278        hide_top_folder = in_top_folder and self._show_base_path is False
279
280        if hide_top_folder:
281            folder_name = ''
282        elif self._path == '/':
283            folder_name = '/'
284        else:
285            assert self._path is not None
286            folder_name = os.path.basename(self._path)
287
288        b_color = (0.6, 0.53, 0.63)
289        b_color_disabled = (0.65, 0.65, 0.65)
290
291        if len(self._recent_paths) < 2:
292            bui.buttonwidget(
293                edit=self._back_button,
294                color=b_color_disabled,
295                textcolor=(0.5, 0.5, 0.5),
296            )
297        else:
298            bui.buttonwidget(
299                edit=self._back_button,
300                color=b_color,
301                textcolor=(0.75, 0.7, 0.8),
302            )
303
304        max_str_width = 300.0
305        str_width = min(
306            max_str_width,
307            bui.get_string_width(folder_name, suppress_warning=True),
308        )
309        bui.textwidget(
310            edit=self._path_text, text=folder_name, maxwidth=max_str_width
311        )
312        bui.imagewidget(
313            edit=self._folder_icon,
314            position=(
315                self._folder_center - str_width * 0.5 - 40,
316                self._height - 117,
317            ),
318            opacity=0.0 if hide_top_folder else 1.0,
319        )
320
321        if self._scrollwidget is not None:
322            self._scrollwidget.delete()
323
324        if self._use_folder_button is not None:
325            self._use_folder_button.delete()
326            bui.widget(edit=self._cancel_button, right_widget=self._back_button)
327
328        self._scrollwidget = bui.scrollwidget(
329            parent=self._root_widget,
330            position=(
331                (self._width - self._scroll_width) * 0.5,
332                self._height - self._scroll_height - 119,
333            ),
334            size=(self._scroll_width, self._scroll_height),
335        )
336
337        if scrollwidget_selected:
338            bui.containerwidget(
339                edit=self._root_widget, selected_child=self._scrollwidget
340            )
341
342        # show error case..
343        if error is not None:
344            self._subcontainer = bui.containerwidget(
345                parent=self._scrollwidget,
346                size=(self._scroll_width, self._scroll_height),
347                background=False,
348            )
349            bui.textwidget(
350                parent=self._subcontainer,
351                color=(1, 1, 0, 1),
352                text=error,
353                maxwidth=self._scroll_width * 0.9,
354                position=(
355                    self._scroll_width * 0.48,
356                    self._scroll_height * 0.57,
357                ),
358                size=(0, 0),
359                h_align='center',
360                v_align='center',
361            )
362
363        else:
364            file_names = [f for f in file_names if not f.startswith('.')]
365            file_names.sort(key=lambda x: x[0].lower())
366
367            entries = file_names
368            entry_height = 35
369            folder_entry_height = 100
370            show_folder_entry = False
371            show_use_folder_button = self._allow_folders and not in_top_folder
372
373            self._subcontainerheight = entry_height * len(entries) + (
374                folder_entry_height if show_folder_entry else 0
375            )
376            v = self._subcontainerheight - (
377                folder_entry_height if show_folder_entry else 0
378            )
379
380            self._subcontainer = bui.containerwidget(
381                parent=self._scrollwidget,
382                size=(self._scroll_width, self._subcontainerheight),
383                background=False,
384            )
385
386            bui.containerwidget(
387                edit=self._scrollwidget,
388                claims_left_right=False,
389                claims_tab=False,
390            )
391            bui.containerwidget(
392                edit=self._subcontainer,
393                claims_left_right=False,
394                claims_tab=False,
395                selection_loops=False,
396                print_list_exit_instructions=False,
397            )
398            bui.widget(edit=self._subcontainer, up_widget=self._back_button)
399
400            if show_use_folder_button:
401                self._use_folder_button = btn = bui.buttonwidget(
402                    parent=self._root_widget,
403                    position=(
404                        self._width - self._button_width - 35 - self._x_inset,
405                        self._height - 67,
406                    ),
407                    size=(self._button_width, 50),
408                    label=bui.Lstr(
409                        resource=f'{self._r}.useThisFolderButtonText'
410                    ),
411                    on_activate_call=self._on_folder_entry_activated,
412                )
413                bui.widget(
414                    edit=btn,
415                    left_widget=self._cancel_button,
416                    down_widget=self._scrollwidget,
417                )
418                bui.widget(edit=self._cancel_button, right_widget=btn)
419                bui.containerwidget(edit=self._root_widget, start_button=btn)
420
421            folder_icon_size = 35
422            for num, entry in enumerate(entries):
423                cnt = bui.containerwidget(
424                    parent=self._subcontainer,
425                    position=(0, v - entry_height),
426                    size=(self._scroll_width, entry_height),
427                    root_selectable=True,
428                    background=False,
429                    click_activate=True,
430                    on_activate_call=bui.Call(self._on_entry_activated, entry),
431                )
432                if num == 0:
433                    bui.widget(edit=cnt, up_widget=self._back_button)
434                is_valid_file_path = self._is_valid_file_path(entry)
435                assert self._path is not None
436                is_dir = os.path.isdir(self._path + '/' + entry)
437                if is_dir:
438                    bui.imagewidget(
439                        parent=cnt,
440                        size=(folder_icon_size, folder_icon_size),
441                        position=(
442                            10,
443                            0.5 * entry_height - folder_icon_size * 0.5,
444                        ),
445                        draw_controller=cnt,
446                        texture=self._folder_tex,
447                        color=self._folder_color,
448                    )
449                else:
450                    bui.imagewidget(
451                        parent=cnt,
452                        size=(folder_icon_size, folder_icon_size),
453                        position=(
454                            10,
455                            0.5 * entry_height - folder_icon_size * 0.5,
456                        ),
457                        opacity=1.0 if is_valid_file_path else 0.5,
458                        draw_controller=cnt,
459                        texture=self._file_tex,
460                        color=self._file_color,
461                    )
462                bui.textwidget(
463                    parent=cnt,
464                    draw_controller=cnt,
465                    text=entry,
466                    h_align='left',
467                    v_align='center',
468                    position=(10 + folder_icon_size * 1.05, entry_height * 0.5),
469                    size=(0, 0),
470                    maxwidth=self._scroll_width * 0.93 - 50,
471                    color=(
472                        (1, 1, 1, 1)
473                        if (is_valid_file_path or is_dir)
474                        else (0.5, 0.5, 0.5, 1)
475                    ),
476                )
477                v -= entry_height
478
479    def _is_valid_file_path(self, path: str) -> bool:
480        return any(
481            path.lower().endswith(ext) for ext in self._valid_file_extensions
482        )
483
484    def _cancel(self) -> None:
485        self.main_window_back()
486        if self._callback is not None:
487            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 = 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)

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

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_is_top_level
main_window_close
main_window_has_control
main_window_back
main_window_replace
on_main_window_close
bauiv1._uitypes.Window
get_root_widget