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

Window for graphics settings.

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

Create a MainWindow given a root widget and transition info.

Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
452    @override
453    def get_main_window_state(self) -> bui.MainWindowState:
454        # Support recreating our window for back/refresh purposes.
455        cls = type(self)
456        return bui.BasicMainWindowState(
457            create_call=lambda transition, origin_widget: cls(
458                transition=transition, origin_widget=origin_widget
459            )
460        )

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
462    @override
463    def on_main_window_close(self) -> None:
464        self._apply_max_fps()

Called before transitioning out a main window.

A good opportunity to save window state/etc.