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