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