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

Window for selecting files.

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