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