Source code for bauiv1lib.settings.graphics

# Released under the MIT License. See LICENSE for details.
#
"""Provides UI for graphics settings."""

from __future__ import annotations

from typing import TYPE_CHECKING, cast, override

from bauiv1lib.popup import PopupMenu
from bauiv1lib.config import ConfigCheckBox
import bauiv1 as bui

if TYPE_CHECKING:
    from typing import Any


[docs] class GraphicsSettingsWindow(bui.MainWindow): """Window for graphics settings.""" def __init__( self, transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements self._r = 'graphicsSettingsWindow' app = bui.app assert app.classic is not None spacing = 32 self._have_selected_child = False uiscale = app.ui_v1.uiscale width = 1200 if uiscale is bui.UIScale.SMALL else 450.0 height = 900 if uiscale is bui.UIScale.SMALL else 302.0 self._max_fps_dirty = False self._last_max_fps_set_time = bui.apptime() self._last_max_fps_str = '' self._show_fullscreen = False fullscreen_spacing_top = spacing * 0.2 fullscreen_spacing = spacing * 1.2 if bui.fullscreen_control_available(): self._show_fullscreen = True height += fullscreen_spacing + fullscreen_spacing_top show_vsync = bui.supports_vsync() show_tv_mode = not bui.app.env.vr show_max_fps = bui.supports_max_fps() if show_max_fps: height += 60 show_resolution = True if app.env.vr: show_resolution = ( app.classic.platform == 'android' and app.classic.subplatform == 'cardboard' ) assert bui.app.classic is not None # Do some fancy math to fill all available screen area up to the # size of our backing container. This lets us fit to the exact # screen shape at small ui scale. screensize = bui.get_virtual_screen_size() scale = ( 1.9 if uiscale is bui.UIScale.SMALL else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0 ) popup_menu_scale = scale * 1.2 # Calc screen size in our local container space and clamp to a # bit smaller than our container size. # target_width = min(width - 80, screensize[0] / scale) target_height = min(height - 80, screensize[1] / scale) # To get top/left coords, go to the center of our window and # offset by half the width/height of our target area. yoffs = 0.5 * height + 0.5 * target_height + 30.0 super().__init__( root_widget=bui.containerwidget( size=(width, height), scale=scale, toolbar_visibility=( 'menu_minimal' if uiscale is bui.UIScale.SMALL else 'menu_full' ), ), transition=transition, origin_widget=origin_widget, # We're affected by screen size only at small ui-scale. refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL, ) # Center most of our content in the middle of the window. v = height * 0.5 + (100 if show_max_fps else 85) h_offs = width * 0.5 - 220 if uiscale is bui.UIScale.SMALL: bui.containerwidget( edit=self._root_widget, on_cancel_call=self.main_window_back ) back_button = None else: back_button = bui.buttonwidget( parent=self._root_widget, position=(35, yoffs - 50), size=(60, 60), scale=0.8, text_scale=1.2, autoselect=True, label=bui.charstr(bui.SpecialChar.BACK), button_type='backSmall', on_activate_call=self.main_window_back, ) bui.containerwidget( edit=self._root_widget, cancel_button=back_button ) bui.textwidget( parent=self._root_widget, position=( width * 0.5, yoffs - (53 if uiscale is bui.UIScale.SMALL else 25), ), size=(0, 0), text=bui.Lstr(resource=f'{self._r}.titleText'), color=bui.app.ui_v1.title_color, h_align='center', v_align='center', ) self._fullscreen_checkbox: bui.Widget | None = None if self._show_fullscreen: v -= fullscreen_spacing_top # Fullscreen control does not necessarily talk to the # app config so we have to wrangle it manually instead of # using a config-checkbox. label = bui.Lstr(resource=f'{self._r}.fullScreenText') # Show keyboard shortcut alongside the control if they # provide one. shortcut = bui.fullscreen_control_key_shortcut() if shortcut is not None: label = bui.Lstr( value='$(NAME) [$(SHORTCUT)]', subs=[('$(NAME)', label), ('$(SHORTCUT)', shortcut)], ) self._fullscreen_checkbox = bui.checkboxwidget( parent=self._root_widget, position=(h_offs + 100, v), value=bui.fullscreen_control_get(), on_value_change_call=bui.fullscreen_control_set, maxwidth=250, size=(300, 30), text=label, ) if not self._have_selected_child: bui.containerwidget( edit=self._root_widget, selected_child=self._fullscreen_checkbox, ) self._have_selected_child = True v -= fullscreen_spacing self._selected_color = (0.5, 1, 0.5, 1) self._unselected_color = (0.7, 0.7, 0.7, 1) # Quality bui.textwidget( parent=self._root_widget, position=(h_offs + 60, v), size=(160, 25), text=bui.Lstr(resource=f'{self._r}.visualsText'), color=bui.app.ui_v1.heading_color, scale=0.65, maxwidth=150, h_align='center', v_align='center', ) PopupMenu( parent=self._root_widget, position=(h_offs + 60, v - 50), width=150, scale=popup_menu_scale, choices=['Auto', 'Higher', 'High', 'Medium', 'Low'], choices_disabled=( ['Higher', 'High'] if bui.get_max_graphics_quality() == 'Medium' else [] ), choices_display=[ bui.Lstr(resource='autoText'), bui.Lstr(resource=f'{self._r}.higherText'), bui.Lstr(resource=f'{self._r}.highText'), bui.Lstr(resource=f'{self._r}.mediumText'), bui.Lstr(resource=f'{self._r}.lowText'), ], current_choice=bui.app.config.resolve('Graphics Quality'), on_value_change_call=self._set_quality, ) # Texture controls bui.textwidget( parent=self._root_widget, position=(h_offs + 230, v), size=(160, 25), text=bui.Lstr(resource=f'{self._r}.texturesText'), color=bui.app.ui_v1.heading_color, scale=0.65, maxwidth=150, h_align='center', v_align='center', ) textures_popup = PopupMenu( parent=self._root_widget, position=(h_offs + 230, v - 50), width=150, scale=popup_menu_scale, choices=['Auto', 'High', 'Medium', 'Low'], choices_display=[ bui.Lstr(resource='autoText'), bui.Lstr(resource=f'{self._r}.highText'), bui.Lstr(resource=f'{self._r}.mediumText'), bui.Lstr(resource=f'{self._r}.lowText'), ], current_choice=bui.app.config.resolve('Texture Quality'), on_value_change_call=self._set_textures, ) bui.widget( edit=textures_popup.get_button(), right_widget=bui.get_special_widget('squad_button'), ) v -= 80 resolution_popup: PopupMenu | None = None if show_resolution: bui.textwidget( parent=self._root_widget, position=(h_offs + 60, v), size=(160, 25), text=bui.Lstr(resource=f'{self._r}.resolutionText'), color=bui.app.ui_v1.heading_color, scale=0.65, maxwidth=150, h_align='center', v_align='center', ) # On standard android we have 'Auto', 'Native', and a few # HD standards. if app.classic.platform == 'android': # on cardboard/daydream android we have a few # render-target-scale options if app.classic.subplatform == 'cardboard': rawval = bui.app.config.resolve('GVR Render Target Scale') current_res_cardboard = ( str(min(100, max(10, int(round(rawval * 100.0))))) + '%' ) resolution_popup = PopupMenu( parent=self._root_widget, position=(h_offs + 60, v - 50), width=120, scale=popup_menu_scale, choices=['100%', '75%', '50%', '35%'], current_choice=current_res_cardboard, on_value_change_call=self._set_gvr_render_target_scale, ) else: native_res = bui.get_display_resolution() assert native_res is not None choices = ['Auto', 'Native'] choices_display = [ bui.Lstr(resource='autoText'), bui.Lstr(resource='nativeText'), ] for res in [1440, 1080, 960, 720, 480]: if native_res[1] >= res: res_str = f'{res}p' choices.append(res_str) choices_display.append(bui.Lstr(value=res_str)) current_res_android = bui.app.config.resolve( 'Resolution (Android)' ) resolution_popup = PopupMenu( parent=self._root_widget, position=(h_offs + 60, v - 50), width=120, scale=popup_menu_scale, choices=choices, choices_display=choices_display, current_choice=current_res_android, on_value_change_call=self._set_android_res, ) else: # If we're on a system that doesn't allow setting resolution, # set pixel-scale instead. current_res = bui.get_display_resolution() if current_res is None: rawval = bui.app.config.resolve('Screen Pixel Scale') current_res2 = ( str(min(100, max(10, int(round(rawval * 100.0))))) + '%' ) resolution_popup = PopupMenu( parent=self._root_widget, position=(h_offs + 60, v - 50), width=120, scale=popup_menu_scale, choices=['100%', '88%', '75%', '63%', '50%'], current_choice=current_res2, on_value_change_call=self._set_pixel_scale, ) else: raise RuntimeError( 'obsolete code path; discrete resolutions' ' no longer supported' ) if resolution_popup is not None: bui.widget( edit=resolution_popup.get_button(), left_widget=back_button, ) vsync_popup: PopupMenu | None = None if show_vsync: bui.textwidget( parent=self._root_widget, position=(h_offs + 230, v), size=(160, 25), text=bui.Lstr(resource=f'{self._r}.verticalSyncText'), color=bui.app.ui_v1.heading_color, scale=0.65, maxwidth=150, h_align='center', v_align='center', ) vsync_popup = PopupMenu( parent=self._root_widget, position=(h_offs + 230, v - 50), width=150, scale=popup_menu_scale, choices=['Auto', 'Always', 'Never'], choices_display=[ bui.Lstr(resource='autoText'), bui.Lstr(resource=f'{self._r}.alwaysText'), bui.Lstr(resource=f'{self._r}.neverText'), ], current_choice=bui.app.config.resolve('Vertical Sync'), on_value_change_call=self._set_vsync, ) if resolution_popup is not None: bui.widget( edit=vsync_popup.get_button(), left_widget=resolution_popup.get_button(), ) if resolution_popup is not None and vsync_popup is not None: bui.widget( edit=resolution_popup.get_button(), right_widget=vsync_popup.get_button(), ) v -= 90 self._max_fps_text: bui.Widget | None = None if show_max_fps: v -= 5 bui.textwidget( parent=self._root_widget, position=(h_offs + 155, v + 10), size=(0, 0), text=bui.Lstr(resource=f'{self._r}.maxFPSText'), color=bui.app.ui_v1.heading_color, scale=0.9, maxwidth=90, h_align='right', v_align='center', ) max_fps_str = str(bui.app.config.resolve('Max FPS')) self._last_max_fps_str = max_fps_str self._max_fps_text = bui.textwidget( parent=self._root_widget, position=(h_offs + 170, v - 5), size=(105, 30), text=max_fps_str, max_chars=5, editable=True, h_align='left', v_align='center', on_return_press_call=self._on_max_fps_return_press, ) v -= 45 if self._max_fps_text is not None and resolution_popup is not None: bui.widget( edit=resolution_popup.get_button(), down_widget=self._max_fps_text, ) bui.widget( edit=self._max_fps_text, up_widget=resolution_popup.get_button(), ) fpsc = ConfigCheckBox( parent=self._root_widget, position=(h_offs + 69, v - 6), size=(210, 30), scale=0.86, configkey='Show FPS', displayname=bui.Lstr(resource=f'{self._r}.showFPSText'), maxwidth=130, ) if self._max_fps_text is not None: bui.widget( edit=self._max_fps_text, down_widget=fpsc.widget, ) bui.widget( edit=fpsc.widget, up_widget=self._max_fps_text, ) if show_tv_mode: tvc = ConfigCheckBox( parent=self._root_widget, position=(h_offs + 240, v - 6), size=(210, 30), scale=0.86, configkey='TV Border', displayname=bui.Lstr(resource=f'{self._r}.tvBorderText'), maxwidth=130, ) bui.widget(edit=fpsc.widget, right_widget=tvc.widget) bui.widget(edit=tvc.widget, left_widget=fpsc.widget) v -= spacing # Make a timer to update our controls in case the config changes # under us. self._update_timer = bui.AppTimer( 0.25, bui.WeakCall(self._update_controls), repeat=True )
[docs] @override def get_main_window_state(self) -> bui.MainWindowState: # Support recreating our window for back/refresh purposes. cls = type(self) return bui.BasicMainWindowState( create_call=lambda transition, origin_widget: cls( transition=transition, origin_widget=origin_widget ) )
[docs] @override def on_main_window_close(self) -> None: self._apply_max_fps()
def _set_quality(self, quality: str) -> None: cfg = bui.app.config cfg['Graphics Quality'] = quality cfg.apply_and_commit() def _set_textures(self, val: str) -> None: cfg = bui.app.config cfg['Texture Quality'] = val cfg.apply_and_commit() def _set_android_res(self, val: str) -> None: cfg = bui.app.config cfg['Resolution (Android)'] = val cfg.apply_and_commit() def _set_pixel_scale(self, res: str) -> None: cfg = bui.app.config cfg['Screen Pixel Scale'] = float(res[:-1]) / 100.0 cfg.apply_and_commit() def _set_gvr_render_target_scale(self, res: str) -> None: cfg = bui.app.config cfg['GVR Render Target Scale'] = float(res[:-1]) / 100.0 cfg.apply_and_commit() def _set_vsync(self, val: str) -> None: cfg = bui.app.config cfg['Vertical Sync'] = val cfg.apply_and_commit() def _on_max_fps_return_press(self) -> None: self._apply_max_fps() bui.containerwidget( edit=self._root_widget, selected_child=cast(bui.Widget, 0) ) def _apply_max_fps(self) -> None: if not self._max_fps_dirty or not self._max_fps_text: return val: Any = bui.textwidget(query=self._max_fps_text) assert isinstance(val, str) # If there's a broken value, replace it with the default. try: ival = int(val) except ValueError: ival = bui.app.config.default_value('Max FPS') assert isinstance(ival, int) # Clamp to reasonable limits (allow -1 to mean no max). if ival != -1: ival = max(10, ival) ival = min(99999, ival) # Store it to the config. cfg = bui.app.config cfg['Max FPS'] = ival cfg.apply_and_commit() # Update the display if we changed the value. if str(ival) != val: bui.textwidget(edit=self._max_fps_text, text=str(ival)) self._max_fps_dirty = False def _update_controls(self) -> None: if self._max_fps_text is not None: # Keep track of when the max-fps value changes. Once it # remains stable for a few moments, apply it. val: Any = bui.textwidget(query=self._max_fps_text) assert isinstance(val, str) if val != self._last_max_fps_str: # Oop; it changed. Note the time and the fact that we'll # need to apply it at some point. self._max_fps_dirty = True self._last_max_fps_str = val self._last_max_fps_set_time = bui.apptime() else: # If its been stable long enough, apply it. if ( self._max_fps_dirty and bui.apptime() - self._last_max_fps_set_time > 1.0 ): self._apply_max_fps() if self._show_fullscreen: # Keep the fullscreen checkbox up to date with the current value. bui.checkboxwidget( edit=self._fullscreen_checkbox, value=bui.fullscreen_control_get(), )
# Docs-generation hack; import some stuff that we likely only forward-declared # in our actual source code so that docs tools can find it. from typing import (Coroutine, Any, Literal, Callable, Generator, Awaitable, Sequence, Self) import asyncio from concurrent.futures import Future from pathlib import Path from enum import Enum