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.
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.