bauiv1lib.settings.graphics

Provides UI for graphics settings.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Provides UI for graphics settings."""
  4
  5from __future__ import annotations
  6
  7from typing import TYPE_CHECKING, cast
  8
  9from bauiv1lib.popup import PopupMenu
 10from bauiv1lib.config import ConfigCheckBox
 11import bauiv1 as bui
 12
 13if TYPE_CHECKING:
 14    from typing import Any
 15
 16
 17class GraphicsSettingsWindow(bui.Window):
 18    """Window for graphics settings."""
 19
 20    def __init__(
 21        self,
 22        transition: str = 'in_right',
 23        origin_widget: bui.Widget | None = None,
 24    ):
 25        # pylint: disable=too-many-locals
 26        # pylint: disable=too-many-branches
 27        # pylint: disable=too-many-statements
 28
 29        # if they provided an origin-widget, scale up from that
 30        scale_origin: tuple[float, float] | None
 31        if origin_widget is not None:
 32            self._transition_out = 'out_scale'
 33            scale_origin = origin_widget.get_screen_space_center()
 34            transition = 'in_scale'
 35        else:
 36            self._transition_out = 'out_right'
 37            scale_origin = None
 38
 39        self._r = 'graphicsSettingsWindow'
 40        app = bui.app
 41        assert app.classic is not None
 42
 43        spacing = 32
 44        self._have_selected_child = False
 45        uiscale = app.ui_v1.uiscale
 46        width = 450.0
 47        height = 302.0
 48        self._max_fps_dirty = False
 49        self._last_max_fps_set_time = bui.apptime()
 50        self._last_max_fps_str = ''
 51
 52        self._show_fullscreen = False
 53        fullscreen_spacing_top = spacing * 0.2
 54        fullscreen_spacing = spacing * 1.2
 55        if bui.fullscreen_control_available():
 56            self._show_fullscreen = True
 57            height += fullscreen_spacing + fullscreen_spacing_top
 58
 59        show_vsync = bui.supports_vsync()
 60        show_tv_mode = not bui.app.env.vr
 61
 62        show_max_fps = bui.supports_max_fps()
 63        if show_max_fps:
 64            height += 50
 65
 66        show_resolution = True
 67        if app.env.vr:
 68            show_resolution = (
 69                app.classic.platform == 'android'
 70                and app.classic.subplatform == 'cardboard'
 71            )
 72
 73        assert bui.app.classic is not None
 74        uiscale = bui.app.ui_v1.uiscale
 75        base_scale = (
 76            2.0
 77            if uiscale is bui.UIScale.SMALL
 78            else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0
 79        )
 80        popup_menu_scale = base_scale * 1.2
 81        v = height - 50
 82        v -= spacing * 1.15
 83        super().__init__(
 84            root_widget=bui.containerwidget(
 85                size=(width, height),
 86                transition=transition,
 87                scale_origin_stack_offset=scale_origin,
 88                scale=base_scale,
 89                stack_offset=(
 90                    (0, -30) if uiscale is bui.UIScale.SMALL else (0, 0)
 91                ),
 92            )
 93        )
 94
 95        back_button = bui.buttonwidget(
 96            parent=self._root_widget,
 97            position=(35, height - 50),
 98            # size=(120, 60),
 99            size=(60, 60),
100            scale=0.8,
101            text_scale=1.2,
102            autoselect=True,
103            label=bui.charstr(bui.SpecialChar.BACK),
104            button_type='backSmall',
105            on_activate_call=self._back,
106        )
107
108        bui.containerwidget(edit=self._root_widget, cancel_button=back_button)
109
110        bui.textwidget(
111            parent=self._root_widget,
112            position=(0, height - 44),
113            size=(width, 25),
114            text=bui.Lstr(resource=self._r + '.titleText'),
115            color=bui.app.ui_v1.title_color,
116            h_align='center',
117            v_align='top',
118        )
119
120        self._fullscreen_checkbox: bui.Widget | None = None
121        if self._show_fullscreen:
122            v -= fullscreen_spacing_top
123            # Fullscreen control does not necessarily talk to the
124            # app config so we have to wrangle it manually instead of
125            # using a config-checkbox.
126            label = bui.Lstr(resource=f'{self._r}.fullScreenText')
127
128            # Show keyboard shortcut alongside the control if they
129            # provide one.
130            shortcut = bui.fullscreen_control_key_shortcut()
131            if shortcut is not None:
132                label = bui.Lstr(
133                    value='$(NAME) [$(SHORTCUT)]',
134                    subs=[('$(NAME)', label), ('$(SHORTCUT)', shortcut)],
135                )
136            self._fullscreen_checkbox = bui.checkboxwidget(
137                parent=self._root_widget,
138                position=(100, v),
139                value=bui.fullscreen_control_get(),
140                on_value_change_call=bui.fullscreen_control_set,
141                maxwidth=250,
142                size=(300, 30),
143                text=label,
144            )
145
146            if not self._have_selected_child:
147                bui.containerwidget(
148                    edit=self._root_widget,
149                    selected_child=self._fullscreen_checkbox,
150                )
151                self._have_selected_child = True
152            v -= fullscreen_spacing
153
154        self._selected_color = (0.5, 1, 0.5, 1)
155        self._unselected_color = (0.7, 0.7, 0.7, 1)
156
157        # Quality
158        bui.textwidget(
159            parent=self._root_widget,
160            position=(60, v),
161            size=(160, 25),
162            text=bui.Lstr(resource=self._r + '.visualsText'),
163            color=bui.app.ui_v1.heading_color,
164            scale=0.65,
165            maxwidth=150,
166            h_align='center',
167            v_align='center',
168        )
169        PopupMenu(
170            parent=self._root_widget,
171            position=(60, v - 50),
172            width=150,
173            scale=popup_menu_scale,
174            choices=['Auto', 'Higher', 'High', 'Medium', 'Low'],
175            choices_disabled=(
176                ['Higher', 'High']
177                if bui.get_max_graphics_quality() == 'Medium'
178                else []
179            ),
180            choices_display=[
181                bui.Lstr(resource='autoText'),
182                bui.Lstr(resource=self._r + '.higherText'),
183                bui.Lstr(resource=self._r + '.highText'),
184                bui.Lstr(resource=self._r + '.mediumText'),
185                bui.Lstr(resource=self._r + '.lowText'),
186            ],
187            current_choice=bui.app.config.resolve('Graphics Quality'),
188            on_value_change_call=self._set_quality,
189        )
190
191        # Texture controls
192        bui.textwidget(
193            parent=self._root_widget,
194            position=(230, v),
195            size=(160, 25),
196            text=bui.Lstr(resource=self._r + '.texturesText'),
197            color=bui.app.ui_v1.heading_color,
198            scale=0.65,
199            maxwidth=150,
200            h_align='center',
201            v_align='center',
202        )
203        textures_popup = PopupMenu(
204            parent=self._root_widget,
205            position=(230, v - 50),
206            width=150,
207            scale=popup_menu_scale,
208            choices=['Auto', 'High', 'Medium', 'Low'],
209            choices_display=[
210                bui.Lstr(resource='autoText'),
211                bui.Lstr(resource=self._r + '.highText'),
212                bui.Lstr(resource=self._r + '.mediumText'),
213                bui.Lstr(resource=self._r + '.lowText'),
214            ],
215            current_choice=bui.app.config.resolve('Texture Quality'),
216            on_value_change_call=self._set_textures,
217        )
218        if bui.app.ui_v1.use_toolbars:
219            bui.widget(
220                edit=textures_popup.get_button(),
221                right_widget=bui.get_special_widget('party_button'),
222            )
223        v -= 80
224
225        h_offs = 0
226
227        resolution_popup: PopupMenu | None = None
228
229        if show_resolution:
230            bui.textwidget(
231                parent=self._root_widget,
232                position=(h_offs + 60, v),
233                size=(160, 25),
234                text=bui.Lstr(resource=self._r + '.resolutionText'),
235                color=bui.app.ui_v1.heading_color,
236                scale=0.65,
237                maxwidth=150,
238                h_align='center',
239                v_align='center',
240            )
241
242            # On standard android we have 'Auto', 'Native', and a few
243            # HD standards.
244            if app.classic.platform == 'android':
245                # on cardboard/daydream android we have a few
246                # render-target-scale options
247                if app.classic.subplatform == 'cardboard':
248                    rawval = bui.app.config.resolve('GVR Render Target Scale')
249                    current_res_cardboard = (
250                        str(min(100, max(10, int(round(rawval * 100.0))))) + '%'
251                    )
252                    resolution_popup = PopupMenu(
253                        parent=self._root_widget,
254                        position=(h_offs + 60, v - 50),
255                        width=120,
256                        scale=popup_menu_scale,
257                        choices=['100%', '75%', '50%', '35%'],
258                        current_choice=current_res_cardboard,
259                        on_value_change_call=self._set_gvr_render_target_scale,
260                    )
261                else:
262                    native_res = bui.get_display_resolution()
263                    assert native_res is not None
264                    choices = ['Auto', 'Native']
265                    choices_display = [
266                        bui.Lstr(resource='autoText'),
267                        bui.Lstr(resource='nativeText'),
268                    ]
269                    for res in [1440, 1080, 960, 720, 480]:
270                        if native_res[1] >= res:
271                            res_str = f'{res}p'
272                            choices.append(res_str)
273                            choices_display.append(bui.Lstr(value=res_str))
274                    current_res_android = bui.app.config.resolve(
275                        'Resolution (Android)'
276                    )
277                    resolution_popup = PopupMenu(
278                        parent=self._root_widget,
279                        position=(h_offs + 60, v - 50),
280                        width=120,
281                        scale=popup_menu_scale,
282                        choices=choices,
283                        choices_display=choices_display,
284                        current_choice=current_res_android,
285                        on_value_change_call=self._set_android_res,
286                    )
287            else:
288                # If we're on a system that doesn't allow setting resolution,
289                # set pixel-scale instead.
290                current_res = bui.get_display_resolution()
291                if current_res is None:
292                    rawval = bui.app.config.resolve('Screen Pixel Scale')
293                    current_res2 = (
294                        str(min(100, max(10, int(round(rawval * 100.0))))) + '%'
295                    )
296                    resolution_popup = PopupMenu(
297                        parent=self._root_widget,
298                        position=(h_offs + 60, v - 50),
299                        width=120,
300                        scale=popup_menu_scale,
301                        choices=['100%', '88%', '75%', '63%', '50%'],
302                        current_choice=current_res2,
303                        on_value_change_call=self._set_pixel_scale,
304                    )
305                else:
306                    raise RuntimeError(
307                        'obsolete code path; discrete resolutions'
308                        ' no longer supported'
309                    )
310        if resolution_popup is not None:
311            bui.widget(
312                edit=resolution_popup.get_button(),
313                left_widget=back_button,
314            )
315
316        vsync_popup: PopupMenu | None = None
317        if show_vsync:
318            bui.textwidget(
319                parent=self._root_widget,
320                position=(230, v),
321                size=(160, 25),
322                text=bui.Lstr(resource=self._r + '.verticalSyncText'),
323                color=bui.app.ui_v1.heading_color,
324                scale=0.65,
325                maxwidth=150,
326                h_align='center',
327                v_align='center',
328            )
329            vsync_popup = PopupMenu(
330                parent=self._root_widget,
331                position=(230, v - 50),
332                width=150,
333                scale=popup_menu_scale,
334                choices=['Auto', 'Always', 'Never'],
335                choices_display=[
336                    bui.Lstr(resource='autoText'),
337                    bui.Lstr(resource=self._r + '.alwaysText'),
338                    bui.Lstr(resource=self._r + '.neverText'),
339                ],
340                current_choice=bui.app.config.resolve('Vertical Sync'),
341                on_value_change_call=self._set_vsync,
342            )
343            if resolution_popup is not None:
344                bui.widget(
345                    edit=vsync_popup.get_button(),
346                    left_widget=resolution_popup.get_button(),
347                )
348
349        if resolution_popup is not None and vsync_popup is not None:
350            bui.widget(
351                edit=resolution_popup.get_button(),
352                right_widget=vsync_popup.get_button(),
353            )
354
355        v -= 90
356        self._max_fps_text: bui.Widget | None = None
357        if show_max_fps:
358            v -= 5
359            bui.textwidget(
360                parent=self._root_widget,
361                position=(155, v + 10),
362                size=(0, 0),
363                text=bui.Lstr(resource=self._r + '.maxFPSText'),
364                color=bui.app.ui_v1.heading_color,
365                scale=0.9,
366                maxwidth=90,
367                h_align='right',
368                v_align='center',
369            )
370
371            max_fps_str = str(bui.app.config.resolve('Max FPS'))
372            self._last_max_fps_str = max_fps_str
373            self._max_fps_text = bui.textwidget(
374                parent=self._root_widget,
375                position=(170, v - 5),
376                size=(105, 30),
377                text=max_fps_str,
378                max_chars=5,
379                editable=True,
380                h_align='left',
381                v_align='center',
382                on_return_press_call=self._on_max_fps_return_press,
383            )
384            v -= 45
385
386        if self._max_fps_text is not None and resolution_popup is not None:
387            bui.widget(
388                edit=resolution_popup.get_button(),
389                down_widget=self._max_fps_text,
390            )
391            bui.widget(
392                edit=self._max_fps_text,
393                up_widget=resolution_popup.get_button(),
394            )
395
396        fpsc = ConfigCheckBox(
397            parent=self._root_widget,
398            position=(69, v - 6),
399            size=(210, 30),
400            scale=0.86,
401            configkey='Show FPS',
402            displayname=bui.Lstr(resource=self._r + '.showFPSText'),
403            maxwidth=130,
404        )
405        if self._max_fps_text is not None:
406            bui.widget(
407                edit=self._max_fps_text,
408                down_widget=fpsc.widget,
409            )
410            bui.widget(
411                edit=fpsc.widget,
412                up_widget=self._max_fps_text,
413            )
414
415        if show_tv_mode:
416            tvc = ConfigCheckBox(
417                parent=self._root_widget,
418                position=(240, v - 6),
419                size=(210, 30),
420                scale=0.86,
421                configkey='TV Border',
422                displayname=bui.Lstr(resource=self._r + '.tvBorderText'),
423                maxwidth=130,
424            )
425            bui.widget(edit=fpsc.widget, right_widget=tvc.widget)
426            bui.widget(edit=tvc.widget, left_widget=fpsc.widget)
427
428        v -= spacing
429
430        # Make a timer to update our controls in case the config changes
431        # under us.
432        self._update_timer = bui.AppTimer(
433            0.25, bui.WeakCall(self._update_controls), repeat=True
434        )
435
436    def _back(self) -> None:
437        from bauiv1lib.settings import allsettings
438
439        # no-op if our underlying widget is dead or on its way out.
440        if not self._root_widget or self._root_widget.transitioning_out:
441            return
442
443        # Applying max-fps takes a few moments. Apply if it hasn't been
444        # yet.
445        self._apply_max_fps()
446
447        bui.containerwidget(
448            edit=self._root_widget, transition=self._transition_out
449        )
450        assert bui.app.classic is not None
451        bui.app.ui_v1.set_main_menu_window(
452            allsettings.AllSettingsWindow(
453                transition='in_left'
454            ).get_root_widget(),
455            from_window=self._root_widget,
456        )
457
458    def _set_quality(self, quality: str) -> None:
459        cfg = bui.app.config
460        cfg['Graphics Quality'] = quality
461        cfg.apply_and_commit()
462
463    def _set_textures(self, val: str) -> None:
464        cfg = bui.app.config
465        cfg['Texture Quality'] = val
466        cfg.apply_and_commit()
467
468    def _set_android_res(self, val: str) -> None:
469        cfg = bui.app.config
470        cfg['Resolution (Android)'] = val
471        cfg.apply_and_commit()
472
473    def _set_pixel_scale(self, res: str) -> None:
474        cfg = bui.app.config
475        cfg['Screen Pixel Scale'] = float(res[:-1]) / 100.0
476        cfg.apply_and_commit()
477
478    def _set_gvr_render_target_scale(self, res: str) -> None:
479        cfg = bui.app.config
480        cfg['GVR Render Target Scale'] = float(res[:-1]) / 100.0
481        cfg.apply_and_commit()
482
483    def _set_vsync(self, val: str) -> None:
484        cfg = bui.app.config
485        cfg['Vertical Sync'] = val
486        cfg.apply_and_commit()
487
488    def _on_max_fps_return_press(self) -> None:
489        self._apply_max_fps()
490        bui.containerwidget(
491            edit=self._root_widget, selected_child=cast(bui.Widget, 0)
492        )
493
494    def _apply_max_fps(self) -> None:
495        if not self._max_fps_dirty or not self._max_fps_text:
496            return
497
498        val: Any = bui.textwidget(query=self._max_fps_text)
499        assert isinstance(val, str)
500        # If there's a broken value, replace it with the default.
501        try:
502            ival = int(val)
503        except ValueError:
504            ival = bui.app.config.default_value('Max FPS')
505        assert isinstance(ival, int)
506
507        # Clamp to reasonable limits (allow -1 to mean no max).
508        if ival != -1:
509            ival = max(10, ival)
510            ival = min(99999, ival)
511
512        # Store it to the config.
513        cfg = bui.app.config
514        cfg['Max FPS'] = ival
515        cfg.apply_and_commit()
516
517        # Update the display if we changed the value.
518        if str(ival) != val:
519            bui.textwidget(edit=self._max_fps_text, text=str(ival))
520
521        self._max_fps_dirty = False
522
523    def _update_controls(self) -> None:
524        if self._max_fps_text is not None:
525            # Keep track of when the max-fps value changes. Once it
526            # remains stable for a few moments, apply it.
527            val: Any = bui.textwidget(query=self._max_fps_text)
528            assert isinstance(val, str)
529            if val != self._last_max_fps_str:
530                # Oop; it changed. Note the time and the fact that we'll
531                # need to apply it at some point.
532                self._max_fps_dirty = True
533                self._last_max_fps_str = val
534                self._last_max_fps_set_time = bui.apptime()
535            else:
536                # If its been stable long enough, apply it.
537                if (
538                    self._max_fps_dirty
539                    and bui.apptime() - self._last_max_fps_set_time > 1.0
540                ):
541                    self._apply_max_fps()
542
543        if self._show_fullscreen:
544            # Keep the fullscreen checkbox up to date with the current value.
545            bui.checkboxwidget(
546                edit=self._fullscreen_checkbox,
547                value=bui.fullscreen_control_get(),
548            )
class GraphicsSettingsWindow(bauiv1._uitypes.Window):
 18class GraphicsSettingsWindow(bui.Window):
 19    """Window for graphics settings."""
 20
 21    def __init__(
 22        self,
 23        transition: str = 'in_right',
 24        origin_widget: bui.Widget | None = None,
 25    ):
 26        # pylint: disable=too-many-locals
 27        # pylint: disable=too-many-branches
 28        # pylint: disable=too-many-statements
 29
 30        # if they provided an origin-widget, scale up from that
 31        scale_origin: tuple[float, float] | None
 32        if origin_widget is not None:
 33            self._transition_out = 'out_scale'
 34            scale_origin = origin_widget.get_screen_space_center()
 35            transition = 'in_scale'
 36        else:
 37            self._transition_out = 'out_right'
 38            scale_origin = None
 39
 40        self._r = 'graphicsSettingsWindow'
 41        app = bui.app
 42        assert app.classic is not None
 43
 44        spacing = 32
 45        self._have_selected_child = False
 46        uiscale = app.ui_v1.uiscale
 47        width = 450.0
 48        height = 302.0
 49        self._max_fps_dirty = False
 50        self._last_max_fps_set_time = bui.apptime()
 51        self._last_max_fps_str = ''
 52
 53        self._show_fullscreen = False
 54        fullscreen_spacing_top = spacing * 0.2
 55        fullscreen_spacing = spacing * 1.2
 56        if bui.fullscreen_control_available():
 57            self._show_fullscreen = True
 58            height += fullscreen_spacing + fullscreen_spacing_top
 59
 60        show_vsync = bui.supports_vsync()
 61        show_tv_mode = not bui.app.env.vr
 62
 63        show_max_fps = bui.supports_max_fps()
 64        if show_max_fps:
 65            height += 50
 66
 67        show_resolution = True
 68        if app.env.vr:
 69            show_resolution = (
 70                app.classic.platform == 'android'
 71                and app.classic.subplatform == 'cardboard'
 72            )
 73
 74        assert bui.app.classic is not None
 75        uiscale = bui.app.ui_v1.uiscale
 76        base_scale = (
 77            2.0
 78            if uiscale is bui.UIScale.SMALL
 79            else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0
 80        )
 81        popup_menu_scale = base_scale * 1.2
 82        v = height - 50
 83        v -= spacing * 1.15
 84        super().__init__(
 85            root_widget=bui.containerwidget(
 86                size=(width, height),
 87                transition=transition,
 88                scale_origin_stack_offset=scale_origin,
 89                scale=base_scale,
 90                stack_offset=(
 91                    (0, -30) if uiscale is bui.UIScale.SMALL else (0, 0)
 92                ),
 93            )
 94        )
 95
 96        back_button = bui.buttonwidget(
 97            parent=self._root_widget,
 98            position=(35, height - 50),
 99            # size=(120, 60),
100            size=(60, 60),
101            scale=0.8,
102            text_scale=1.2,
103            autoselect=True,
104            label=bui.charstr(bui.SpecialChar.BACK),
105            button_type='backSmall',
106            on_activate_call=self._back,
107        )
108
109        bui.containerwidget(edit=self._root_widget, cancel_button=back_button)
110
111        bui.textwidget(
112            parent=self._root_widget,
113            position=(0, height - 44),
114            size=(width, 25),
115            text=bui.Lstr(resource=self._r + '.titleText'),
116            color=bui.app.ui_v1.title_color,
117            h_align='center',
118            v_align='top',
119        )
120
121        self._fullscreen_checkbox: bui.Widget | None = None
122        if self._show_fullscreen:
123            v -= fullscreen_spacing_top
124            # Fullscreen control does not necessarily talk to the
125            # app config so we have to wrangle it manually instead of
126            # using a config-checkbox.
127            label = bui.Lstr(resource=f'{self._r}.fullScreenText')
128
129            # Show keyboard shortcut alongside the control if they
130            # provide one.
131            shortcut = bui.fullscreen_control_key_shortcut()
132            if shortcut is not None:
133                label = bui.Lstr(
134                    value='$(NAME) [$(SHORTCUT)]',
135                    subs=[('$(NAME)', label), ('$(SHORTCUT)', shortcut)],
136                )
137            self._fullscreen_checkbox = bui.checkboxwidget(
138                parent=self._root_widget,
139                position=(100, v),
140                value=bui.fullscreen_control_get(),
141                on_value_change_call=bui.fullscreen_control_set,
142                maxwidth=250,
143                size=(300, 30),
144                text=label,
145            )
146
147            if not self._have_selected_child:
148                bui.containerwidget(
149                    edit=self._root_widget,
150                    selected_child=self._fullscreen_checkbox,
151                )
152                self._have_selected_child = True
153            v -= fullscreen_spacing
154
155        self._selected_color = (0.5, 1, 0.5, 1)
156        self._unselected_color = (0.7, 0.7, 0.7, 1)
157
158        # Quality
159        bui.textwidget(
160            parent=self._root_widget,
161            position=(60, v),
162            size=(160, 25),
163            text=bui.Lstr(resource=self._r + '.visualsText'),
164            color=bui.app.ui_v1.heading_color,
165            scale=0.65,
166            maxwidth=150,
167            h_align='center',
168            v_align='center',
169        )
170        PopupMenu(
171            parent=self._root_widget,
172            position=(60, v - 50),
173            width=150,
174            scale=popup_menu_scale,
175            choices=['Auto', 'Higher', 'High', 'Medium', 'Low'],
176            choices_disabled=(
177                ['Higher', 'High']
178                if bui.get_max_graphics_quality() == 'Medium'
179                else []
180            ),
181            choices_display=[
182                bui.Lstr(resource='autoText'),
183                bui.Lstr(resource=self._r + '.higherText'),
184                bui.Lstr(resource=self._r + '.highText'),
185                bui.Lstr(resource=self._r + '.mediumText'),
186                bui.Lstr(resource=self._r + '.lowText'),
187            ],
188            current_choice=bui.app.config.resolve('Graphics Quality'),
189            on_value_change_call=self._set_quality,
190        )
191
192        # Texture controls
193        bui.textwidget(
194            parent=self._root_widget,
195            position=(230, v),
196            size=(160, 25),
197            text=bui.Lstr(resource=self._r + '.texturesText'),
198            color=bui.app.ui_v1.heading_color,
199            scale=0.65,
200            maxwidth=150,
201            h_align='center',
202            v_align='center',
203        )
204        textures_popup = PopupMenu(
205            parent=self._root_widget,
206            position=(230, v - 50),
207            width=150,
208            scale=popup_menu_scale,
209            choices=['Auto', 'High', 'Medium', 'Low'],
210            choices_display=[
211                bui.Lstr(resource='autoText'),
212                bui.Lstr(resource=self._r + '.highText'),
213                bui.Lstr(resource=self._r + '.mediumText'),
214                bui.Lstr(resource=self._r + '.lowText'),
215            ],
216            current_choice=bui.app.config.resolve('Texture Quality'),
217            on_value_change_call=self._set_textures,
218        )
219        if bui.app.ui_v1.use_toolbars:
220            bui.widget(
221                edit=textures_popup.get_button(),
222                right_widget=bui.get_special_widget('party_button'),
223            )
224        v -= 80
225
226        h_offs = 0
227
228        resolution_popup: PopupMenu | None = None
229
230        if show_resolution:
231            bui.textwidget(
232                parent=self._root_widget,
233                position=(h_offs + 60, v),
234                size=(160, 25),
235                text=bui.Lstr(resource=self._r + '.resolutionText'),
236                color=bui.app.ui_v1.heading_color,
237                scale=0.65,
238                maxwidth=150,
239                h_align='center',
240                v_align='center',
241            )
242
243            # On standard android we have 'Auto', 'Native', and a few
244            # HD standards.
245            if app.classic.platform == 'android':
246                # on cardboard/daydream android we have a few
247                # render-target-scale options
248                if app.classic.subplatform == 'cardboard':
249                    rawval = bui.app.config.resolve('GVR Render Target Scale')
250                    current_res_cardboard = (
251                        str(min(100, max(10, int(round(rawval * 100.0))))) + '%'
252                    )
253                    resolution_popup = PopupMenu(
254                        parent=self._root_widget,
255                        position=(h_offs + 60, v - 50),
256                        width=120,
257                        scale=popup_menu_scale,
258                        choices=['100%', '75%', '50%', '35%'],
259                        current_choice=current_res_cardboard,
260                        on_value_change_call=self._set_gvr_render_target_scale,
261                    )
262                else:
263                    native_res = bui.get_display_resolution()
264                    assert native_res is not None
265                    choices = ['Auto', 'Native']
266                    choices_display = [
267                        bui.Lstr(resource='autoText'),
268                        bui.Lstr(resource='nativeText'),
269                    ]
270                    for res in [1440, 1080, 960, 720, 480]:
271                        if native_res[1] >= res:
272                            res_str = f'{res}p'
273                            choices.append(res_str)
274                            choices_display.append(bui.Lstr(value=res_str))
275                    current_res_android = bui.app.config.resolve(
276                        'Resolution (Android)'
277                    )
278                    resolution_popup = PopupMenu(
279                        parent=self._root_widget,
280                        position=(h_offs + 60, v - 50),
281                        width=120,
282                        scale=popup_menu_scale,
283                        choices=choices,
284                        choices_display=choices_display,
285                        current_choice=current_res_android,
286                        on_value_change_call=self._set_android_res,
287                    )
288            else:
289                # If we're on a system that doesn't allow setting resolution,
290                # set pixel-scale instead.
291                current_res = bui.get_display_resolution()
292                if current_res is None:
293                    rawval = bui.app.config.resolve('Screen Pixel Scale')
294                    current_res2 = (
295                        str(min(100, max(10, int(round(rawval * 100.0))))) + '%'
296                    )
297                    resolution_popup = PopupMenu(
298                        parent=self._root_widget,
299                        position=(h_offs + 60, v - 50),
300                        width=120,
301                        scale=popup_menu_scale,
302                        choices=['100%', '88%', '75%', '63%', '50%'],
303                        current_choice=current_res2,
304                        on_value_change_call=self._set_pixel_scale,
305                    )
306                else:
307                    raise RuntimeError(
308                        'obsolete code path; discrete resolutions'
309                        ' no longer supported'
310                    )
311        if resolution_popup is not None:
312            bui.widget(
313                edit=resolution_popup.get_button(),
314                left_widget=back_button,
315            )
316
317        vsync_popup: PopupMenu | None = None
318        if show_vsync:
319            bui.textwidget(
320                parent=self._root_widget,
321                position=(230, v),
322                size=(160, 25),
323                text=bui.Lstr(resource=self._r + '.verticalSyncText'),
324                color=bui.app.ui_v1.heading_color,
325                scale=0.65,
326                maxwidth=150,
327                h_align='center',
328                v_align='center',
329            )
330            vsync_popup = PopupMenu(
331                parent=self._root_widget,
332                position=(230, v - 50),
333                width=150,
334                scale=popup_menu_scale,
335                choices=['Auto', 'Always', 'Never'],
336                choices_display=[
337                    bui.Lstr(resource='autoText'),
338                    bui.Lstr(resource=self._r + '.alwaysText'),
339                    bui.Lstr(resource=self._r + '.neverText'),
340                ],
341                current_choice=bui.app.config.resolve('Vertical Sync'),
342                on_value_change_call=self._set_vsync,
343            )
344            if resolution_popup is not None:
345                bui.widget(
346                    edit=vsync_popup.get_button(),
347                    left_widget=resolution_popup.get_button(),
348                )
349
350        if resolution_popup is not None and vsync_popup is not None:
351            bui.widget(
352                edit=resolution_popup.get_button(),
353                right_widget=vsync_popup.get_button(),
354            )
355
356        v -= 90
357        self._max_fps_text: bui.Widget | None = None
358        if show_max_fps:
359            v -= 5
360            bui.textwidget(
361                parent=self._root_widget,
362                position=(155, v + 10),
363                size=(0, 0),
364                text=bui.Lstr(resource=self._r + '.maxFPSText'),
365                color=bui.app.ui_v1.heading_color,
366                scale=0.9,
367                maxwidth=90,
368                h_align='right',
369                v_align='center',
370            )
371
372            max_fps_str = str(bui.app.config.resolve('Max FPS'))
373            self._last_max_fps_str = max_fps_str
374            self._max_fps_text = bui.textwidget(
375                parent=self._root_widget,
376                position=(170, v - 5),
377                size=(105, 30),
378                text=max_fps_str,
379                max_chars=5,
380                editable=True,
381                h_align='left',
382                v_align='center',
383                on_return_press_call=self._on_max_fps_return_press,
384            )
385            v -= 45
386
387        if self._max_fps_text is not None and resolution_popup is not None:
388            bui.widget(
389                edit=resolution_popup.get_button(),
390                down_widget=self._max_fps_text,
391            )
392            bui.widget(
393                edit=self._max_fps_text,
394                up_widget=resolution_popup.get_button(),
395            )
396
397        fpsc = ConfigCheckBox(
398            parent=self._root_widget,
399            position=(69, v - 6),
400            size=(210, 30),
401            scale=0.86,
402            configkey='Show FPS',
403            displayname=bui.Lstr(resource=self._r + '.showFPSText'),
404            maxwidth=130,
405        )
406        if self._max_fps_text is not None:
407            bui.widget(
408                edit=self._max_fps_text,
409                down_widget=fpsc.widget,
410            )
411            bui.widget(
412                edit=fpsc.widget,
413                up_widget=self._max_fps_text,
414            )
415
416        if show_tv_mode:
417            tvc = ConfigCheckBox(
418                parent=self._root_widget,
419                position=(240, v - 6),
420                size=(210, 30),
421                scale=0.86,
422                configkey='TV Border',
423                displayname=bui.Lstr(resource=self._r + '.tvBorderText'),
424                maxwidth=130,
425            )
426            bui.widget(edit=fpsc.widget, right_widget=tvc.widget)
427            bui.widget(edit=tvc.widget, left_widget=fpsc.widget)
428
429        v -= spacing
430
431        # Make a timer to update our controls in case the config changes
432        # under us.
433        self._update_timer = bui.AppTimer(
434            0.25, bui.WeakCall(self._update_controls), repeat=True
435        )
436
437    def _back(self) -> None:
438        from bauiv1lib.settings import allsettings
439
440        # no-op if our underlying widget is dead or on its way out.
441        if not self._root_widget or self._root_widget.transitioning_out:
442            return
443
444        # Applying max-fps takes a few moments. Apply if it hasn't been
445        # yet.
446        self._apply_max_fps()
447
448        bui.containerwidget(
449            edit=self._root_widget, transition=self._transition_out
450        )
451        assert bui.app.classic is not None
452        bui.app.ui_v1.set_main_menu_window(
453            allsettings.AllSettingsWindow(
454                transition='in_left'
455            ).get_root_widget(),
456            from_window=self._root_widget,
457        )
458
459    def _set_quality(self, quality: str) -> None:
460        cfg = bui.app.config
461        cfg['Graphics Quality'] = quality
462        cfg.apply_and_commit()
463
464    def _set_textures(self, val: str) -> None:
465        cfg = bui.app.config
466        cfg['Texture Quality'] = val
467        cfg.apply_and_commit()
468
469    def _set_android_res(self, val: str) -> None:
470        cfg = bui.app.config
471        cfg['Resolution (Android)'] = val
472        cfg.apply_and_commit()
473
474    def _set_pixel_scale(self, res: str) -> None:
475        cfg = bui.app.config
476        cfg['Screen Pixel Scale'] = float(res[:-1]) / 100.0
477        cfg.apply_and_commit()
478
479    def _set_gvr_render_target_scale(self, res: str) -> None:
480        cfg = bui.app.config
481        cfg['GVR Render Target Scale'] = float(res[:-1]) / 100.0
482        cfg.apply_and_commit()
483
484    def _set_vsync(self, val: str) -> None:
485        cfg = bui.app.config
486        cfg['Vertical Sync'] = val
487        cfg.apply_and_commit()
488
489    def _on_max_fps_return_press(self) -> None:
490        self._apply_max_fps()
491        bui.containerwidget(
492            edit=self._root_widget, selected_child=cast(bui.Widget, 0)
493        )
494
495    def _apply_max_fps(self) -> None:
496        if not self._max_fps_dirty or not self._max_fps_text:
497            return
498
499        val: Any = bui.textwidget(query=self._max_fps_text)
500        assert isinstance(val, str)
501        # If there's a broken value, replace it with the default.
502        try:
503            ival = int(val)
504        except ValueError:
505            ival = bui.app.config.default_value('Max FPS')
506        assert isinstance(ival, int)
507
508        # Clamp to reasonable limits (allow -1 to mean no max).
509        if ival != -1:
510            ival = max(10, ival)
511            ival = min(99999, ival)
512
513        # Store it to the config.
514        cfg = bui.app.config
515        cfg['Max FPS'] = ival
516        cfg.apply_and_commit()
517
518        # Update the display if we changed the value.
519        if str(ival) != val:
520            bui.textwidget(edit=self._max_fps_text, text=str(ival))
521
522        self._max_fps_dirty = False
523
524    def _update_controls(self) -> None:
525        if self._max_fps_text is not None:
526            # Keep track of when the max-fps value changes. Once it
527            # remains stable for a few moments, apply it.
528            val: Any = bui.textwidget(query=self._max_fps_text)
529            assert isinstance(val, str)
530            if val != self._last_max_fps_str:
531                # Oop; it changed. Note the time and the fact that we'll
532                # need to apply it at some point.
533                self._max_fps_dirty = True
534                self._last_max_fps_str = val
535                self._last_max_fps_set_time = bui.apptime()
536            else:
537                # If its been stable long enough, apply it.
538                if (
539                    self._max_fps_dirty
540                    and bui.apptime() - self._last_max_fps_set_time > 1.0
541                ):
542                    self._apply_max_fps()
543
544        if self._show_fullscreen:
545            # Keep the fullscreen checkbox up to date with the current value.
546            bui.checkboxwidget(
547                edit=self._fullscreen_checkbox,
548                value=bui.fullscreen_control_get(),
549            )

Window for graphics settings.

GraphicsSettingsWindow( transition: str = 'in_right', origin_widget: _bauiv1.Widget | None = None)
 21    def __init__(
 22        self,
 23        transition: str = 'in_right',
 24        origin_widget: bui.Widget | None = None,
 25    ):
 26        # pylint: disable=too-many-locals
 27        # pylint: disable=too-many-branches
 28        # pylint: disable=too-many-statements
 29
 30        # if they provided an origin-widget, scale up from that
 31        scale_origin: tuple[float, float] | None
 32        if origin_widget is not None:
 33            self._transition_out = 'out_scale'
 34            scale_origin = origin_widget.get_screen_space_center()
 35            transition = 'in_scale'
 36        else:
 37            self._transition_out = 'out_right'
 38            scale_origin = None
 39
 40        self._r = 'graphicsSettingsWindow'
 41        app = bui.app
 42        assert app.classic is not None
 43
 44        spacing = 32
 45        self._have_selected_child = False
 46        uiscale = app.ui_v1.uiscale
 47        width = 450.0
 48        height = 302.0
 49        self._max_fps_dirty = False
 50        self._last_max_fps_set_time = bui.apptime()
 51        self._last_max_fps_str = ''
 52
 53        self._show_fullscreen = False
 54        fullscreen_spacing_top = spacing * 0.2
 55        fullscreen_spacing = spacing * 1.2
 56        if bui.fullscreen_control_available():
 57            self._show_fullscreen = True
 58            height += fullscreen_spacing + fullscreen_spacing_top
 59
 60        show_vsync = bui.supports_vsync()
 61        show_tv_mode = not bui.app.env.vr
 62
 63        show_max_fps = bui.supports_max_fps()
 64        if show_max_fps:
 65            height += 50
 66
 67        show_resolution = True
 68        if app.env.vr:
 69            show_resolution = (
 70                app.classic.platform == 'android'
 71                and app.classic.subplatform == 'cardboard'
 72            )
 73
 74        assert bui.app.classic is not None
 75        uiscale = bui.app.ui_v1.uiscale
 76        base_scale = (
 77            2.0
 78            if uiscale is bui.UIScale.SMALL
 79            else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0
 80        )
 81        popup_menu_scale = base_scale * 1.2
 82        v = height - 50
 83        v -= spacing * 1.15
 84        super().__init__(
 85            root_widget=bui.containerwidget(
 86                size=(width, height),
 87                transition=transition,
 88                scale_origin_stack_offset=scale_origin,
 89                scale=base_scale,
 90                stack_offset=(
 91                    (0, -30) if uiscale is bui.UIScale.SMALL else (0, 0)
 92                ),
 93            )
 94        )
 95
 96        back_button = bui.buttonwidget(
 97            parent=self._root_widget,
 98            position=(35, height - 50),
 99            # size=(120, 60),
100            size=(60, 60),
101            scale=0.8,
102            text_scale=1.2,
103            autoselect=True,
104            label=bui.charstr(bui.SpecialChar.BACK),
105            button_type='backSmall',
106            on_activate_call=self._back,
107        )
108
109        bui.containerwidget(edit=self._root_widget, cancel_button=back_button)
110
111        bui.textwidget(
112            parent=self._root_widget,
113            position=(0, height - 44),
114            size=(width, 25),
115            text=bui.Lstr(resource=self._r + '.titleText'),
116            color=bui.app.ui_v1.title_color,
117            h_align='center',
118            v_align='top',
119        )
120
121        self._fullscreen_checkbox: bui.Widget | None = None
122        if self._show_fullscreen:
123            v -= fullscreen_spacing_top
124            # Fullscreen control does not necessarily talk to the
125            # app config so we have to wrangle it manually instead of
126            # using a config-checkbox.
127            label = bui.Lstr(resource=f'{self._r}.fullScreenText')
128
129            # Show keyboard shortcut alongside the control if they
130            # provide one.
131            shortcut = bui.fullscreen_control_key_shortcut()
132            if shortcut is not None:
133                label = bui.Lstr(
134                    value='$(NAME) [$(SHORTCUT)]',
135                    subs=[('$(NAME)', label), ('$(SHORTCUT)', shortcut)],
136                )
137            self._fullscreen_checkbox = bui.checkboxwidget(
138                parent=self._root_widget,
139                position=(100, v),
140                value=bui.fullscreen_control_get(),
141                on_value_change_call=bui.fullscreen_control_set,
142                maxwidth=250,
143                size=(300, 30),
144                text=label,
145            )
146
147            if not self._have_selected_child:
148                bui.containerwidget(
149                    edit=self._root_widget,
150                    selected_child=self._fullscreen_checkbox,
151                )
152                self._have_selected_child = True
153            v -= fullscreen_spacing
154
155        self._selected_color = (0.5, 1, 0.5, 1)
156        self._unselected_color = (0.7, 0.7, 0.7, 1)
157
158        # Quality
159        bui.textwidget(
160            parent=self._root_widget,
161            position=(60, v),
162            size=(160, 25),
163            text=bui.Lstr(resource=self._r + '.visualsText'),
164            color=bui.app.ui_v1.heading_color,
165            scale=0.65,
166            maxwidth=150,
167            h_align='center',
168            v_align='center',
169        )
170        PopupMenu(
171            parent=self._root_widget,
172            position=(60, v - 50),
173            width=150,
174            scale=popup_menu_scale,
175            choices=['Auto', 'Higher', 'High', 'Medium', 'Low'],
176            choices_disabled=(
177                ['Higher', 'High']
178                if bui.get_max_graphics_quality() == 'Medium'
179                else []
180            ),
181            choices_display=[
182                bui.Lstr(resource='autoText'),
183                bui.Lstr(resource=self._r + '.higherText'),
184                bui.Lstr(resource=self._r + '.highText'),
185                bui.Lstr(resource=self._r + '.mediumText'),
186                bui.Lstr(resource=self._r + '.lowText'),
187            ],
188            current_choice=bui.app.config.resolve('Graphics Quality'),
189            on_value_change_call=self._set_quality,
190        )
191
192        # Texture controls
193        bui.textwidget(
194            parent=self._root_widget,
195            position=(230, v),
196            size=(160, 25),
197            text=bui.Lstr(resource=self._r + '.texturesText'),
198            color=bui.app.ui_v1.heading_color,
199            scale=0.65,
200            maxwidth=150,
201            h_align='center',
202            v_align='center',
203        )
204        textures_popup = PopupMenu(
205            parent=self._root_widget,
206            position=(230, v - 50),
207            width=150,
208            scale=popup_menu_scale,
209            choices=['Auto', 'High', 'Medium', 'Low'],
210            choices_display=[
211                bui.Lstr(resource='autoText'),
212                bui.Lstr(resource=self._r + '.highText'),
213                bui.Lstr(resource=self._r + '.mediumText'),
214                bui.Lstr(resource=self._r + '.lowText'),
215            ],
216            current_choice=bui.app.config.resolve('Texture Quality'),
217            on_value_change_call=self._set_textures,
218        )
219        if bui.app.ui_v1.use_toolbars:
220            bui.widget(
221                edit=textures_popup.get_button(),
222                right_widget=bui.get_special_widget('party_button'),
223            )
224        v -= 80
225
226        h_offs = 0
227
228        resolution_popup: PopupMenu | None = None
229
230        if show_resolution:
231            bui.textwidget(
232                parent=self._root_widget,
233                position=(h_offs + 60, v),
234                size=(160, 25),
235                text=bui.Lstr(resource=self._r + '.resolutionText'),
236                color=bui.app.ui_v1.heading_color,
237                scale=0.65,
238                maxwidth=150,
239                h_align='center',
240                v_align='center',
241            )
242
243            # On standard android we have 'Auto', 'Native', and a few
244            # HD standards.
245            if app.classic.platform == 'android':
246                # on cardboard/daydream android we have a few
247                # render-target-scale options
248                if app.classic.subplatform == 'cardboard':
249                    rawval = bui.app.config.resolve('GVR Render Target Scale')
250                    current_res_cardboard = (
251                        str(min(100, max(10, int(round(rawval * 100.0))))) + '%'
252                    )
253                    resolution_popup = PopupMenu(
254                        parent=self._root_widget,
255                        position=(h_offs + 60, v - 50),
256                        width=120,
257                        scale=popup_menu_scale,
258                        choices=['100%', '75%', '50%', '35%'],
259                        current_choice=current_res_cardboard,
260                        on_value_change_call=self._set_gvr_render_target_scale,
261                    )
262                else:
263                    native_res = bui.get_display_resolution()
264                    assert native_res is not None
265                    choices = ['Auto', 'Native']
266                    choices_display = [
267                        bui.Lstr(resource='autoText'),
268                        bui.Lstr(resource='nativeText'),
269                    ]
270                    for res in [1440, 1080, 960, 720, 480]:
271                        if native_res[1] >= res:
272                            res_str = f'{res}p'
273                            choices.append(res_str)
274                            choices_display.append(bui.Lstr(value=res_str))
275                    current_res_android = bui.app.config.resolve(
276                        'Resolution (Android)'
277                    )
278                    resolution_popup = PopupMenu(
279                        parent=self._root_widget,
280                        position=(h_offs + 60, v - 50),
281                        width=120,
282                        scale=popup_menu_scale,
283                        choices=choices,
284                        choices_display=choices_display,
285                        current_choice=current_res_android,
286                        on_value_change_call=self._set_android_res,
287                    )
288            else:
289                # If we're on a system that doesn't allow setting resolution,
290                # set pixel-scale instead.
291                current_res = bui.get_display_resolution()
292                if current_res is None:
293                    rawval = bui.app.config.resolve('Screen Pixel Scale')
294                    current_res2 = (
295                        str(min(100, max(10, int(round(rawval * 100.0))))) + '%'
296                    )
297                    resolution_popup = PopupMenu(
298                        parent=self._root_widget,
299                        position=(h_offs + 60, v - 50),
300                        width=120,
301                        scale=popup_menu_scale,
302                        choices=['100%', '88%', '75%', '63%', '50%'],
303                        current_choice=current_res2,
304                        on_value_change_call=self._set_pixel_scale,
305                    )
306                else:
307                    raise RuntimeError(
308                        'obsolete code path; discrete resolutions'
309                        ' no longer supported'
310                    )
311        if resolution_popup is not None:
312            bui.widget(
313                edit=resolution_popup.get_button(),
314                left_widget=back_button,
315            )
316
317        vsync_popup: PopupMenu | None = None
318        if show_vsync:
319            bui.textwidget(
320                parent=self._root_widget,
321                position=(230, v),
322                size=(160, 25),
323                text=bui.Lstr(resource=self._r + '.verticalSyncText'),
324                color=bui.app.ui_v1.heading_color,
325                scale=0.65,
326                maxwidth=150,
327                h_align='center',
328                v_align='center',
329            )
330            vsync_popup = PopupMenu(
331                parent=self._root_widget,
332                position=(230, v - 50),
333                width=150,
334                scale=popup_menu_scale,
335                choices=['Auto', 'Always', 'Never'],
336                choices_display=[
337                    bui.Lstr(resource='autoText'),
338                    bui.Lstr(resource=self._r + '.alwaysText'),
339                    bui.Lstr(resource=self._r + '.neverText'),
340                ],
341                current_choice=bui.app.config.resolve('Vertical Sync'),
342                on_value_change_call=self._set_vsync,
343            )
344            if resolution_popup is not None:
345                bui.widget(
346                    edit=vsync_popup.get_button(),
347                    left_widget=resolution_popup.get_button(),
348                )
349
350        if resolution_popup is not None and vsync_popup is not None:
351            bui.widget(
352                edit=resolution_popup.get_button(),
353                right_widget=vsync_popup.get_button(),
354            )
355
356        v -= 90
357        self._max_fps_text: bui.Widget | None = None
358        if show_max_fps:
359            v -= 5
360            bui.textwidget(
361                parent=self._root_widget,
362                position=(155, v + 10),
363                size=(0, 0),
364                text=bui.Lstr(resource=self._r + '.maxFPSText'),
365                color=bui.app.ui_v1.heading_color,
366                scale=0.9,
367                maxwidth=90,
368                h_align='right',
369                v_align='center',
370            )
371
372            max_fps_str = str(bui.app.config.resolve('Max FPS'))
373            self._last_max_fps_str = max_fps_str
374            self._max_fps_text = bui.textwidget(
375                parent=self._root_widget,
376                position=(170, v - 5),
377                size=(105, 30),
378                text=max_fps_str,
379                max_chars=5,
380                editable=True,
381                h_align='left',
382                v_align='center',
383                on_return_press_call=self._on_max_fps_return_press,
384            )
385            v -= 45
386
387        if self._max_fps_text is not None and resolution_popup is not None:
388            bui.widget(
389                edit=resolution_popup.get_button(),
390                down_widget=self._max_fps_text,
391            )
392            bui.widget(
393                edit=self._max_fps_text,
394                up_widget=resolution_popup.get_button(),
395            )
396
397        fpsc = ConfigCheckBox(
398            parent=self._root_widget,
399            position=(69, v - 6),
400            size=(210, 30),
401            scale=0.86,
402            configkey='Show FPS',
403            displayname=bui.Lstr(resource=self._r + '.showFPSText'),
404            maxwidth=130,
405        )
406        if self._max_fps_text is not None:
407            bui.widget(
408                edit=self._max_fps_text,
409                down_widget=fpsc.widget,
410            )
411            bui.widget(
412                edit=fpsc.widget,
413                up_widget=self._max_fps_text,
414            )
415
416        if show_tv_mode:
417            tvc = ConfigCheckBox(
418                parent=self._root_widget,
419                position=(240, v - 6),
420                size=(210, 30),
421                scale=0.86,
422                configkey='TV Border',
423                displayname=bui.Lstr(resource=self._r + '.tvBorderText'),
424                maxwidth=130,
425            )
426            bui.widget(edit=fpsc.widget, right_widget=tvc.widget)
427            bui.widget(edit=tvc.widget, left_widget=fpsc.widget)
428
429        v -= spacing
430
431        # Make a timer to update our controls in case the config changes
432        # under us.
433        self._update_timer = bui.AppTimer(
434            0.25, bui.WeakCall(self._update_controls), repeat=True
435        )
Inherited Members
bauiv1._uitypes.Window
get_root_widget