bauiv1lib.settings.nettesting

Provides ui for network related testing.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Provides ui for network related testing."""
  4
  5from __future__ import annotations
  6
  7import time
  8import copy
  9import weakref
 10from threading import Thread
 11from typing import TYPE_CHECKING, override
 12
 13from efro.error import CleanError
 14from bauiv1lib.settings.testing import TestingWindow
 15import bauiv1 as bui
 16
 17if TYPE_CHECKING:
 18    from typing import Callable, Any
 19
 20# We generally want all net tests to timeout on their own, but we add
 21# sort of sane max in case they don't.
 22MAX_TEST_SECONDS = 60 * 2
 23
 24
 25class NetTestingWindow(bui.MainWindow):
 26    """Window that runs a networking test suite to help diagnose issues."""
 27
 28    def __init__(
 29        self,
 30        transition: str | None = 'in_right',
 31        origin_widget: bui.Widget | None = None,
 32    ):
 33        uiscale = bui.app.ui_v1.uiscale
 34        self._width = 1200 if uiscale is bui.UIScale.SMALL else 820
 35        self._height = (
 36            800
 37            if uiscale is bui.UIScale.SMALL
 38            else 550 if uiscale is bui.UIScale.MEDIUM else 650
 39        )
 40
 41        self._printed_lines: list[str] = []
 42        assert bui.app.classic is not None
 43
 44        # Do some fancy math to fill all available screen area up to the
 45        # size of our backing container. This lets us fit to the exact
 46        # screen shape at small ui scale.
 47        screensize = bui.get_virtual_screen_size()
 48        scale = (
 49            1.75
 50            if uiscale is bui.UIScale.SMALL
 51            else 1.0 if uiscale is bui.UIScale.MEDIUM else 0.75
 52        )
 53
 54        # Calc screen size in our local container space and clamp to a
 55        # bit smaller than our container size.
 56        target_width = min(self._width - 90, screensize[0] / scale)
 57        target_height = min(self._height - 90, screensize[1] / scale)
 58
 59        # To get top/left coords, go to the center of our window and
 60        # offset by half the width/height of our target area.
 61        yoffs = 0.5 * self._height + 0.5 * target_height + 30.0
 62
 63        scroll_width = target_width
 64        scroll_height = target_height - 52
 65        scroll_bottom = yoffs - 82 - scroll_height
 66
 67        super().__init__(
 68            root_widget=bui.containerwidget(
 69                size=(self._width, self._height),
 70                scale=scale,
 71                toolbar_visibility=(
 72                    'menu_minimal'
 73                    if uiscale is bui.UIScale.SMALL
 74                    else 'menu_full'
 75                ),
 76            ),
 77            transition=transition,
 78            origin_widget=origin_widget,
 79            # We're affected by screen size only at small ui-scale.
 80            refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL,
 81        )
 82        self._back_button: bui.Widget | None
 83        if uiscale is bui.UIScale.SMALL:
 84            bui.containerwidget(
 85                edit=self._root_widget, on_cancel_call=self.main_window_back
 86            )
 87            self._back_button = None
 88        else:
 89            self._back_button = bui.buttonwidget(
 90                parent=self._root_widget,
 91                position=(46, yoffs - 77),
 92                size=(60, 60),
 93                scale=0.9,
 94                label=bui.charstr(bui.SpecialChar.BACK),
 95                button_type='backSmall',
 96                autoselect=True,
 97                on_activate_call=self.main_window_back,
 98            )
 99            bui.containerwidget(
100                edit=self._root_widget, cancel_button=self._back_button
101            )
102
103        # Avoid squads button on small mode.
104        # xinset = -50 if uiscale is bui.UIScale.SMALL else 0
105
106        xextra = -80 if uiscale is bui.UIScale.SMALL else 0
107        self._copy_button = bui.buttonwidget(
108            parent=self._root_widget,
109            position=(
110                self._width * 0.5 + scroll_width * 0.5 - 210 + 80 + xextra,
111                yoffs - 79,
112            ),
113            size=(100, 60),
114            scale=0.8,
115            autoselect=True,
116            label=bui.Lstr(resource='copyText'),
117            on_activate_call=self._copy,
118        )
119
120        self._settings_button = bui.buttonwidget(
121            parent=self._root_widget,
122            position=(
123                self._width * 0.5 + scroll_width * 0.5 - 110 + 80 + xextra,
124                yoffs - 77,
125            ),
126            size=(60, 60),
127            scale=0.8,
128            autoselect=True,
129            label=bui.Lstr(value='...'),
130            on_activate_call=self._show_val_testing,
131        )
132
133        bui.textwidget(
134            parent=self._root_widget,
135            position=(self._width * 0.5, yoffs - 55),
136            size=(0, 0),
137            text=bui.Lstr(resource='settingsWindowAdvanced.netTestingText'),
138            color=(0.8, 0.8, 0.8, 1.0),
139            h_align='center',
140            v_align='center',
141            maxwidth=250,
142        )
143
144        self._scroll = bui.scrollwidget(
145            parent=self._root_widget,
146            size=(scroll_width, scroll_height),
147            position=(self._width * 0.5 - scroll_width * 0.5, scroll_bottom),
148            capture_arrows=True,
149            autoselect=True,
150            border_opacity=0.4,
151        )
152        self._rows = bui.columnwidget(parent=self._scroll)
153
154        # Now kick off the tests.
155        # Pass a weak-ref to this window so we don't keep it alive
156        # if we back out before it completes. Also set is as daemon
157        # so it doesn't keep the app running if the user is trying to quit.
158        Thread(
159            daemon=True,
160            target=bui.Call(_run_diagnostics, weakref.ref(self)),
161        ).start()
162
163    @override
164    def get_main_window_state(self) -> bui.MainWindowState:
165        # Support recreating our window for back/refresh purposes.
166        cls = type(self)
167        return bui.BasicMainWindowState(
168            create_call=lambda transition, origin_widget: cls(
169                transition=transition, origin_widget=origin_widget
170            )
171        )
172
173    def print(self, text: str, color: tuple[float, float, float]) -> None:
174        """Print text to our console thingie."""
175        for line in text.splitlines():
176            txt = bui.textwidget(
177                parent=self._rows,
178                color=color,
179                text=line,
180                scale=0.75,
181                flatness=1.0,
182                shadow=0.0,
183                size=(0, 20),
184            )
185            bui.containerwidget(edit=self._rows, visible_child=txt)
186            self._printed_lines.append(line)
187
188    def _copy(self) -> None:
189        if not bui.clipboard_is_supported():
190            bui.screenmessage(
191                'Clipboard not supported on this platform.', color=(1, 0, 0)
192            )
193            return
194        bui.clipboard_set_text('\n'.join(self._printed_lines))
195        bui.screenmessage(f'{len(self._printed_lines)} lines copied.')
196
197    def _show_val_testing(self) -> None:
198        assert bui.app.classic is not None
199
200        # no-op if we're not in control.
201        if not self.main_window_has_control():
202            return
203
204        self.main_window_replace(get_net_val_testing_window())
205
206
207def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None:
208    # pylint: disable=too-many-statements
209    # pylint: disable=too-many-branches
210
211    from efro.util import utc_now
212
213    have_error = [False]
214
215    # We're running in a background thread but UI stuff needs to run
216    # in the logic thread; give ourself a way to pass stuff to it.
217    def _print(
218        text: str, color: tuple[float, float, float] | None = None
219    ) -> None:
220        def _print_in_logic_thread() -> None:
221            win = weakwin()
222            if win is not None:
223                win.print(text, (1.0, 1.0, 1.0) if color is None else color)
224
225        bui.pushcall(_print_in_logic_thread, from_other_thread=True)
226
227    def _print_test_results(call: Callable[[], Any]) -> bool:
228        """Run the provided call, print result, & return success."""
229        starttime = time.monotonic()
230        try:
231            call()
232            duration = time.monotonic() - starttime
233            _print(f'Succeeded in {duration:.2f}s.', color=(0, 1, 0))
234            return True
235        except Exception as exc:
236            import traceback
237
238            duration = time.monotonic() - starttime
239            msg = (
240                str(exc)
241                if isinstance(exc, CleanError)
242                else traceback.format_exc()
243            )
244            _print(msg, color=(1.0, 1.0, 0.3))
245            _print(f'Failed in {duration:.2f}s.', color=(1, 0, 0))
246            have_error[0] = True
247            return False
248
249    try:
250        plus = bui.app.plus
251        assert plus is not None
252
253        assert bui.app.classic is not None
254
255        _print(
256            f'Running network diagnostics...\n'
257            f'ua: {bui.app.classic.legacy_user_agent_string}\n'
258            f'time: {utc_now()}.'
259        )
260
261        if bool(False):
262            _print('\nRunning dummy success test...')
263            _print_test_results(_dummy_success)
264
265            _print('\nRunning dummy fail test...')
266            _print_test_results(_dummy_fail)
267
268        # V1 ping
269        baseaddr = plus.get_master_server_address(source=0, version=1)
270        _print(f'\nContacting V1 master-server src0 ({baseaddr})...')
271        v1worked = _print_test_results(lambda: _test_fetch(baseaddr))
272
273        # V1 alternate ping (only if primary fails since this often fails).
274        if v1worked:
275            _print('\nSkipping V1 master-server src1 test since src0 worked.')
276        else:
277            baseaddr = plus.get_master_server_address(source=1, version=1)
278            _print(f'\nContacting V1 master-server src1 ({baseaddr})...')
279            _print_test_results(lambda: _test_fetch(baseaddr))
280
281        if 'none succeeded' in bui.app.net.v1_test_log:
282            _print(
283                f'\nV1-test-log failed: {bui.app.net.v1_test_log}',
284                color=(1, 0, 0),
285            )
286            have_error[0] = True
287        else:
288            _print(f'\nV1-test-log ok: {bui.app.net.v1_test_log}')
289
290        for srcid, result in sorted(bui.app.net.v1_ctest_results.items()):
291            _print(f'\nV1 src{srcid} result: {result}')
292
293        curv1addr = plus.get_master_server_address(version=1)
294        _print(f'\nUsing V1 address: {curv1addr}')
295
296        if plus.get_v1_account_state() == 'signed_in':
297            _print('\nRunning V1 transaction...')
298            _print_test_results(_test_v1_transaction)
299        else:
300            _print('\nSkipping V1 transaction (Not signed into V1).')
301
302        # V2 ping
303        baseaddr = plus.get_master_server_address(version=2)
304        _print(f'\nContacting V2 master-server ({baseaddr})...')
305        _print_test_results(lambda: _test_fetch(baseaddr))
306
307        _print('\nComparing local time to V2 server...')
308        _print_test_results(_test_v2_time)
309
310        # Get V2 nearby zone
311        with bui.app.net.zone_pings_lock:
312            zone_pings = copy.deepcopy(bui.app.net.zone_pings)
313        nearest_zone = (
314            None
315            if not zone_pings
316            else sorted(zone_pings.items(), key=lambda i: i[1])[0]
317        )
318
319        if nearest_zone is not None:
320            nearstr = f'{nearest_zone[0]}: {nearest_zone[1]:.0f}ms'
321        else:
322            nearstr = '-'
323        _print(f'\nChecking nearest V2 zone ping ({nearstr})...')
324        _print_test_results(lambda: _test_nearby_zone_ping(nearest_zone))
325
326        _print('\nSending V2 cloud message...')
327        _print_test_results(_test_v2_cloud_message)
328
329        if have_error[0]:
330            _print(
331                '\nDiagnostics complete. Some diagnostics failed.',
332                color=(10, 0, 0),
333            )
334        else:
335            _print(
336                '\nDiagnostics complete. Everything looks good!',
337                color=(0, 1, 0),
338            )
339    except Exception:
340        import traceback
341
342        _print(
343            f'An unexpected error occurred during testing;'
344            f' please report this.\n'
345            f'{traceback.format_exc()}',
346            color=(1, 0, 0),
347        )
348
349
350def _dummy_success() -> None:
351    """Dummy success test."""
352    time.sleep(1.2)
353
354
355def _dummy_fail() -> None:
356    """Dummy fail test case."""
357    raise RuntimeError('fail-test')
358
359
360def _test_v1_transaction() -> None:
361    """Dummy fail test case."""
362    plus = bui.app.plus
363    assert plus is not None
364
365    if plus.get_v1_account_state() != 'signed_in':
366        raise RuntimeError('Not signed in.')
367
368    starttime = time.monotonic()
369
370    # Gets set to True on success or string on error.
371    results: list[Any] = [False]
372
373    def _cb(cbresults: Any) -> None:
374        # Simply set results here; our other thread acts on them.
375        if not isinstance(cbresults, dict) or 'party_code' not in cbresults:
376            results[0] = 'Unexpected transaction response'
377            return
378        results[0] = True  # Success!
379
380    def _do_it() -> None:
381        assert plus is not None
382        # Fire off a transaction with a callback.
383        plus.add_v1_account_transaction(
384            {
385                'type': 'PRIVATE_PARTY_QUERY',
386                'expire_time': time.time() + 20,
387            },
388            callback=_cb,
389        )
390        plus.run_v1_account_transactions()
391
392    bui.pushcall(_do_it, from_other_thread=True)
393
394    while results[0] is False:
395        time.sleep(0.01)
396        if time.monotonic() - starttime > MAX_TEST_SECONDS:
397            raise RuntimeError(
398                f'test timed out after {MAX_TEST_SECONDS} seconds'
399            )
400
401    # If we got left a string, its an error.
402    if isinstance(results[0], str):
403        raise RuntimeError(results[0])
404
405
406def _test_v2_cloud_message() -> None:
407    from dataclasses import dataclass
408    import bacommon.cloud
409
410    @dataclass
411    class _Results:
412        errstr: str | None = None
413        send_time: float | None = None
414        response_time: float | None = None
415
416    results = _Results()
417
418    def _cb(response: bacommon.cloud.PingResponse | Exception) -> None:
419        # Note: this runs in another thread so need to avoid exceptions.
420        results.response_time = time.monotonic()
421        if isinstance(response, Exception):
422            results.errstr = str(response)
423        if not isinstance(response, bacommon.cloud.PingResponse):
424            results.errstr = f'invalid response type: {type(response)}.'
425
426    def _send() -> None:
427        # Note: this runs in another thread so need to avoid exceptions.
428        results.send_time = time.monotonic()
429        assert bui.app.plus is not None
430        bui.app.plus.cloud.send_message_cb(bacommon.cloud.PingMessage(), _cb)
431
432    # This stuff expects to be run from the logic thread.
433    bui.pushcall(_send, from_other_thread=True)
434
435    wait_start_time = time.monotonic()
436    while True:
437        if results.response_time is not None:
438            break
439        time.sleep(0.01)
440        if time.monotonic() - wait_start_time > MAX_TEST_SECONDS:
441            raise RuntimeError(
442                f'Timeout ({MAX_TEST_SECONDS} seconds)'
443                f' waiting for cloud message response'
444            )
445    if results.errstr is not None:
446        raise RuntimeError(results.errstr)
447
448
449def _test_v2_time() -> None:
450    offset = bui.app.net.server_time_offset_hours
451    if offset is None:
452        raise RuntimeError(
453            'no time offset found;'
454            ' perhaps unable to communicate with v2 server?'
455        )
456    if abs(offset) >= 2.0:
457        raise CleanError(
458            f'Your device time is off from world time by {offset:.1f} hours.\n'
459            'This may cause network operations to fail due to your device\n'
460            ' incorrectly treating SSL certificates as not-yet-valid, etc.\n'
461            'Check your device time and time-zone settings to fix this.\n'
462        )
463
464
465def _test_fetch(baseaddr: str) -> None:
466    # pylint: disable=consider-using-with
467    import urllib.request
468
469    assert bui.app.classic is not None
470    response = urllib.request.urlopen(
471        urllib.request.Request(
472            f'{baseaddr}/ping',
473            None,
474            {'User-Agent': bui.app.classic.legacy_user_agent_string},
475        ),
476        context=bui.app.net.sslcontext,
477        timeout=MAX_TEST_SECONDS,
478    )
479    if response.getcode() != 200:
480        raise RuntimeError(
481            f'Got unexpected response code {response.getcode()}.'
482        )
483    data = response.read()
484    if data != b'pong':
485        raise RuntimeError('Got unexpected response data.')
486
487
488def _test_nearby_zone_ping(nearest_zone: tuple[str, float] | None) -> None:
489    """Try to ping nearest v2 zone."""
490    if nearest_zone is None:
491        raise RuntimeError('No nearest zone.')
492    if nearest_zone[1] > 500:
493        raise RuntimeError('Ping too high.')
494
495
496def get_net_val_testing_window() -> TestingWindow:
497    """Create a window for testing net values."""
498    entries = [
499        {'name': 'bufferTime', 'label': 'Buffer Time', 'increment': 1.0},
500        {
501            'name': 'delaySampling',
502            'label': 'Delay Sampling',
503            'increment': 1.0,
504        },
505        {
506            'name': 'dynamicsSyncTime',
507            'label': 'Dynamics Sync Time',
508            'increment': 10,
509        },
510        {'name': 'showNetInfo', 'label': 'Show Net Info', 'increment': 1},
511    ]
512    return TestingWindow(
513        title=bui.Lstr(resource='settingsWindowAdvanced.netTestingText'),
514        entries=entries,
515    )
516
517
518# class NetValTestingWindow(TestingWindow):
519#     """Window to test network related settings."""
520
521#     def __init__(self, transition: str = 'in_right'):
522#         entries = [
523#             {'name': 'bufferTime', 'label': 'Buffer Time', 'increment': 1.0},
524#             {
525#                 'name': 'delaySampling',
526#                 'label': 'Delay Sampling',
527#                 'increment': 1.0,
528#             },
529#             {
530#                 'name': 'dynamicsSyncTime',
531#                 'label': 'Dynamics Sync Time',
532#                 'increment': 10,
533#             },
534#             {'name': 'showNetInfo', 'label': 'Show Net Info', 'increment': 1},
535#         ]
536#         super().__init__(
537#             title=bui.Lstr(resource='settingsWindowAdvanced.netTestingText'),
538#             entries=entries,
539#             transition=transition,
540#         )
MAX_TEST_SECONDS = 120
class NetTestingWindow(bauiv1._uitypes.MainWindow):
 26class NetTestingWindow(bui.MainWindow):
 27    """Window that runs a networking test suite to help diagnose issues."""
 28
 29    def __init__(
 30        self,
 31        transition: str | None = 'in_right',
 32        origin_widget: bui.Widget | None = None,
 33    ):
 34        uiscale = bui.app.ui_v1.uiscale
 35        self._width = 1200 if uiscale is bui.UIScale.SMALL else 820
 36        self._height = (
 37            800
 38            if uiscale is bui.UIScale.SMALL
 39            else 550 if uiscale is bui.UIScale.MEDIUM else 650
 40        )
 41
 42        self._printed_lines: list[str] = []
 43        assert bui.app.classic is not None
 44
 45        # Do some fancy math to fill all available screen area up to the
 46        # size of our backing container. This lets us fit to the exact
 47        # screen shape at small ui scale.
 48        screensize = bui.get_virtual_screen_size()
 49        scale = (
 50            1.75
 51            if uiscale is bui.UIScale.SMALL
 52            else 1.0 if uiscale is bui.UIScale.MEDIUM else 0.75
 53        )
 54
 55        # Calc screen size in our local container space and clamp to a
 56        # bit smaller than our container size.
 57        target_width = min(self._width - 90, screensize[0] / scale)
 58        target_height = min(self._height - 90, screensize[1] / scale)
 59
 60        # To get top/left coords, go to the center of our window and
 61        # offset by half the width/height of our target area.
 62        yoffs = 0.5 * self._height + 0.5 * target_height + 30.0
 63
 64        scroll_width = target_width
 65        scroll_height = target_height - 52
 66        scroll_bottom = yoffs - 82 - scroll_height
 67
 68        super().__init__(
 69            root_widget=bui.containerwidget(
 70                size=(self._width, self._height),
 71                scale=scale,
 72                toolbar_visibility=(
 73                    'menu_minimal'
 74                    if uiscale is bui.UIScale.SMALL
 75                    else 'menu_full'
 76                ),
 77            ),
 78            transition=transition,
 79            origin_widget=origin_widget,
 80            # We're affected by screen size only at small ui-scale.
 81            refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL,
 82        )
 83        self._back_button: bui.Widget | None
 84        if uiscale is bui.UIScale.SMALL:
 85            bui.containerwidget(
 86                edit=self._root_widget, on_cancel_call=self.main_window_back
 87            )
 88            self._back_button = None
 89        else:
 90            self._back_button = bui.buttonwidget(
 91                parent=self._root_widget,
 92                position=(46, yoffs - 77),
 93                size=(60, 60),
 94                scale=0.9,
 95                label=bui.charstr(bui.SpecialChar.BACK),
 96                button_type='backSmall',
 97                autoselect=True,
 98                on_activate_call=self.main_window_back,
 99            )
100            bui.containerwidget(
101                edit=self._root_widget, cancel_button=self._back_button
102            )
103
104        # Avoid squads button on small mode.
105        # xinset = -50 if uiscale is bui.UIScale.SMALL else 0
106
107        xextra = -80 if uiscale is bui.UIScale.SMALL else 0
108        self._copy_button = bui.buttonwidget(
109            parent=self._root_widget,
110            position=(
111                self._width * 0.5 + scroll_width * 0.5 - 210 + 80 + xextra,
112                yoffs - 79,
113            ),
114            size=(100, 60),
115            scale=0.8,
116            autoselect=True,
117            label=bui.Lstr(resource='copyText'),
118            on_activate_call=self._copy,
119        )
120
121        self._settings_button = bui.buttonwidget(
122            parent=self._root_widget,
123            position=(
124                self._width * 0.5 + scroll_width * 0.5 - 110 + 80 + xextra,
125                yoffs - 77,
126            ),
127            size=(60, 60),
128            scale=0.8,
129            autoselect=True,
130            label=bui.Lstr(value='...'),
131            on_activate_call=self._show_val_testing,
132        )
133
134        bui.textwidget(
135            parent=self._root_widget,
136            position=(self._width * 0.5, yoffs - 55),
137            size=(0, 0),
138            text=bui.Lstr(resource='settingsWindowAdvanced.netTestingText'),
139            color=(0.8, 0.8, 0.8, 1.0),
140            h_align='center',
141            v_align='center',
142            maxwidth=250,
143        )
144
145        self._scroll = bui.scrollwidget(
146            parent=self._root_widget,
147            size=(scroll_width, scroll_height),
148            position=(self._width * 0.5 - scroll_width * 0.5, scroll_bottom),
149            capture_arrows=True,
150            autoselect=True,
151            border_opacity=0.4,
152        )
153        self._rows = bui.columnwidget(parent=self._scroll)
154
155        # Now kick off the tests.
156        # Pass a weak-ref to this window so we don't keep it alive
157        # if we back out before it completes. Also set is as daemon
158        # so it doesn't keep the app running if the user is trying to quit.
159        Thread(
160            daemon=True,
161            target=bui.Call(_run_diagnostics, weakref.ref(self)),
162        ).start()
163
164    @override
165    def get_main_window_state(self) -> bui.MainWindowState:
166        # Support recreating our window for back/refresh purposes.
167        cls = type(self)
168        return bui.BasicMainWindowState(
169            create_call=lambda transition, origin_widget: cls(
170                transition=transition, origin_widget=origin_widget
171            )
172        )
173
174    def print(self, text: str, color: tuple[float, float, float]) -> None:
175        """Print text to our console thingie."""
176        for line in text.splitlines():
177            txt = bui.textwidget(
178                parent=self._rows,
179                color=color,
180                text=line,
181                scale=0.75,
182                flatness=1.0,
183                shadow=0.0,
184                size=(0, 20),
185            )
186            bui.containerwidget(edit=self._rows, visible_child=txt)
187            self._printed_lines.append(line)
188
189    def _copy(self) -> None:
190        if not bui.clipboard_is_supported():
191            bui.screenmessage(
192                'Clipboard not supported on this platform.', color=(1, 0, 0)
193            )
194            return
195        bui.clipboard_set_text('\n'.join(self._printed_lines))
196        bui.screenmessage(f'{len(self._printed_lines)} lines copied.')
197
198    def _show_val_testing(self) -> None:
199        assert bui.app.classic is not None
200
201        # no-op if we're not in control.
202        if not self.main_window_has_control():
203            return
204
205        self.main_window_replace(get_net_val_testing_window())

Window that runs a networking test suite to help diagnose issues.

NetTestingWindow( transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None)
 29    def __init__(
 30        self,
 31        transition: str | None = 'in_right',
 32        origin_widget: bui.Widget | None = None,
 33    ):
 34        uiscale = bui.app.ui_v1.uiscale
 35        self._width = 1200 if uiscale is bui.UIScale.SMALL else 820
 36        self._height = (
 37            800
 38            if uiscale is bui.UIScale.SMALL
 39            else 550 if uiscale is bui.UIScale.MEDIUM else 650
 40        )
 41
 42        self._printed_lines: list[str] = []
 43        assert bui.app.classic is not None
 44
 45        # Do some fancy math to fill all available screen area up to the
 46        # size of our backing container. This lets us fit to the exact
 47        # screen shape at small ui scale.
 48        screensize = bui.get_virtual_screen_size()
 49        scale = (
 50            1.75
 51            if uiscale is bui.UIScale.SMALL
 52            else 1.0 if uiscale is bui.UIScale.MEDIUM else 0.75
 53        )
 54
 55        # Calc screen size in our local container space and clamp to a
 56        # bit smaller than our container size.
 57        target_width = min(self._width - 90, screensize[0] / scale)
 58        target_height = min(self._height - 90, screensize[1] / scale)
 59
 60        # To get top/left coords, go to the center of our window and
 61        # offset by half the width/height of our target area.
 62        yoffs = 0.5 * self._height + 0.5 * target_height + 30.0
 63
 64        scroll_width = target_width
 65        scroll_height = target_height - 52
 66        scroll_bottom = yoffs - 82 - scroll_height
 67
 68        super().__init__(
 69            root_widget=bui.containerwidget(
 70                size=(self._width, self._height),
 71                scale=scale,
 72                toolbar_visibility=(
 73                    'menu_minimal'
 74                    if uiscale is bui.UIScale.SMALL
 75                    else 'menu_full'
 76                ),
 77            ),
 78            transition=transition,
 79            origin_widget=origin_widget,
 80            # We're affected by screen size only at small ui-scale.
 81            refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL,
 82        )
 83        self._back_button: bui.Widget | None
 84        if uiscale is bui.UIScale.SMALL:
 85            bui.containerwidget(
 86                edit=self._root_widget, on_cancel_call=self.main_window_back
 87            )
 88            self._back_button = None
 89        else:
 90            self._back_button = bui.buttonwidget(
 91                parent=self._root_widget,
 92                position=(46, yoffs - 77),
 93                size=(60, 60),
 94                scale=0.9,
 95                label=bui.charstr(bui.SpecialChar.BACK),
 96                button_type='backSmall',
 97                autoselect=True,
 98                on_activate_call=self.main_window_back,
 99            )
100            bui.containerwidget(
101                edit=self._root_widget, cancel_button=self._back_button
102            )
103
104        # Avoid squads button on small mode.
105        # xinset = -50 if uiscale is bui.UIScale.SMALL else 0
106
107        xextra = -80 if uiscale is bui.UIScale.SMALL else 0
108        self._copy_button = bui.buttonwidget(
109            parent=self._root_widget,
110            position=(
111                self._width * 0.5 + scroll_width * 0.5 - 210 + 80 + xextra,
112                yoffs - 79,
113            ),
114            size=(100, 60),
115            scale=0.8,
116            autoselect=True,
117            label=bui.Lstr(resource='copyText'),
118            on_activate_call=self._copy,
119        )
120
121        self._settings_button = bui.buttonwidget(
122            parent=self._root_widget,
123            position=(
124                self._width * 0.5 + scroll_width * 0.5 - 110 + 80 + xextra,
125                yoffs - 77,
126            ),
127            size=(60, 60),
128            scale=0.8,
129            autoselect=True,
130            label=bui.Lstr(value='...'),
131            on_activate_call=self._show_val_testing,
132        )
133
134        bui.textwidget(
135            parent=self._root_widget,
136            position=(self._width * 0.5, yoffs - 55),
137            size=(0, 0),
138            text=bui.Lstr(resource='settingsWindowAdvanced.netTestingText'),
139            color=(0.8, 0.8, 0.8, 1.0),
140            h_align='center',
141            v_align='center',
142            maxwidth=250,
143        )
144
145        self._scroll = bui.scrollwidget(
146            parent=self._root_widget,
147            size=(scroll_width, scroll_height),
148            position=(self._width * 0.5 - scroll_width * 0.5, scroll_bottom),
149            capture_arrows=True,
150            autoselect=True,
151            border_opacity=0.4,
152        )
153        self._rows = bui.columnwidget(parent=self._scroll)
154
155        # Now kick off the tests.
156        # Pass a weak-ref to this window so we don't keep it alive
157        # if we back out before it completes. Also set is as daemon
158        # so it doesn't keep the app running if the user is trying to quit.
159        Thread(
160            daemon=True,
161            target=bui.Call(_run_diagnostics, weakref.ref(self)),
162        ).start()

Create a MainWindow given a root widget and transition info.

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

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
164    @override
165    def get_main_window_state(self) -> bui.MainWindowState:
166        # Support recreating our window for back/refresh purposes.
167        cls = type(self)
168        return bui.BasicMainWindowState(
169            create_call=lambda transition, origin_widget: cls(
170                transition=transition, origin_widget=origin_widget
171            )
172        )

Return a WindowState to recreate this window, if supported.

def print(self, text: str, color: tuple[float, float, float]) -> None:
174    def print(self, text: str, color: tuple[float, float, float]) -> None:
175        """Print text to our console thingie."""
176        for line in text.splitlines():
177            txt = bui.textwidget(
178                parent=self._rows,
179                color=color,
180                text=line,
181                scale=0.75,
182                flatness=1.0,
183                shadow=0.0,
184                size=(0, 20),
185            )
186            bui.containerwidget(edit=self._rows, visible_child=txt)
187            self._printed_lines.append(line)

Print text to our console thingie.

def get_net_val_testing_window() -> bauiv1lib.settings.testing.TestingWindow:
497def get_net_val_testing_window() -> TestingWindow:
498    """Create a window for testing net values."""
499    entries = [
500        {'name': 'bufferTime', 'label': 'Buffer Time', 'increment': 1.0},
501        {
502            'name': 'delaySampling',
503            'label': 'Delay Sampling',
504            'increment': 1.0,
505        },
506        {
507            'name': 'dynamicsSyncTime',
508            'label': 'Dynamics Sync Time',
509            'increment': 10,
510        },
511        {'name': 'showNetInfo', 'label': 'Show Net Info', 'increment': 1},
512    ]
513    return TestingWindow(
514        title=bui.Lstr(resource='settingsWindowAdvanced.netTestingText'),
515        entries=entries,
516    )

Create a window for testing net values.