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