diff --git a/.efrocachemap b/.efrocachemap index a48f4efd..a21fb9a3 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -4060,26 +4060,26 @@ "build/assets/windows/Win32/ucrtbased.dll": "2def5335207d41b21b9823f6805997f1", "build/assets/windows/Win32/vc_redist.x86.exe": "b08a55e2e77623fe657bea24f223a3ae", "build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599", - "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "9fe23e06319e4e256b9fa88814a14afa", - "build/prefab/full/linux_arm64_gui/release/ballisticakit": "4306acae21ce88235f9d1589086866e7", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "75e4f7d3a3df67dedd079ec3f4441094", - "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "bd5eda13f239b81886ac80596d6ade73", - "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "0805235a92dd91f96d43ea54575eecac", - "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "07589a61b11cbc5fca0bbc8b7fc1c955", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "f28629761060c8152168b6792b71adae", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "1cfd1a33474cdb31834994f626385ed0", - "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "d50879a92d9d344c376f6f196d78d1be", - "build/prefab/full/mac_arm64_gui/release/ballisticakit": "78cd0edf2698f197f2acd80ca364fae7", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "d656f47118ebc3af57c40423cb258bc8", - "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "75df540b27779342a7c696e1bdbe593f", - "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "76f0dfacaa9ea67e45e8ccf3bb3bc1c6", - "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "2acc754bed825a9265e0621dc09899e0", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "62c2b6190de8784ea8750ea50e6a2304", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "e57358fd9a948a8ce82a54cdd5c766fc", - "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "d36e3303e13049eae5e7ec19861d300e", - "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "46971a2ca1e3021e52ea5d0f4938d2ff", - "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "19ebd36613cf62c4bd50e70b93371368", - "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "49ef5905b6e9e1a9caaed3d1c1da4ea5", + "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "c2b80379179c8731be37581e52259377", + "build/prefab/full/linux_arm64_gui/release/ballisticakit": "0a2257e46a20ae6453d888515a00f1a8", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "a8bf3602e161931f96c41989d9d4e630", + "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "15e5d036605366840182cf3f86d247ae", + "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "4212f5014bcf30f5ceb6e9ed00c1b443", + "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "a9ee1b8f07dc7466b4daf90a34991f3b", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "cbc9bf3f8ddce331912b6dbd8f1c6415", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "6b4458aa454391fa0070099ef4cac711", + "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "fae27ee9108877bd75aaa222f02f239d", + "build/prefab/full/mac_arm64_gui/release/ballisticakit": "37f2b4219ffe85aa1d28ab7df7fd4c44", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "3f8de90abf2069a0881f8d91f1ec78b2", + "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "62a6ee7583ef9d83f9bb1601fce6ddaa", + "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "f495a8547ca8b824f80006603537d1cf", + "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "672c351fca7843d85a2be7aba13faf1f", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "50f6d105ad4c2bb2df4f3d335b6d2cfa", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "44e1d633accc2410ca6d825e4c464f45", + "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "5cce0ce595304313ac28c02e235f19d7", + "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "9433fc9da2f4b7255f2c7fa95868604a", + "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "18eb1871a6db9bbe17f5e9678d3d492a", + "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "cf51dafe0553d06a44349d0911c21d71", "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "c22901e06e88a55cce0b4e08bbf41a4c", "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "a27963487e346338e4c216bd4fbb9e2a", "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "c22901e06e88a55cce0b4e08bbf41a4c", @@ -4096,14 +4096,14 @@ "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "852fe46c736082611a831a618923c241", "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "36fbda7829ed5c2862c34feb09b03402", "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "852fe46c736082611a831a618923c241", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "38b4b5b85a9bafdb76222d0f0c962b06", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "9a8af3d217bcb0bacfaed4c30dd5f42e", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "6d10ca306f60d66efb4942636e4955d6", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "1c65d36e4420ed79380dc8c041c94a8b", - "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "9b1b72f3d41c89a6b06288be63e8f40a", - "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "e0f2eb8ea024bc88e999b9dc16317fd4", - "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "42be1225757328f432d91de950444ba0", - "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "05bc2832cc0c9fba308668fc1a6d3b0f", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "8ec03141da397548f8a06259222c14e3", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "9f20a365cc0ec1831e0e301a34198b0e", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "37980dc34b9ad281cdab738f7053af26", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "148e0f3a1ee77607d0b93a636b253295", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "e20a2aa49741757075634f422dc5ac7a", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "fa7018f7c2ee8c952bff7879b92709cb", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "cd38a41872e74b43de005375330e3cd0", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "5bc4faf70f09f56e2ced27cafe2ad3e6", "src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c", "src/assets/ba_data/python/babase/_mgen/enums.py": "28323912b56ec07701eda3d41a6a4101", "src/ballistica/base/mgen/pyembed/binding_base.inc": "72bfed2cce8ff19741989dec28302f3f", @@ -4114,5 +4114,5 @@ "src/ballistica/core/mgen/python_modules_monolithic.h": "fb967ed1c7db0c77d8deb4f00a7103c5", "src/ballistica/scene_v1/mgen/pyembed/binding_scene_v1.inc": "d80f970053099b3044204bfe29ddefce", "src/ballistica/template_fs/mgen/pyembed/binding_template_fs.inc": "44a45492db057bf7f7158c3b0fa11f0f", - "src/ballistica/ui_v1/mgen/pyembed/binding_ui_v1.inc": "8f4c2070174bdc2fbf735180394d7b3a" + "src/ballistica/ui_v1/mgen/pyembed/binding_ui_v1.inc": "f5f054050d2b2fcd3763a4833fb32269" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f3ac4a8..fd6cae22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 1.7.30 (build 21636, api 8, 2023-11-30) +### 1.7.30 (build 21639, api 8, 2023-11-30) - Continued work on the big 1.7.28 update. - Got the Android version back up and running. There's been lots of cleanup and simplification on the Android layer, cleaning out years of cruft. This should @@ -21,9 +21,16 @@ builds when tv-border was on (Thanks for the heads-up Loup(Dliwk's fan)!). - (build 21631) Fixes an issue where '^^^^^^^^^^^^^' lines in stack traces could get chopped into tiny bits each on their own line in the dev console. -- Fixed a longstanding issue where multiple key presses simultaneously could - cause multiple windows to pop up where only one is expected. Please holler if - you still see this problem happening anywhere. +- Hopefully finally fixed a longstanding issue where obscure cases such as + multiple key presses simultaneously could cause multiple main menu windows to + pop up. Please holler if you still see this problem happening anywhere. Also + added a few related safety checks and warnings to help ensure UI code is free + from such problems going forward. To make sure your custom UIs are behaving + well in this system, do the following two things: 1) any time you call + `set_main_menu_window()`, pass your existing main menu window root widget as + `from_window`. 2) In any call that can lead to you switching the main menu + window, check if your root widget is dead or transitioning out first and abort + if it is. See any window in `ui_v1_lib` for examples. ### 1.7.29 (build 21619, api 8, 2023-11-21) diff --git a/src/assets/ba_data/python/baclassic/_appdelegate.py b/src/assets/ba_data/python/baclassic/_appdelegate.py index 4595bd63..1dd1a6b0 100644 --- a/src/assets/ba_data/python/baclassic/_appdelegate.py +++ b/src/assets/ba_data/python/baclassic/_appdelegate.py @@ -41,5 +41,6 @@ class AppDelegate: sessiontype, settings, completion_call=completion_call, - ).get_root_widget() + ).get_root_widget(), + from_window=False, # Disable check since we don't know. ) diff --git a/src/assets/ba_data/python/baclassic/_subsystem.py b/src/assets/ba_data/python/baclassic/_subsystem.py index f8aba3bc..6746251c 100644 --- a/src/assets/ba_data/python/baclassic/_subsystem.py +++ b/src/assets/ba_data/python/baclassic/_subsystem.py @@ -800,5 +800,6 @@ class ClassicSubsystem(babase.AppSubsystem): bauiv1.getsound('swish').play() babase.app.ui_v1.set_main_menu_window( - MainMenuWindow().get_root_widget() + MainMenuWindow().get_root_widget(), + from_window=False, # Disable check here. ) diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py index 82c27c87..4e411438 100644 --- a/src/assets/ba_data/python/baenv.py +++ b/src/assets/ba_data/python/baenv.py @@ -52,7 +52,7 @@ if TYPE_CHECKING: # Build number and version of the ballistica binary we expect to be # using. -TARGET_BALLISTICA_BUILD = 21636 +TARGET_BALLISTICA_BUILD = 21639 TARGET_BALLISTICA_VERSION = '1.7.30' diff --git a/src/assets/ba_data/python/bascenev1lib/mainmenu.py b/src/assets/ba_data/python/bascenev1lib/mainmenu.py index 88b85249..6092f0ce 100644 --- a/src/assets/ba_data/python/bascenev1lib/mainmenu.py +++ b/src/assets/ba_data/python/bascenev1lib/mainmenu.py @@ -317,7 +317,8 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): from bauiv1lib.kiosk import KioskWindow bs.app.ui_v1.set_main_menu_window( - KioskWindow().get_root_widget() + KioskWindow().get_root_widget(), + from_window=False, # Disable check here. ) # ..or in normal cases go back to the main menu else: @@ -326,14 +327,16 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): from bauiv1lib.gather import GatherWindow bs.app.ui_v1.set_main_menu_window( - GatherWindow(transition=None).get_root_widget() + GatherWindow(transition=None).get_root_widget(), + from_window=False, # Disable check here. ) elif main_menu_location == 'Watch': # pylint: disable=cyclic-import from bauiv1lib.watch import WatchWindow bs.app.ui_v1.set_main_menu_window( - WatchWindow(transition=None).get_root_widget() + WatchWindow(transition=None).get_root_widget(), + from_window=False, # Disable check here. ) elif main_menu_location == 'Team Game Select': # pylint: disable=cyclic-import @@ -344,7 +347,8 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): bs.app.ui_v1.set_main_menu_window( PlaylistBrowserWindow( sessiontype=bs.DualTeamSession, transition=None - ).get_root_widget() + ).get_root_widget(), + from_window=False, # Disable check here. ) elif main_menu_location == 'Free-for-All Game Select': # pylint: disable=cyclic-import @@ -356,28 +360,34 @@ class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): PlaylistBrowserWindow( sessiontype=bs.FreeForAllSession, transition=None, - ).get_root_widget() + ).get_root_widget(), + from_window=False, # Disable check here. ) elif main_menu_location == 'Coop Select': # pylint: disable=cyclic-import from bauiv1lib.coop.browser import CoopBrowserWindow bs.app.ui_v1.set_main_menu_window( - CoopBrowserWindow(transition=None).get_root_widget() + CoopBrowserWindow( + transition=None + ).get_root_widget(), + from_window=False, # Disable check here. ) elif main_menu_location == 'Benchmarks & Stress Tests': # pylint: disable=cyclic-import from bauiv1lib.debug import DebugWindow bs.app.ui_v1.set_main_menu_window( - DebugWindow(transition=None).get_root_widget() + DebugWindow(transition=None).get_root_widget(), + from_window=False, # Disable check here. ) else: # pylint: disable=cyclic-import from bauiv1lib.mainmenu import MainMenuWindow bs.app.ui_v1.set_main_menu_window( - MainMenuWindow(transition=None).get_root_widget() + MainMenuWindow(transition=None).get_root_widget(), + from_window=None, ) # attempt to show any pending offers immediately. diff --git a/src/assets/ba_data/python/bauiv1/_hooks.py b/src/assets/ba_data/python/bauiv1/_hooks.py index c7b6072a..30903564 100644 --- a/src/assets/ba_data/python/bauiv1/_hooks.py +++ b/src/assets/ba_data/python/bauiv1/_hooks.py @@ -6,6 +6,7 @@ from __future__ import annotations import logging +import inspect from typing import TYPE_CHECKING import _bauiv1 @@ -87,3 +88,19 @@ def show_url_window(address: str) -> None: return app.classic.show_url_window(address) + + +def double_transition_out_warning() -> None: + """Called if a widget is set to transition out twice.""" + caller_frame = inspect.stack()[1] + caller_filename = caller_frame.filename + caller_line_number = caller_frame.lineno + logging.warning( + 'ContainerWidget was set to transition out twice;' + ' this often implies buggy code (%s line %s).\n' + ' Generally you should check the value of' + ' _root_widget.transitioning_out and only kick off transitions' + ' when that is False.', + caller_filename, + caller_line_number, + ) diff --git a/src/assets/ba_data/python/bauiv1/_subsystem.py b/src/assets/ba_data/python/bauiv1/_subsystem.py index 83d88d87..b2645faf 100644 --- a/src/assets/ba_data/python/bauiv1/_subsystem.py +++ b/src/assets/ba_data/python/bauiv1/_subsystem.py @@ -5,6 +5,7 @@ from __future__ import annotations import logging +import inspect from typing import TYPE_CHECKING import babase @@ -116,21 +117,69 @@ class UIV1Subsystem(babase.AppSubsystem): # FIXME: Can probably kill this if we do immediate UI death checks. self.upkeeptimer = babase.AppTimer(2.6543, ui_upkeep, repeat=True) - def set_main_menu_window(self, window: bauiv1.Widget) -> None: - """Set the current 'main' window, replacing any existing.""" + def set_main_menu_window( + self, + window: bauiv1.Widget, + from_window: bauiv1.Widget | None | bool = True, + ) -> None: + """Set the current 'main' window, replacing any existing. + + If 'from_window' is passed as a bauiv1.Widget or None, a warning + will be issued if it that value does not match the current main + window. This can help clean up flawed code that can lead to bad + UI states. A value of False will disable the check. + """ + existing = self._main_menu_window - from inspect import currentframe, getframeinfo + + try: + if isinstance(from_window, bool): + # For default val True we warn that the arg wasn't + # passed. False can be explicitly passed to disable this + # check. + if from_window is True: + caller_frame = inspect.stack()[1] + caller_filename = caller_frame.filename + caller_line_number = caller_frame.lineno + logging.warning( + 'set_main_menu_window() should be passed a' + " 'from_window' value to help ensure proper UI behavior" + ' (%s line %i).', + caller_filename, + caller_line_number, + ) + else: + # For everything else, warn if what they passed wasn't + # the previous main menu widget. + if from_window is not existing: + caller_frame = inspect.stack()[1] + caller_filename = caller_frame.filename + caller_line_number = caller_frame.lineno + logging.warning( + "set_main_menu_window() was passed 'from_window' %s" + ' but existing main-menu-window is %s. (%s line %i).', + from_window, + existing, + caller_filename, + caller_line_number, + ) + except Exception: + # Prevent any bugs in these checks from causing problems. + logging.exception('Error checking from_window') + + # Once the above code leads to us fixing all leftover window bugs + # at the source, we can kill the code below. # Let's grab the location where we were called from to report # if we have to force-kill the existing window (which normally # should not happen). frameline = None try: - frame = currentframe() + frame = inspect.currentframe() if frame is not None: frame = frame.f_back if frame is not None: - frameinfo = getframeinfo(frame) + frameinfo = inspect.getframeinfo(frame) frameline = f'{frameinfo.filename} {frameinfo.lineno}' except Exception: logging.exception('Error calcing line for set_main_menu_window') @@ -167,6 +216,7 @@ class UIV1Subsystem(babase.AppSubsystem): ) else: self._main_menu_window.delete() + self._main_menu_window = None def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None: """(internal)""" diff --git a/src/assets/ba_data/python/bauiv1lib/account/settings.py b/src/assets/ba_data/python/bauiv1lib/account/settings.py index 8c29aee5..63937ef6 100644 --- a/src/assets/ba_data/python/bauiv1lib/account/settings.py +++ b/src/assets/ba_data/python/bauiv1lib/account/settings.py @@ -1507,9 +1507,18 @@ class AccountSettingsWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.profile.browser import ProfileBrowserWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') - ProfileBrowserWindow(origin_widget=self._player_profiles_button) + bui.app.ui_v1.set_main_menu_window( + ProfileBrowserWindow( + origin_widget=self._player_profiles_button + ).get_root_widget(), + from_window=self._root_widget, + ) def _cancel_sign_in_press(self) -> None: # If we're waiting on an adapter to give us credentials, abort. @@ -1670,6 +1679,10 @@ class AccountSettingsWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.mainmenu import MainMenuWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget( edit=self._root_widget, transition=self._transition_out @@ -1678,7 +1691,8 @@ class AccountSettingsWindow(bui.Window): if not self._modal: assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - MainMenuWindow(transition='in_left').get_root_widget() + MainMenuWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) def _save_state(self) -> None: diff --git a/src/assets/ba_data/python/bauiv1lib/coop/browser.py b/src/assets/ba_data/python/bauiv1lib/coop/browser.py index 20760381..256e6771 100644 --- a/src/assets/ba_data/python/bauiv1lib/coop/browser.py +++ b/src/assets/ba_data/python/bauiv1lib/coop/browser.py @@ -1019,6 +1019,10 @@ class CoopBrowserWindow(bui.Window): from bauiv1lib.account import show_sign_in_prompt from bauiv1lib.league.rankwindow import LeagueRankWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + plus = bui.app.plus assert plus is not None @@ -1032,7 +1036,8 @@ class CoopBrowserWindow(bui.Window): bui.app.ui_v1.set_main_menu_window( LeagueRankWindow( origin_widget=self._league_rank_button.get_button() - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _switch_to_score( @@ -1043,6 +1048,10 @@ class CoopBrowserWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.account import show_sign_in_prompt + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + plus = bui.app.plus assert plus is not None @@ -1058,7 +1067,8 @@ class CoopBrowserWindow(bui.Window): origin_widget=self._store_button.get_button(), show_tab=show_tab, back_location='CoopBrowserWindow', - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def is_tourney_data_up_to_date(self) -> bool: @@ -1218,6 +1228,10 @@ class CoopBrowserWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.play import PlayWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + # If something is selected, store it. self._save_state() bui.containerwidget( @@ -1225,7 +1239,8 @@ class CoopBrowserWindow(bui.Window): ) assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - PlayWindow(transition='in_left').get_root_widget() + PlayWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) def _save_state(self) -> None: diff --git a/src/assets/ba_data/python/bauiv1lib/creditslist.py b/src/assets/ba_data/python/bauiv1lib/creditslist.py index e6a0769f..c087dfbf 100644 --- a/src/assets/ba_data/python/bauiv1lib/creditslist.py +++ b/src/assets/ba_data/python/bauiv1lib/creditslist.py @@ -359,10 +359,15 @@ class CreditsListWindow(bui.Window): def _back(self) -> None: from bauiv1lib.mainmenu import MainMenuWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - MainMenuWindow(transition='in_left').get_root_widget() + MainMenuWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) diff --git a/src/assets/ba_data/python/bauiv1lib/debug.py b/src/assets/ba_data/python/bauiv1lib/debug.py index 7cd5e5d2..b397610f 100644 --- a/src/assets/ba_data/python/bauiv1lib/debug.py +++ b/src/assets/ba_data/python/bauiv1lib/debug.py @@ -379,8 +379,13 @@ class DebugWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.settings.advanced import AdvancedSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + bui.containerwidget(edit=self._root_widget, transition='out_right') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - AdvancedSettingsWindow(transition='in_left').get_root_widget() + AdvancedSettingsWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) diff --git a/src/assets/ba_data/python/bauiv1lib/gather/__init__.py b/src/assets/ba_data/python/bauiv1lib/gather/__init__.py index 3b5f02d6..8fc2e841 100644 --- a/src/assets/ba_data/python/bauiv1lib/gather/__init__.py +++ b/src/assets/ba_data/python/bauiv1lib/gather/__init__.py @@ -270,12 +270,17 @@ class GatherWindow(bui.Window): """Called by the private-hosting tab to select a playlist.""" from bauiv1lib.play import PlayWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.selecting_private_party_playlist = True bui.app.ui_v1.set_main_menu_window( - PlayWindow(origin_widget=origin_widget).get_root_widget() + PlayWindow(origin_widget=origin_widget).get_root_widget(), + from_window=self._root_widget, ) def _set_tab(self, tab_id: TabID) -> None: @@ -383,11 +388,16 @@ class GatherWindow(bui.Window): def _back(self) -> None: from bauiv1lib.mainmenu import MainMenuWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - MainMenuWindow(transition='in_left').get_root_widget() + MainMenuWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) diff --git a/src/assets/ba_data/python/bauiv1lib/getcurrency.py b/src/assets/ba_data/python/bauiv1lib/getcurrency.py index a87ec18f..78ab9af1 100644 --- a/src/assets/ba_data/python/bauiv1lib/getcurrency.py +++ b/src/assets/ba_data/python/bauiv1lib/getcurrency.py @@ -732,8 +732,13 @@ class GetCurrencyWindow(bui.Window): def _back(self) -> None: from bauiv1lib.store import browser + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + if self._transitioning_out: return + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) @@ -745,7 +750,9 @@ class GetCurrencyWindow(bui.Window): ).get_root_widget() if not self._from_modal_store: assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window(window) + bui.app.ui_v1.set_main_menu_window( + window, from_window=self._root_widget + ) self._transitioning_out = True diff --git a/src/assets/ba_data/python/bauiv1lib/helpui.py b/src/assets/ba_data/python/bauiv1lib/helpui.py index 6a7ed437..e2fc64ea 100644 --- a/src/assets/ba_data/python/bauiv1lib/helpui.py +++ b/src/assets/ba_data/python/bauiv1lib/helpui.py @@ -645,11 +645,16 @@ class HelpWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.mainmenu import MainMenuWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) if self._main_menu: assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - MainMenuWindow(transition='in_left').get_root_widget() + MainMenuWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) diff --git a/src/assets/ba_data/python/bauiv1lib/kiosk.py b/src/assets/ba_data/python/bauiv1lib/kiosk.py index 377a86d0..ab1ca87e 100644 --- a/src/assets/ba_data/python/bauiv1lib/kiosk.py +++ b/src/assets/ba_data/python/bauiv1lib/kiosk.py @@ -501,9 +501,15 @@ class KioskWindow(bui.Window): def _do_full_menu(self) -> None: from bauiv1lib.mainmenu import MainMenuWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + assert bui.app.classic is not None self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') bui.app.classic.did_menu_intro = True # prevent delayed transition-in - bui.app.ui_v1.set_main_menu_window(MainMenuWindow().get_root_widget()) + bui.app.ui_v1.set_main_menu_window( + MainMenuWindow().get_root_widget(), from_window=self._root_widget + ) diff --git a/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py b/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py index d108cbb3..725bb318 100644 --- a/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py +++ b/src/assets/ba_data/python/bauiv1lib/league/rankwindow.py @@ -1142,6 +1142,10 @@ class LeagueRankWindow(bui.Window): def _back(self) -> None: from bauiv1lib.coop.browser import CoopBrowserWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget( edit=self._root_widget, transition=self._transition_out @@ -1149,5 +1153,6 @@ class LeagueRankWindow(bui.Window): if not self._modal: assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - CoopBrowserWindow(transition='in_left').get_root_widget() + CoopBrowserWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) diff --git a/src/assets/ba_data/python/bauiv1lib/mainmenu.py b/src/assets/ba_data/python/bauiv1lib/mainmenu.py index 7e6ca3d4..16f318ae 100644 --- a/src/assets/ba_data/python/bauiv1lib/mainmenu.py +++ b/src/assets/ba_data/python/bauiv1lib/mainmenu.py @@ -1038,6 +1038,10 @@ class MainMenuWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.confirm import QuitWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + # Note: Normally we should go through bui.quit(confirm=True) but # invoking the window directly lets us scale it up from the # button. @@ -1047,24 +1051,34 @@ class MainMenuWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.kiosk import KioskWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_right') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - KioskWindow(transition='in_left').get_root_widget() + KioskWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) def _show_account_window(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.account.settings import AccountSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( AccountSettingsWindow( origin_widget=self._account_button - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _on_store_pressed(self) -> None: @@ -1072,6 +1086,10 @@ class MainMenuWindow(bui.Window): from bauiv1lib.store.browser import StoreBrowserWindow from bauiv1lib.account import show_sign_in_prompt + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + plus = bui.app.plus assert plus is not None @@ -1084,7 +1102,8 @@ class MainMenuWindow(bui.Window): bui.app.ui_v1.set_main_menu_window( StoreBrowserWindow( origin_widget=self._store_button - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _is_benchmark(self) -> bool: @@ -1149,8 +1168,11 @@ class MainMenuWindow(bui.Window): def _end_game(self) -> None: assert bui.app.classic is not None - if not self._root_widget: + + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: return + bui.containerwidget(edit=self._root_widget, transition='out_left') bui.app.classic.return_to_main_menu_session_gracefully(reset_ui=False) @@ -1166,39 +1188,54 @@ class MainMenuWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.creditslist import CreditsListWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( CreditsListWindow( origin_widget=self._credits_button - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _howtoplay(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.helpui import HelpWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( HelpWindow( main_menu=True, origin_widget=self._how_to_play_button - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _settings(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.settings.allsettings import AllSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( AllSettingsWindow( origin_widget=self._settings_button - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _resume_and_call(self, call: Callable[[], Any]) -> None: @@ -1281,35 +1318,50 @@ class MainMenuWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.gather import GatherWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - GatherWindow(origin_widget=self._gather_button).get_root_widget() + GatherWindow(origin_widget=self._gather_button).get_root_widget(), + from_window=self._root_widget, ) def _watch_press(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.watch import WatchWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - WatchWindow(origin_widget=self._watch_button).get_root_widget() + WatchWindow(origin_widget=self._watch_button).get_root_widget(), + from_window=self._root_widget, ) def _play_press(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.play import PlayWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.selecting_private_party_playlist = False bui.app.ui_v1.set_main_menu_window( - PlayWindow(origin_widget=self._start_button).get_root_widget() + PlayWindow(origin_widget=self._start_button).get_root_widget(), + from_window=self._root_widget, ) def _resume(self) -> None: diff --git a/src/assets/ba_data/python/bauiv1lib/play.py b/src/assets/ba_data/python/bauiv1lib/play.py index ef1ca896..f06a9d59 100644 --- a/src/assets/ba_data/python/bauiv1lib/play.py +++ b/src/assets/ba_data/python/bauiv1lib/play.py @@ -521,13 +521,19 @@ class PlayWindow(bui.Window): def _back(self) -> None: # pylint: disable=cyclic-import + + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + if self._is_main_menu: from bauiv1lib.mainmenu import MainMenuWindow self._save_state() assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - MainMenuWindow(transition='in_left').get_root_widget() + MainMenuWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) bui.containerwidget( edit=self._root_widget, transition=self._transition_out @@ -538,7 +544,8 @@ class PlayWindow(bui.Window): self._save_state() assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - GatherWindow(transition='in_left').get_root_widget() + GatherWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) bui.containerwidget( edit=self._root_widget, transition=self._transition_out @@ -549,6 +556,10 @@ class PlayWindow(bui.Window): from bauiv1lib.account import show_sign_in_prompt from bauiv1lib.coop.browser import CoopBrowserWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + plus = bui.app.plus assert plus is not None @@ -559,26 +570,38 @@ class PlayWindow(bui.Window): bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - CoopBrowserWindow(origin_widget=self._coop_button).get_root_widget() + CoopBrowserWindow( + origin_widget=self._coop_button + ).get_root_widget(), + from_window=self._root_widget, ) def _team_tourney(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.playlist.browser import PlaylistBrowserWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( PlaylistBrowserWindow( origin_widget=self._teams_button, sessiontype=bs.DualTeamSession - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _free_for_all(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.playlist.browser import PlaylistBrowserWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None @@ -586,7 +609,8 @@ class PlayWindow(bui.Window): PlaylistBrowserWindow( origin_widget=self._free_for_all_button, sessiontype=bs.FreeForAllSession, - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _draw_dude( diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/browser.py b/src/assets/ba_data/python/bauiv1lib/playlist/browser.py index 287496c2..8a61e91c 100644 --- a/src/assets/ba_data/python/bauiv1lib/playlist/browser.py +++ b/src/assets/ba_data/python/bauiv1lib/playlist/browser.py @@ -684,6 +684,10 @@ class PlaylistBrowserWindow(bui.Window): PlaylistCustomizeBrowserWindow, ) + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None @@ -691,13 +695,18 @@ class PlaylistBrowserWindow(bui.Window): PlaylistCustomizeBrowserWindow( origin_widget=self._customize_button, sessiontype=self._sessiontype, - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _on_back_press(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.play import PlayWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + # Store our selected playlist if that's changed. if self._selected_playlist is not None: prev_sel = bui.app.config.get( @@ -716,7 +725,8 @@ class PlaylistBrowserWindow(bui.Window): ) assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - PlayWindow(transition='in_left').get_root_widget() + PlayWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) def _save_state(self) -> None: diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/customizebrowser.py b/src/assets/ba_data/python/bauiv1lib/playlist/customizebrowser.py index 789e7f2e..f0a433b5 100644 --- a/src/assets/ba_data/python/bauiv1lib/playlist/customizebrowser.py +++ b/src/assets/ba_data/python/bauiv1lib/playlist/customizebrowser.py @@ -323,6 +323,10 @@ class PlaylistCustomizeBrowserWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.playlist import browser + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + if self._selected_playlist_name is not None: cfg = bui.app.config cfg[ @@ -337,7 +341,8 @@ class PlaylistCustomizeBrowserWindow(bui.Window): bui.app.ui_v1.set_main_menu_window( browser.PlaylistBrowserWindow( transition='in_left', sessiontype=self._sessiontype - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _select(self, name: str, index: int) -> None: diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/edit.py b/src/assets/ba_data/python/bauiv1lib/playlist/edit.py index 1b971514..07f7adae 100644 --- a/src/assets/ba_data/python/bauiv1lib/playlist/edit.py +++ b/src/assets/ba_data/python/bauiv1lib/playlist/edit.py @@ -283,6 +283,10 @@ class PlaylistEditWindow(bui.Window): PlaylistCustomizeBrowserWindow, ) + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + bui.getsound('powerdown01').play() bui.containerwidget(edit=self._root_widget, transition='out_right') assert bui.app.classic is not None @@ -293,7 +297,8 @@ class PlaylistEditWindow(bui.Window): select_playlist=( self._editcontroller.get_existing_playlist_name() ), - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _add(self) -> None: @@ -315,6 +320,10 @@ class PlaylistEditWindow(bui.Window): PlaylistCustomizeBrowserWindow, ) + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + plus = bui.app.plus assert plus is not None @@ -380,7 +389,8 @@ class PlaylistEditWindow(bui.Window): transition='in_left', sessiontype=self._editcontroller.get_session_type(), select_playlist=new_name, - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _save_press_with_sound(self) -> None: diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/editcontroller.py b/src/assets/ba_data/python/bauiv1lib/playlist/editcontroller.py index 26e53b27..7ed9a92a 100644 --- a/src/assets/ba_data/python/bauiv1lib/playlist/editcontroller.py +++ b/src/assets/ba_data/python/bauiv1lib/playlist/editcontroller.py @@ -92,7 +92,8 @@ class PlaylistEditController: bui.app.ui_v1.set_main_menu_window( PlaylistEditWindow( editcontroller=self, transition=transition - ).get_root_widget() + ).get_root_widget(), + from_window=False, # Disable this check. ) def get_config_name(self) -> str: @@ -150,7 +151,8 @@ class PlaylistEditController: assert bui.app.classic is not None bui.app.ui_v1.clear_main_menu_window(transition='out_left') bui.app.ui_v1.set_main_menu_window( - PlaylistAddGameWindow(editcontroller=self).get_root_widget() + PlaylistAddGameWindow(editcontroller=self).get_root_widget(), + from_window=None, ) def edit_game_pressed(self) -> None: @@ -175,7 +177,8 @@ class PlaylistEditController: bui.app.ui_v1.set_main_menu_window( PlaylistEditWindow( editcontroller=self, transition='in_left' - ).get_root_widget() + ).get_root_widget(), + from_window=None, ) def _show_edit_ui( @@ -205,7 +208,8 @@ class PlaylistEditController: bui.app.ui_v1.set_main_menu_window( PlaylistEditWindow( editcontroller=self, transition='in_left' - ).get_root_widget() + ).get_root_widget(), + from_window=None, ) # Otherwise we were adding; go back to the add type choice list. @@ -214,7 +218,8 @@ class PlaylistEditController: bui.app.ui_v1.set_main_menu_window( PlaylistAddGameWindow( editcontroller=self, transition='in_left' - ).get_root_widget() + ).get_root_widget(), + from_window=None, ) else: # Make sure type is in there. @@ -236,5 +241,6 @@ class PlaylistEditController: bui.app.ui_v1.set_main_menu_window( PlaylistEditWindow( editcontroller=self, transition='in_left' - ).get_root_widget() + ).get_root_widget(), + from_window=None, ) diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/editgame.py b/src/assets/ba_data/python/bauiv1lib/playlist/editgame.py index 4e7c3d84..cb951624 100644 --- a/src/assets/ba_data/python/bauiv1lib/playlist/editgame.py +++ b/src/assets/ba_data/python/bauiv1lib/playlist/editgame.py @@ -514,6 +514,10 @@ class PlaylistEditGameWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.playlist.mapselect import PlaylistMapSelectWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + # Replace ourself with the map-select UI. bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None @@ -524,7 +528,8 @@ class PlaylistEditGameWindow(bui.Window): copy.deepcopy(self._getconfig()), self._edit_info, self._completion_call, - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _choice_inc( diff --git a/src/assets/ba_data/python/bauiv1lib/playlist/mapselect.py b/src/assets/ba_data/python/bauiv1lib/playlist/mapselect.py index c86854d6..4d351379 100644 --- a/src/assets/ba_data/python/bauiv1lib/playlist/mapselect.py +++ b/src/assets/ba_data/python/bauiv1lib/playlist/mapselect.py @@ -273,6 +273,10 @@ class PlaylistMapSelectWindow(bui.Window): def _select(self, map_name: str) -> None: from bauiv1lib.playlist.editgame import PlaylistEditGameWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._config['settings']['map'] = map_name bui.containerwidget(edit=self._root_widget, transition='out_right') assert bui.app.classic is not None @@ -285,7 +289,8 @@ class PlaylistMapSelectWindow(bui.Window): default_selection='map', transition='in_left', edit_info=self._edit_info, - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _select_with_delay(self, map_name: str) -> None: @@ -296,6 +301,10 @@ class PlaylistMapSelectWindow(bui.Window): def _cancel(self) -> None: from bauiv1lib.playlist.editgame import PlaylistEditGameWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + bui.containerwidget(edit=self._root_widget, transition='out_right') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( @@ -307,5 +316,6 @@ class PlaylistMapSelectWindow(bui.Window): default_selection='map', transition='in_left', edit_info=self._edit_info, - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) diff --git a/src/assets/ba_data/python/bauiv1lib/playoptions.py b/src/assets/ba_data/python/bauiv1lib/playoptions.py index fdc79e74..12fd4e3e 100644 --- a/src/assets/ba_data/python/bauiv1lib/playoptions.py +++ b/src/assets/ba_data/python/bauiv1lib/playoptions.py @@ -140,7 +140,6 @@ class PlayOptionsWindow(PopupWindow): if show_shuffle_check_box: self._height += 40 - # Creates our _root_widget. uiscale = bui.app.ui_v1.uiscale scale = ( 1.69 @@ -149,6 +148,7 @@ class PlayOptionsWindow(PopupWindow): if uiscale is bui.UIScale.MEDIUM else 0.85 ) + # Creates our _root_widget. super().__init__( position=scale_origin, size=(self._width, self._height), scale=scale ) @@ -448,6 +448,10 @@ class PlayOptionsWindow(PopupWindow): self._transition_out() def _on_ok_press(self) -> None: + # no-op if our underlying widget is dead or on its way out. + if not self.root_widget or self.root_widget.transitioning_out: + return + # Disallow if our playlist has disappeared. if not self._does_target_playlist_exist(): return @@ -479,7 +483,8 @@ class PlayOptionsWindow(PopupWindow): bui.getsound('gunCocking').play() assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - GatherWindow(transition='in_right').get_root_widget() + GatherWindow(transition='in_right').get_root_widget(), + from_window=self.root_widget, ) self._transition_out(transition='out_left') if self._delegate is not None: diff --git a/src/assets/ba_data/python/bauiv1lib/profile/browser.py b/src/assets/ba_data/python/bauiv1lib/profile/browser.py index 0fc51e14..2f47251e 100644 --- a/src/assets/ba_data/python/bauiv1lib/profile/browser.py +++ b/src/assets/ba_data/python/bauiv1lib/profile/browser.py @@ -212,6 +212,10 @@ class ProfileBrowserWindow(bui.Window): from bauiv1lib.profile.edit import EditProfileWindow from bauiv1lib.purchase import PurchaseWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + plus = bui.app.plus assert plus is not None @@ -252,7 +256,8 @@ class ProfileBrowserWindow(bui.Window): bui.app.ui_v1.set_main_menu_window( EditProfileWindow( existing_profile=None, in_main_menu=self._in_main_menu - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _delete_profile(self) -> None: @@ -301,6 +306,10 @@ class ProfileBrowserWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.profile.edit import EditProfileWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + if self._selected_profile is None: bui.getsound('error').play() bui.screenmessage( @@ -313,7 +322,8 @@ class ProfileBrowserWindow(bui.Window): bui.app.ui_v1.set_main_menu_window( EditProfileWindow( self._selected_profile, in_main_menu=self._in_main_menu - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _select(self, name: str, index: int) -> None: @@ -324,6 +334,10 @@ class ProfileBrowserWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.account.settings import AccountSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + assert bui.app.classic is not None self._save_state() @@ -333,7 +347,8 @@ class ProfileBrowserWindow(bui.Window): if self._in_main_menu: assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - AccountSettingsWindow(transition='in_left').get_root_widget() + AccountSettingsWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) # If we're being called up standalone, handle pause/resume ourself. diff --git a/src/assets/ba_data/python/bauiv1lib/profile/edit.py b/src/assets/ba_data/python/bauiv1lib/profile/edit.py index 5c9c899d..b216d1f9 100644 --- a/src/assets/ba_data/python/bauiv1lib/profile/edit.py +++ b/src/assets/ba_data/python/bauiv1lib/profile/edit.py @@ -18,12 +18,18 @@ class EditProfileWindow(bui.Window): # FIXME: WILL NEED TO CHANGE THIS FOR UILOCATION. def reload_window(self) -> None: """Transitions out and recreates ourself.""" + + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( EditProfileWindow( self.getname(), self._in_main_menu - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def __init__( @@ -653,6 +659,10 @@ class EditProfileWindow(bui.Window): def _cancel(self) -> None: from bauiv1lib.profile.browser import ProfileBrowserWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + bui.containerwidget(edit=self._root_widget, transition='out_right') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( @@ -660,7 +670,8 @@ class EditProfileWindow(bui.Window): 'in_left', selected_profile=self._existing_profile, in_main_menu=self._in_main_menu, - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _set_color(self, color: tuple[float, float, float]) -> None: @@ -759,6 +770,10 @@ class EditProfileWindow(bui.Window): """Save has been selected.""" from bauiv1lib.profile.browser import ProfileBrowserWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return False + plus = bui.app.plus assert plus is not None @@ -808,6 +823,7 @@ class EditProfileWindow(bui.Window): 'in_left', selected_profile=new_name, in_main_menu=self._in_main_menu, - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) return True diff --git a/src/assets/ba_data/python/bauiv1lib/promocode.py b/src/assets/ba_data/python/bauiv1lib/promocode.py index 3e7f035c..3cf745b5 100644 --- a/src/assets/ba_data/python/bauiv1lib/promocode.py +++ b/src/assets/ba_data/python/bauiv1lib/promocode.py @@ -142,13 +142,18 @@ class PromoCodeWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.settings.advanced import AdvancedSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) if not self._modal: assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - AdvancedSettingsWindow(transition='in_left').get_root_widget() + AdvancedSettingsWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) def _activate_enter_button(self) -> None: @@ -158,6 +163,10 @@ class PromoCodeWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.settings.advanced import AdvancedSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + plus = bui.app.plus assert plus is not None @@ -167,7 +176,8 @@ class PromoCodeWindow(bui.Window): if not self._modal: assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - AdvancedSettingsWindow(transition='in_left').get_root_widget() + AdvancedSettingsWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) plus.add_v1_account_transaction( { diff --git a/src/assets/ba_data/python/bauiv1lib/settings/advanced.py b/src/assets/ba_data/python/bauiv1lib/settings/advanced.py index e7724815..0db77056 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/advanced.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/advanced.py @@ -682,11 +682,16 @@ class AdvancedSettingsWindow(bui.Window): def _on_vr_test_press(self) -> None: from bauiv1lib.settings.vrtesting import VRTestingWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - VRTestingWindow(transition='in_right').get_root_widget() + VRTestingWindow(transition='in_right').get_root_widget(), + from_window=self._root_widget, ) def _on_net_test_press(self) -> None: @@ -694,6 +699,10 @@ class AdvancedSettingsWindow(bui.Window): assert plus is not None from bauiv1lib.settings.nettesting import NetTestingWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + # Net-testing requires a signed in v1 account. if plus.get_v1_account_state() != 'signed_in': bui.screenmessage( @@ -706,7 +715,8 @@ class AdvancedSettingsWindow(bui.Window): bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - NetTestingWindow(transition='in_right').get_root_widget() + NetTestingWindow(transition='in_right').get_root_widget(), + from_window=self._root_widget, ) def _on_friend_promo_code_press(self) -> None: @@ -724,17 +734,26 @@ class AdvancedSettingsWindow(bui.Window): def _on_plugins_button_press(self) -> None: from bauiv1lib.settings.plugins import PluginWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - PluginWindow(origin_widget=self._plugins_button).get_root_widget() + PluginWindow(origin_widget=self._plugins_button).get_root_widget(), + from_window=self._root_widget, ) def _on_promo_code_press(self) -> None: from bauiv1lib.promocode import PromoCodeWindow from bauiv1lib.account import show_sign_in_prompt + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + plus = bui.app.plus assert plus is not None @@ -742,23 +761,30 @@ class AdvancedSettingsWindow(bui.Window): if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( PromoCodeWindow( origin_widget=self._promo_code_button - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _on_benchmark_press(self) -> None: from bauiv1lib.debug import DebugWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - DebugWindow(transition='in_right').get_root_widget() + DebugWindow(transition='in_right').get_root_widget(), + from_window=self._root_widget, ) def _save_state(self) -> None: @@ -908,11 +934,16 @@ class AdvancedSettingsWindow(bui.Window): def _do_back(self) -> None: from bauiv1lib.settings.allsettings import AllSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - AllSettingsWindow(transition='in_left').get_root_widget() + AllSettingsWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) diff --git a/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py b/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py index d6d18b5f..75e0f633 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/allsettings.py @@ -235,65 +235,90 @@ class AllSettingsWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.mainmenu import MainMenuWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - MainMenuWindow(transition='in_left').get_root_widget() + MainMenuWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) def _do_controllers(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.settings.controls import ControlsSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( ControlsSettingsWindow( origin_widget=self._controllers_button - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _do_graphics(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.settings.graphics import GraphicsSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( GraphicsSettingsWindow( origin_widget=self._graphics_button - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _do_audio(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.settings.audio import AudioSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( AudioSettingsWindow( origin_widget=self._audio_button - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _do_advanced(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.settings.advanced import AdvancedSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( AdvancedSettingsWindow( origin_widget=self._advanced_button - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _save_state(self) -> None: diff --git a/src/assets/ba_data/python/bauiv1lib/settings/audio.py b/src/assets/ba_data/python/bauiv1lib/settings/audio.py index 643e23fa..fc39b719 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/audio.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/audio.py @@ -237,6 +237,10 @@ class AudioSettingsWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.soundtrack import browser as stb + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + # We require disk access for soundtracks; # if we don't have it, request it. if not bui.have_permission(bui.Permission.STORAGE): @@ -256,13 +260,18 @@ class AudioSettingsWindow(bui.Window): bui.app.ui_v1.set_main_menu_window( stb.SoundtrackBrowserWindow( origin_widget=self._soundtrack_button - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _back(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.settings import allsettings + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget( edit=self._root_widget, transition=self._transition_out @@ -271,7 +280,8 @@ class AudioSettingsWindow(bui.Window): bui.app.ui_v1.set_main_menu_window( allsettings.AllSettingsWindow( transition='in_left' - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _save_state(self) -> None: diff --git a/src/assets/ba_data/python/bauiv1lib/settings/controls.py b/src/assets/ba_data/python/bauiv1lib/settings/controls.py index 168df15c..108657f5 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/controls.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/controls.py @@ -367,59 +367,84 @@ class ControlsSettingsWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.settings.keyboard import ConfigKeyboardWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( ConfigKeyboardWindow( bs.getinputdevice('Keyboard', '#1') - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _config_keyboard2(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.settings.keyboard import ConfigKeyboardWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( ConfigKeyboardWindow( bs.getinputdevice('Keyboard', '#2') - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _do_mobile_devices(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.settings.remoteapp import RemoteAppSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - RemoteAppSettingsWindow().get_root_widget() + RemoteAppSettingsWindow().get_root_widget(), + from_window=self._root_widget, ) def _do_gamepads(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.settings.gamepadselect import GamepadSelectWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - GamepadSelectWindow().get_root_widget() + GamepadSelectWindow().get_root_widget(), + from_window=self._root_widget, ) def _do_touchscreen(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.settings.touchscreen import TouchscreenSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - TouchscreenSettingsWindow().get_root_widget() + TouchscreenSettingsWindow().get_root_widget(), + from_window=self._root_widget, ) def _save_state(self) -> None: @@ -466,11 +491,16 @@ class ControlsSettingsWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.settings.allsettings import AllSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - AllSettingsWindow(transition='in_left').get_root_widget() + AllSettingsWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) diff --git a/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py b/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py index 3c67b23f..a63847d4 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/gamepad.py @@ -795,19 +795,28 @@ class GamepadSettingsWindow(bui.Window): def _cancel(self) -> None: from bauiv1lib.settings.controls import ControlsSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) if self._is_main_menu: assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - ControlsSettingsWindow(transition='in_left').get_root_widget() + ControlsSettingsWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) def _save(self) -> None: classic = bui.app.classic assert classic is not None + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) @@ -852,7 +861,8 @@ class GamepadSettingsWindow(bui.Window): assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - ControlsSettingsWindow(transition='in_left').get_root_widget() + ControlsSettingsWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) diff --git a/src/assets/ba_data/python/bauiv1lib/settings/gamepadselect.py b/src/assets/ba_data/python/bauiv1lib/settings/gamepadselect.py index b576d422..d0dba0ac 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/gamepadselect.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/gamepadselect.py @@ -33,7 +33,8 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None: assert isinstance(device, bs.InputDevice) if device.allows_configuring: bui.app.ui_v1.set_main_menu_window( - gamepad.GamepadSettingsWindow(device).get_root_widget() + gamepad.GamepadSettingsWindow(device).get_root_widget(), + from_window=None, ) else: width = 700 @@ -51,7 +52,7 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None: size=(width, height), transition='in_right', ) - bui.app.ui_v1.set_main_menu_window(dlg) + bui.app.ui_v1.set_main_menu_window(dlg, from_window=None) if device.allows_configuring_in_system_settings: msg = bui.Lstr( @@ -81,12 +82,17 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None: def _ok() -> None: from bauiv1lib.settings import controls + # no-op if our underlying widget is dead or on its way out. + if not dlg or dlg.transitioning_out: + return + bui.containerwidget(edit=dlg, transition='out_right') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( controls.ControlsSettingsWindow( transition='in_left' - ).get_root_widget() + ).get_root_widget(), + from_window=dlg, ) bui.buttonwidget( @@ -191,11 +197,16 @@ class GamepadSelectWindow(bui.Window): def _back(self) -> None: from bauiv1lib.settings import controls + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + bs.release_gamepad_input() bui.containerwidget(edit=self._root_widget, transition='out_right') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( controls.ControlsSettingsWindow( transition='in_left' - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) diff --git a/src/assets/ba_data/python/bauiv1lib/settings/graphics.py b/src/assets/ba_data/python/bauiv1lib/settings/graphics.py index 7e20a537..f441826c 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/graphics.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/graphics.py @@ -436,6 +436,10 @@ class GraphicsSettingsWindow(bui.Window): def _back(self) -> None: from bauiv1lib.settings import allsettings + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + # Applying max-fps takes a few moments. Apply if it hasn't been # yet. self._apply_max_fps() @@ -447,7 +451,8 @@ class GraphicsSettingsWindow(bui.Window): bui.app.ui_v1.set_main_menu_window( allsettings.AllSettingsWindow( transition='in_left' - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _set_quality(self, quality: str) -> None: diff --git a/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py b/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py index 60aab600..1e564378 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/keyboard.py @@ -271,15 +271,24 @@ class ConfigKeyboardWindow(bui.Window): def _cancel(self) -> None: from bauiv1lib.settings.controls import ControlsSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + bui.containerwidget(edit=self._root_widget, transition='out_right') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - ControlsSettingsWindow(transition='in_left').get_root_widget() + ControlsSettingsWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) def _save(self) -> None: from bauiv1lib.settings.controls import ControlsSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + assert bui.app.classic is not None bui.containerwidget(edit=self._root_widget, transition='out_right') bui.getsound('gunCocking').play() @@ -314,7 +323,8 @@ class ConfigKeyboardWindow(bui.Window): ) bui.app.config.apply_and_commit() bui.app.ui_v1.set_main_menu_window( - ControlsSettingsWindow(transition='in_left').get_root_widget() + ControlsSettingsWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) diff --git a/src/assets/ba_data/python/bauiv1lib/settings/nettesting.py b/src/assets/ba_data/python/bauiv1lib/settings/nettesting.py index dd295108..e4e6e996 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/nettesting.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/nettesting.py @@ -135,8 +135,14 @@ class NetTestingWindow(bui.Window): def _show_val_testing(self) -> None: assert bui.app.classic is not None + + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + bui.app.ui_v1.set_main_menu_window( - NetValTestingWindow().get_root_widget() + NetValTestingWindow().get_root_widget(), + from_window=self._root_widget, ) bui.containerwidget(edit=self._root_widget, transition='out_left') @@ -144,9 +150,14 @@ class NetTestingWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.settings.advanced import AdvancedSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - AdvancedSettingsWindow(transition='in_left').get_root_widget() + AdvancedSettingsWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) bui.containerwidget(edit=self._root_widget, transition='out_right') diff --git a/src/assets/ba_data/python/bauiv1lib/settings/plugins.py b/src/assets/ba_data/python/bauiv1lib/settings/plugins.py index 5f22847b..c2327bbe 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/plugins.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/plugins.py @@ -212,11 +212,16 @@ class PluginWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.settings.pluginsettings import PluginSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget(edit=self._root_widget, transition='out_left') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - PluginSettingsWindow(transition='in_right').get_root_widget() + PluginSettingsWindow(transition='in_right').get_root_widget(), + from_window=self._root_widget, ) def _show_category_options(self) -> None: @@ -412,11 +417,16 @@ class PluginWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.settings.advanced import AdvancedSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - AdvancedSettingsWindow(transition='in_left').get_root_widget() + AdvancedSettingsWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) diff --git a/src/assets/ba_data/python/bauiv1lib/settings/pluginsettings.py b/src/assets/ba_data/python/bauiv1lib/settings/pluginsettings.py index 03e9e9e8..1474f5cb 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/pluginsettings.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/pluginsettings.py @@ -161,10 +161,15 @@ class PluginSettingsWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.settings.plugins import PluginWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - PluginWindow(transition='in_left').get_root_widget() + PluginWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) diff --git a/src/assets/ba_data/python/bauiv1lib/settings/remoteapp.py b/src/assets/ba_data/python/bauiv1lib/settings/remoteapp.py index 03b1c611..3542f992 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/remoteapp.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/remoteapp.py @@ -138,10 +138,15 @@ class RemoteAppSettingsWindow(bui.Window): def _back(self) -> None: from bauiv1lib.settings import controls + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + bui.containerwidget(edit=self._root_widget, transition='out_right') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( controls.ControlsSettingsWindow( transition='in_left' - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) diff --git a/src/assets/ba_data/python/bauiv1lib/settings/testing.py b/src/assets/ba_data/python/bauiv1lib/settings/testing.py index c4bcd58a..30f11e28 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/testing.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/testing.py @@ -217,6 +217,10 @@ class TestingWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.settings.advanced import AdvancedSettingsWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + bui.containerwidget(edit=self._root_widget, transition='out_right') backwin = ( self._back_call() @@ -224,4 +228,6 @@ class TestingWindow(bui.Window): else AdvancedSettingsWindow(transition='in_left') ) assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window(backwin.get_root_widget()) + bui.app.ui_v1.set_main_menu_window( + backwin.get_root_widget(), from_window=self._root_widget + ) diff --git a/src/assets/ba_data/python/bauiv1lib/settings/touchscreen.py b/src/assets/ba_data/python/bauiv1lib/settings/touchscreen.py index 61041bbd..d77a16a2 100644 --- a/src/assets/ba_data/python/bauiv1lib/settings/touchscreen.py +++ b/src/assets/ba_data/python/bauiv1lib/settings/touchscreen.py @@ -276,11 +276,16 @@ class TouchscreenSettingsWindow(bui.Window): def _back(self) -> None: from bauiv1lib.settings import controls + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + bui.containerwidget(edit=self._root_widget, transition='out_right') assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( controls.ControlsSettingsWindow( transition='in_left' - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) bs.set_touchscreen_editing(False) diff --git a/src/assets/ba_data/python/bauiv1lib/soundtrack/browser.py b/src/assets/ba_data/python/bauiv1lib/soundtrack/browser.py index 021942e6..a3b561b4 100644 --- a/src/assets/ba_data/python/bauiv1lib/soundtrack/browser.py +++ b/src/assets/ba_data/python/bauiv1lib/soundtrack/browser.py @@ -394,13 +394,18 @@ class SoundtrackBrowserWindow(bui.Window): # pylint: disable=cyclic-import from bauiv1lib.settings import audio + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - audio.AudioSettingsWindow(transition='in_left').get_root_widget() + audio.AudioSettingsWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) def _edit_soundtrack_with_sound(self) -> None: @@ -421,6 +426,10 @@ class SoundtrackBrowserWindow(bui.Window): from bauiv1lib.purchase import PurchaseWindow from bauiv1lib.soundtrack.edit import SoundtrackEditWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + if ( bui.app.classic is not None and not bui.app.classic.accounts.have_pro_options() @@ -443,7 +452,8 @@ class SoundtrackBrowserWindow(bui.Window): bui.app.ui_v1.set_main_menu_window( SoundtrackEditWindow( existing_soundtrack=self._selected_soundtrack - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _get_soundtrack_display_name(self, soundtrack: str) -> bui.Lstr: diff --git a/src/assets/ba_data/python/bauiv1lib/soundtrack/edit.py b/src/assets/ba_data/python/bauiv1lib/soundtrack/edit.py index 0e1088b4..8c3887eb 100644 --- a/src/assets/ba_data/python/bauiv1lib/soundtrack/edit.py +++ b/src/assets/ba_data/python/bauiv1lib/soundtrack/edit.py @@ -351,7 +351,8 @@ class SoundtrackEditWindow(bui.Window): soundtrack[musictype] = entry bui.app.ui_v1.set_main_menu_window( - cls(state, transition='in_left').get_root_widget() + cls(state, transition='in_left').get_root_widget(), + from_window=False, # Disable check here. ) def _get_entry( @@ -359,6 +360,11 @@ class SoundtrackEditWindow(bui.Window): ) -> None: assert bui.app.classic is not None music = bui.app.classic.music + + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + if selection_target_name != '': selection_target_name = "'" + selection_target_name + "'" state = { @@ -375,7 +381,8 @@ class SoundtrackEditWindow(bui.Window): entry, selection_target_name, ) - .get_root_widget() + .get_root_widget(), + from_window=self._root_widget, ) def _test(self, song_type: bs.MusicType) -> None: @@ -422,6 +429,10 @@ class SoundtrackEditWindow(bui.Window): def _cancel(self) -> None: from bauiv1lib.soundtrack import browser as stb + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + assert bui.app.classic is not None music = bui.app.classic.music @@ -429,12 +440,17 @@ class SoundtrackEditWindow(bui.Window): music.set_music_play_mode(bui.app.classic.MusicPlayMode.REGULAR) bui.containerwidget(edit=self._root_widget, transition='out_right') bui.app.ui_v1.set_main_menu_window( - stb.SoundtrackBrowserWindow(transition='in_left').get_root_widget() + stb.SoundtrackBrowserWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) def _do_it(self) -> None: from bauiv1lib.soundtrack import browser as stb + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + assert bui.app.classic is not None music = bui.app.classic.music cfg = bui.app.config @@ -483,7 +499,8 @@ class SoundtrackEditWindow(bui.Window): ) bui.app.ui_v1.set_main_menu_window( - stb.SoundtrackBrowserWindow(transition='in_left').get_root_widget() + stb.SoundtrackBrowserWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) def _do_it_with_sound(self) -> None: diff --git a/src/assets/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py b/src/assets/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py index 2adaa721..583855a8 100644 --- a/src/assets/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py +++ b/src/assets/ba_data/python/bauiv1lib/soundtrack/entrytypeselect.py @@ -166,6 +166,10 @@ class SoundtrackEntryTypeSelectWindow(bui.Window): MacMusicAppPlaylistSelectWindow, ) + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + bui.containerwidget(edit=self._root_widget, transition='out_left') current_playlist_entry: str | None @@ -181,7 +185,8 @@ class SoundtrackEntryTypeSelectWindow(bui.Window): bui.app.ui_v1.set_main_menu_window( MacMusicAppPlaylistSelectWindow( self._callback, current_playlist_entry, self._current_entry - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _on_music_file_press(self) -> None: @@ -189,6 +194,10 @@ class SoundtrackEntryTypeSelectWindow(bui.Window): from baclassic.osmusic import OSMusicPlayer from bauiv1lib.fileselector import FileSelectorWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + bui.containerwidget(edit=self._root_widget, transition='out_left') base_path = android_get_external_files_dir() assert bui.app.classic is not None @@ -201,13 +210,18 @@ class SoundtrackEntryTypeSelectWindow(bui.Window): OSMusicPlayer.get_valid_music_file_extensions() ), allow_folders=False, - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _on_music_folder_press(self) -> None: from bauiv1lib.fileselector import FileSelectorWindow from babase import android_get_external_files_dir + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + bui.containerwidget(edit=self._root_widget, transition='out_left') base_path = android_get_external_files_dir() assert bui.app.classic is not None @@ -218,7 +232,8 @@ class SoundtrackEntryTypeSelectWindow(bui.Window): show_base_path=False, valid_file_extensions=[], allow_folders=True, - ).get_root_widget() + ).get_root_widget(), + from_window=self._root_widget, ) def _music_file_selector_cb(self, result: str | None) -> None: diff --git a/src/assets/ba_data/python/bauiv1lib/store/browser.py b/src/assets/ba_data/python/bauiv1lib/store/browser.py index 03af613a..201643f3 100644 --- a/src/assets/ba_data/python/bauiv1lib/store/browser.py +++ b/src/assets/ba_data/python/bauiv1lib/store/browser.py @@ -1329,6 +1329,10 @@ class StoreBrowserWindow(bui.Window): from bauiv1lib.account import show_sign_in_prompt from bauiv1lib.getcurrency import GetCurrencyWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + plus = bui.app.plus assert plus is not None @@ -1343,13 +1347,19 @@ class StoreBrowserWindow(bui.Window): ).get_root_widget() if not self._modal: assert bui.app.classic is not None - bui.app.ui_v1.set_main_menu_window(window) + bui.app.ui_v1.set_main_menu_window( + window, from_window=self._root_widget + ) def _back(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.coop.browser import CoopBrowserWindow from bauiv1lib.mainmenu import MainMenuWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget( edit=self._root_widget, transition=self._transition_out @@ -1358,11 +1368,13 @@ class StoreBrowserWindow(bui.Window): assert bui.app.classic is not None if self._back_location == 'CoopBrowserWindow': bui.app.ui_v1.set_main_menu_window( - CoopBrowserWindow(transition='in_left').get_root_widget() + CoopBrowserWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) else: bui.app.ui_v1.set_main_menu_window( - MainMenuWindow(transition='in_left').get_root_widget() + MainMenuWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) if self._on_close_call is not None: self._on_close_call() diff --git a/src/assets/ba_data/python/bauiv1lib/watch.py b/src/assets/ba_data/python/bauiv1lib/watch.py index 1bdd4fb3..c2e50e4c 100644 --- a/src/assets/ba_data/python/bauiv1lib/watch.py +++ b/src/assets/ba_data/python/bauiv1lib/watch.py @@ -663,11 +663,16 @@ class WatchWindow(bui.Window): def _back(self) -> None: from bauiv1lib.mainmenu import MainMenuWindow + # no-op if our underlying widget is dead or on its way out. + if not self._root_widget or self._root_widget.transitioning_out: + return + self._save_state() bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) assert bui.app.classic is not None bui.app.ui_v1.set_main_menu_window( - MainMenuWindow(transition='in_left').get_root_widget() + MainMenuWindow(transition='in_left').get_root_widget(), + from_window=self._root_widget, ) diff --git a/src/ballistica/base/graphics/graphics.cc b/src/ballistica/base/graphics/graphics.cc index c5d920d3..f6802563 100644 --- a/src/ballistica/base/graphics/graphics.cc +++ b/src/ballistica/base/graphics/graphics.cc @@ -615,7 +615,7 @@ void Graphics::FadeScreen(bool to, millisecs_t time, PyObject* endcall) { Log(LogLevel::kWarning, "2 fades overlapping; running first fade-end-call early."); } - fade_end_call_->ScheduleOnce(); + fade_end_call_->Schedule(); fade_end_call_.Clear(); } set_fade_start_on_next_draw_ = true; @@ -1021,7 +1021,7 @@ void Graphics::DrawFades(FrameDef* frame_def) { } else { fade_ = 0; if (!was_done && fade_end_call_.Exists()) { - fade_end_call_->ScheduleOnce(); + fade_end_call_->Schedule(); fade_end_call_.Clear(); } } diff --git a/src/ballistica/base/python/methods/python_methods_app.cc b/src/ballistica/base/python/methods/python_methods_app.cc index 8815316c..44055fd7 100644 --- a/src/ballistica/base/python/methods/python_methods_app.cc +++ b/src/ballistica/base/python/methods/python_methods_app.cc @@ -279,7 +279,7 @@ static auto PyPushCall(PyObject* self, PyObject* args, PyObject* keywds) if (!g_base->InLogicThread()) { throw Exception("You must use from_other_thread mode."); } - Object::New(call_obj)->ScheduleOnce(); + Object::New(call_obj)->Schedule(); } Py_RETURN_NONE; BA_PYTHON_CATCH; diff --git a/src/ballistica/base/python/support/python_context_call.cc b/src/ballistica/base/python/support/python_context_call.cc index 4be03012..a37ddffb 100644 --- a/src/ballistica/base/python/support/python_context_call.cc +++ b/src/ballistica/base/python/support/python_context_call.cc @@ -125,83 +125,45 @@ void PythonContextCall::Schedule() { Object::Ref ref(this); assert(base::g_base); - schedule_count_++; base::g_base->logic->event_loop()->PushCall([ref] { assert(ref.Exists()); - ref->schedule_count_--; - assert(ref->schedule_count_ >= 0); ref->Run(); }); } -void PythonContextCall::ScheduleOnce() { - if (schedule_count_ > 0) { - return; - } - Schedule(); -} void PythonContextCall::Schedule(const PythonRef& args) { // Since we're mucking with Object::Refs, need to limit to logic thread. BA_PRECONDITION(g_base->InLogicThread()); Object::Ref ref(this); assert(base::g_base); - schedule_count_++; base::g_base->logic->event_loop()->PushCall([ref, args] { assert(ref.Exists()); - ref->schedule_count_--; - assert(ref->schedule_count_ >= 0); ref->Run(args); }); } -void PythonContextCall::ScheduleOnce(const PythonRef& args) { - if (schedule_count_ > 0) { - return; - } - Schedule(args); -} void PythonContextCall::ScheduleWeak() { // Since we're mucking with Object::WeakRefs, need to limit to logic thread. BA_PRECONDITION(g_base->InLogicThread()); Object::WeakRef ref(this); assert(base::g_base); - schedule_count_++; base::g_base->logic->event_loop()->PushCall([ref] { if (auto* call = ref.Get()) { - call->schedule_count_--; - assert(call->schedule_count_ >= 0); call->Run(); } }); } -void PythonContextCall::ScheduleWeakOnce() { - if (schedule_count_ > 0) { - return; - } - ScheduleWeak(); -} - void PythonContextCall::ScheduleWeak(const PythonRef& args) { // Since we're mucking with Object::WeakRefs, need to limit to logic thread. BA_PRECONDITION(g_base->InLogicThread()); Object::WeakRef ref(this); assert(base::g_base); - schedule_count_++; base::g_base->logic->event_loop()->PushCall([ref, args] { if (auto* call = ref.Get()) { - call->schedule_count_--; - assert(call->schedule_count_ >= 0); call->Run(args); } }); } -void PythonContextCall::ScheduleWeakOnce(const PythonRef& args) { - if (schedule_count_ > 0) { - return; - } - ScheduleWeak(args); -} - } // namespace ballistica::base diff --git a/src/ballistica/base/python/support/python_context_call.h b/src/ballistica/base/python/support/python_context_call.h index 1c99d3bc..1c7d5d01 100644 --- a/src/ballistica/base/python/support/python_context_call.h +++ b/src/ballistica/base/python/support/python_context_call.h @@ -42,56 +42,26 @@ class PythonContextCall : public Object { /// context_ref-call is guaranteed to exist until run. void Schedule(); - /// Schedule only if this instance is not already scheduled. Generally a - /// good idea unless you know you need multiple runs scheduled. Avoids - /// problems such as UIs expecting to be activated only once getting - /// activated twice due to two simultenous key presses. - void ScheduleOnce(); - /// Run in an upcoming cycle of the logic thread with provided args. Must /// be called from the logic thread. This form creates a strong-reference /// so the context_ref-call is guaranteed to exist until run. void Schedule(const PythonRef& args); - /// Schedule only if this instance is not already scheduled. Generally a - /// good idea unless you know you need multiple runs scheduled. Avoids - /// problems such as UIs expecting to be activated only once getting - /// activated twice due to two simultenous key presses. - void ScheduleOnce(const PythonRef& args); - /// Run in an upcoming cycle of the logic thread. Must be called from the /// logic thread. This form creates a weak-reference and is a no-op if the /// context_ref-call is destroyed before its scheduled run. void ScheduleWeak(); - /// Schedule weakly only if this instance is not already scheduled. - /// Generally a good idea unless you know you need multiple runs - /// scheduled. Avoids problems such as UIs expecting to be activated only - /// once getting activated twice due to two simultenous key presses. - void ScheduleWeakOnce(); - /// Run in an upcoming cycle of the logic thread with provided args. Must /// be called from the logic thread. This form creates a weak-reference /// and is a no-op if the context_ref-call is destroyed before its /// scheduled run. void ScheduleWeak(const PythonRef& args); - /// Schedule weakly only if this instance is not already scheduled. - /// Generally a good idea unless you know you need multiple runs - /// scheduled. Avoids problems such as UIs expecting to be activated only - /// once getting activated twice due to two simultenous key presses. - void ScheduleWeakOnce(const PythonRef& args); - - auto IsScheduled() const { - assert(g_base->InLogicThread()); - return schedule_count_ > 0; - } - private: void GetTrace(); // we try to grab basic trace info int line_{}; - int schedule_count_{}; bool dead_{}; std::string file_loc_; PythonRef object_; diff --git a/src/ballistica/scene_v1/python/class/python_class_session_player.cc b/src/ballistica/scene_v1/python/class/python_class_session_player.cc index 76b7c1c8..5431aff9 100644 --- a/src/ballistica/scene_v1/python/class/python_class_session_player.cc +++ b/src/ballistica/scene_v1/python/class/python_class_session_player.cc @@ -208,7 +208,7 @@ auto PythonClassSessionPlayer::tp_getattro(PythonClassSessionPlayer* self, PyObject* attr) -> PyObject* { BA_PYTHON_TRY; - assert(g_base->InLogicThread()); + BA_PRECONDITION(g_base->InLogicThread()); // Assuming this will always be a str? assert(PyUnicode_Check(attr)); @@ -327,6 +327,9 @@ auto PythonClassSessionPlayer::tp_setattro(PythonClassSessionPlayer* self, PyObject* attr, PyObject* val) -> int { BA_PYTHON_TRY; + + BA_PRECONDITION(g_base->InLogicThread()); + // Assuming this will always be a str? assert(PyUnicode_Check(attr)); const char* s = PyUnicode_AsUTF8(attr); diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc index 38e7096c..bb2af3c5 100644 --- a/src/ballistica/shared/ballistica.cc +++ b/src/ballistica/shared/ballistica.cc @@ -39,7 +39,7 @@ auto main(int argc, char** argv) -> int { namespace ballistica { // These are set automatically via script; don't modify them here. -const int kEngineBuildNumber = 21636; +const int kEngineBuildNumber = 21639; const char* kEngineVersion = "1.7.30"; const int kEngineApiVersion = 8; diff --git a/src/ballistica/ui_v1/python/class/python_class_widget.cc b/src/ballistica/ui_v1/python/class/python_class_widget.cc index 7a31bb13..57853ee0 100644 --- a/src/ballistica/ui_v1/python/class/python_class_widget.cc +++ b/src/ballistica/ui_v1/python/class/python_class_widget.cc @@ -17,6 +17,12 @@ auto PythonClassWidget::nb_bool(PythonClassWidget* self) -> int { PyNumberMethods PythonClassWidget::as_number_; +// Attrs we expose through our custom getattr/setattr. +#define ATTR_TRANSITIONING_OUT "transitioning_out" + +// The set we expose via dir(). +static const char* extra_dir_attrs[] = {ATTR_TRANSITIONING_OUT, nullptr}; + auto PythonClassWidget::type_name() -> const char* { return "Widget"; } void PythonClassWidget::SetupType(PyTypeObject* cls) { @@ -24,6 +30,9 @@ void PythonClassWidget::SetupType(PyTypeObject* cls) { // Fully qualified type path we will be exposed as: cls->tp_name = "bauiv1.Widget"; cls->tp_basicsize = sizeof(PythonClassWidget); + + // clang-format off + cls->tp_doc = "Internal type for low level UI elements; buttons, windows, etc.\n" "\n" @@ -31,11 +40,22 @@ void PythonClassWidget::SetupType(PyTypeObject* cls) { "\n" "This class represents a weak reference to a widget object\n" "in the internal C++ layer. Currently, functions such as\n" - "babase.buttonwidget() must be used to instantiate or edit these."; + "babase.buttonwidget() must be used to instantiate or edit these.\n" + "Attributes:\n" + " " ATTR_TRANSITIONING_OUT " (bool):\n" + " Whether this widget is in the process of dying (read only).\n" + "\n" + " It can be useful to check this on a window's root widget to\n" + " prevent multiple window actions from firing simultaneously,\n" + " potentially leaving the UI in a broken state.\n"; + + // clang-format on + cls->tp_new = tp_new; cls->tp_dealloc = (destructor)tp_dealloc; cls->tp_repr = (reprfunc)tp_repr; cls->tp_methods = tp_methods; + cls->tp_getattro = (getattrofunc)tp_getattro; // we provide number methods only for bool functionality memset(&as_number_, 0, sizeof(as_number_)); @@ -44,7 +64,7 @@ void PythonClassWidget::SetupType(PyTypeObject* cls) { } auto PythonClassWidget::Create(Widget* widget) -> PyObject* { - // Make sure we only have one python ref per widget. + // Make sure we only have one Python ref per Widget. if (widget) { assert(!widget->has_py_ref()); } @@ -62,10 +82,56 @@ auto PythonClassWidget::Create(Widget* widget) -> PyObject* { auto PythonClassWidget::GetWidget() const -> Widget* { Widget* w = widget_->Get(); - if (!w) throw Exception("Invalid widget"); + if (!w) { + throw Exception("Invalid Widget", PyExcType::kReference); + } return w; } +auto PythonClassWidget::tp_getattro(PythonClassWidget* self, PyObject* attr) + -> PyObject* { + BA_PYTHON_TRY; + + BA_PRECONDITION(g_base->InLogicThread()); + + // Assuming this will always be a str? + assert(PyUnicode_Check(attr)); + + const char* s = PyUnicode_AsUTF8(attr); + if (!strcmp(s, ATTR_TRANSITIONING_OUT)) { + Widget* w = self->widget_->Get(); + if (!w) { + throw Exception("Invalid Widget", PyExcType::kReference); + } + if (w->IsTransitioningOut()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + } + + // Fall back to generic behavior. + PyObject* val; + val = PyObject_GenericGetAttr(reinterpret_cast(self), attr); + return val; + BA_PYTHON_CATCH; +} + +auto PythonClassWidget::tp_setattro(PythonClassWidget* self, PyObject* attr, + PyObject* val) -> int { + BA_PYTHON_TRY; + + BA_PRECONDITION(g_base->InLogicThread()); + + // Assuming this will always be a str? + assert(PyUnicode_Check(attr)); + const char* s = PyUnicode_AsUTF8(attr); + + throw Exception("Attr '" + std::string(PyUnicode_AsUTF8(attr)) + + "' is not settable on SessionPlayer objects.", + PyExcType::kAttribute); + BA_PYTHON_INT_CATCH; +} + auto PythonClassWidget::tp_repr(PythonClassWidget* self) -> PyObject* { BA_PYTHON_TRY; Widget* w = self->widget_->Get(); @@ -96,8 +162,8 @@ auto PythonClassWidget::tp_new(PyTypeObject* type, PyObject* args, void PythonClassWidget::tp_dealloc(PythonClassWidget* self) { BA_PYTHON_TRY; - // these have to be destructed in the logic thread - send them along to it if - // need be + // these have to be destructed in the logic thread - send them along to it + // if need be if (!g_base->InLogicThread()) { Object::WeakRef* w = self->widget_; g_base->logic->event_loop()->PushCall([w] { delete w; }); diff --git a/src/ballistica/ui_v1/python/class/python_class_widget.h b/src/ballistica/ui_v1/python/class/python_class_widget.h index 012612da..b4e98409 100644 --- a/src/ballistica/ui_v1/python/class/python_class_widget.h +++ b/src/ballistica/ui_v1/python/class/python_class_widget.h @@ -26,6 +26,9 @@ class PythonClassWidget : public PythonClass { static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) -> PyObject*; static void tp_dealloc(PythonClassWidget* self); + static auto tp_getattro(PythonClassWidget* self, PyObject* attr) -> PyObject*; + static auto tp_setattro(PythonClassWidget* self, PyObject* attr, + PyObject* val) -> int; static auto Exists(PythonClassWidget* self) -> PyObject*; static auto GetWidgetType(PythonClassWidget* self) -> PyObject*; static auto Activate(PythonClassWidget* self) -> PyObject*; diff --git a/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc index ce6f13dd..b071343e 100644 --- a/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc +++ b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc @@ -1219,17 +1219,17 @@ static auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds) if (transition_obj != Py_None) { std::string t = Python::GetPyString(transition_obj); if (t == "in_left") - widget->SetTransition(ContainerWidget::TRANSITION_IN_LEFT); + widget->SetTransition(ContainerWidget::TransitionType::kInLeft); else if (t == "in_right") - widget->SetTransition(ContainerWidget::TRANSITION_IN_RIGHT); + widget->SetTransition(ContainerWidget::TransitionType::kInRight); else if (t == "out_left") - widget->SetTransition(ContainerWidget::TRANSITION_OUT_LEFT); + widget->SetTransition(ContainerWidget::TransitionType::kOutLeft); else if (t == "out_right") - widget->SetTransition(ContainerWidget::TRANSITION_OUT_RIGHT); + widget->SetTransition(ContainerWidget::TransitionType::kOutRight); else if (t == "in_scale") - widget->SetTransition(ContainerWidget::TRANSITION_IN_SCALE); + widget->SetTransition(ContainerWidget::TransitionType::kInScale); else if (t == "out_scale") - widget->SetTransition(ContainerWidget::TRANSITION_OUT_SCALE); + widget->SetTransition(ContainerWidget::TransitionType::kOutScale); } if (cancel_button_obj != Py_None) { diff --git a/src/ballistica/ui_v1/python/ui_v1_python.cc b/src/ballistica/ui_v1/python/ui_v1_python.cc index f4157594..f12321b0 100644 --- a/src/ballistica/ui_v1/python/ui_v1_python.cc +++ b/src/ballistica/ui_v1/python/ui_v1_python.cc @@ -115,7 +115,7 @@ void UIV1Python::InvokeStringEditor(PyObject* string_edit_adapter_instance) { PythonRef::kSteal); Object::New( objs().Get(ObjID::kOnScreenKeyboardClass)) - ->ScheduleOnce(args); + ->Schedule(args); } void UIV1Python::LaunchStringEditOld(TextWidget* w) { @@ -131,7 +131,7 @@ void UIV1Python::LaunchStringEditOld(TextWidget* w) { PythonRef::kSteal); Object::New( objs().Get(ObjID::kOnScreenKeyboardClass)) - ->ScheduleOnce(args); + ->Schedule(args); } void UIV1Python::InvokeQuitWindow(QuitType quit_type) { diff --git a/src/ballistica/ui_v1/python/ui_v1_python.h b/src/ballistica/ui_v1/python/ui_v1_python.h index 8e10e9a1..734cd476 100644 --- a/src/ballistica/ui_v1/python/ui_v1_python.h +++ b/src/ballistica/ui_v1/python/ui_v1_python.h @@ -39,6 +39,7 @@ class UIV1Python { kQuitWindowCall, kDeviceMenuPressCall, kShowURLWindowCall, + kDoubleTransitionOutWarningCall, kTextWidgetStringEditAdapterClass, kLast // Sentinel; must be at end. }; diff --git a/src/ballistica/ui_v1/widget/button_widget.cc b/src/ballistica/ui_v1/widget/button_widget.cc index 7b824db1..29c6db6a 100644 --- a/src/ballistica/ui_v1/widget/button_widget.cc +++ b/src/ballistica/ui_v1/widget/button_widget.cc @@ -562,7 +562,7 @@ void ButtonWidget::DoActivate(bool is_repeat) { if (auto* call = on_activate_call_.Get()) { // Call this in the next cycle (don't want to risk mucking with UI from // within a UI loop.) - call->ScheduleWeakOnce(); + call->ScheduleWeak(); return; } } diff --git a/src/ballistica/ui_v1/widget/check_box_widget.cc b/src/ballistica/ui_v1/widget/check_box_widget.cc index f252870a..a6e47b3e 100644 --- a/src/ballistica/ui_v1/widget/check_box_widget.cc +++ b/src/ballistica/ui_v1/widget/check_box_widget.cc @@ -247,7 +247,7 @@ void CheckBoxWidget::Activate() { // Call this in the next cycle (don't want to risk mucking with UI from // within a UI loop) - call->ScheduleWeakOnce(args); + call->ScheduleWeak(args); } } @@ -271,12 +271,13 @@ auto CheckBoxWidget::HandleMessage(const base::WidgetMessage& m) -> bool { float x = m.fval1; float y = m.fval2; bool claimed = (m.fval3 > 0.0f); - if (claimed) + if (claimed) { mouse_over_ = false; - else + } else { mouse_over_ = ((x >= (-left_overlap)) && (x < (width_ + right_overlap)) && (y >= (-bottom_overlap)) && (y < (height_ + top_overlap))); + } return mouse_over_; } case base::WidgetMessage::Type::kMouseDown: { diff --git a/src/ballistica/ui_v1/widget/container_widget.cc b/src/ballistica/ui_v1/widget/container_widget.cc index e47bbb17..4f205dbd 100644 --- a/src/ballistica/ui_v1/widget/container_widget.cc +++ b/src/ballistica/ui_v1/widget/container_widget.cc @@ -11,6 +11,7 @@ #include "ballistica/shared/generic/utils.h" #include "ballistica/shared/math/random.h" #include "ballistica/shared/python/python.h" +#include "ballistica/ui_v1/python/ui_v1_python.h" #include "ballistica/ui_v1/widget/button_widget.h" #include "ballistica/ui_v1/widget/root_widget.h" #include "ballistica/ui_v1/widget/stack_widget.h" @@ -347,7 +348,7 @@ auto ContainerWidget::HandleMessage(const base::WidgetMessage& m) -> bool { // Call this in the next cycle (don't wanna risk mucking with UI from // within a UI loop). - call->ScheduleWeakOnce(); + call->ScheduleWeak(); } else { OnCancelCustom(); } @@ -631,7 +632,7 @@ auto ContainerWidget::HandleMessage(const base::WidgetMessage& m) -> bool { if (!claimed && on_outside_click_call_.Exists()) { // Call this in the next cycle (don't wanna risk mucking with UI from // within a UI loop). - on_outside_click_call_->ScheduleWeakOnce(); + on_outside_click_call_->ScheduleWeak(); } // Always claim if they want. @@ -793,7 +794,7 @@ void ContainerWidget::Draw(base::RenderPass* pass, bool draw_transparent) { bg_dirty_ = true; if (!draw_transparent) { - if (transition_type_ == TRANSITION_IN_SCALE) { + if (transition_type_ == TransitionType::kInScale) { if (net_time - dynamics_update_time_millisecs_ > 1000) dynamics_update_time_millisecs_ = net_time - 1000; while (net_time - dynamics_update_time_millisecs_ > 5) { @@ -808,7 +809,7 @@ void ContainerWidget::Draw(base::RenderPass* pass, bool draw_transparent) { transitioning_ = false; } } - } else if (transition_type_ == TRANSITION_OUT_SCALE) { + } else if (transition_type_ == TransitionType::kOutScale) { if (net_time - dynamics_update_time_millisecs_ > 1000) dynamics_update_time_millisecs_ = net_time - 1000; while (net_time - dynamics_update_time_millisecs_ > 5) { @@ -908,8 +909,8 @@ void ContainerWidget::Draw(base::RenderPass* pass, bool draw_transparent) { // If we're scaling in or out, update our transition offset // (so we can zoom from a point somewhere else on screen). - if (transition_type_ == TRANSITION_IN_SCALE - || transition_type_ == TRANSITION_OUT_SCALE) { + if (transition_type_ == TransitionType::kInScale + || transition_type_ == TransitionType::kOutScale) { // Add a fudge factor since our scale point isn't exactly in our center. // :-( float xdiff = scale_origin_stack_offset_x_ - stack_offset_x() @@ -1071,7 +1072,7 @@ void ContainerWidget::Activate() { if (auto* call = on_activate_call_.Get()) { // Call this in the next cycle (don't wanna risk mucking with UI from within // a UI loop). - call->ScheduleWeakOnce(); + call->ScheduleWeak(); } } @@ -1156,8 +1157,23 @@ void ContainerWidget::SetStartButton(ButtonWidget* button) { button->set_icon_type(ButtonWidget::IconType::kStart); } +static auto _IsTransitionOut(ContainerWidget::TransitionType type) { + switch (type) { + case ContainerWidget::TransitionType::kUnset: + case ContainerWidget::TransitionType::kInLeft: + case ContainerWidget::TransitionType::kInRight: + case ContainerWidget::TransitionType::kInScale: + return false; + case ContainerWidget::TransitionType::kOutLeft: + case ContainerWidget::TransitionType::kOutRight: + case ContainerWidget::TransitionType::kOutScale: + return true; + } +} + void ContainerWidget::SetTransition(TransitionType t) { BA_DEBUG_UI_READ_LOCK; + assert(g_base->InLogicThread()); bg_dirty_ = glow_dirty_ = true; ContainerWidget* parent = parent_widget(); @@ -1167,17 +1183,26 @@ void ContainerWidget::SetTransition(TransitionType t) { parent->CheckLayout(); auto display_time_millisecs = static_cast(g_base->logic->display_time() * 1000.0); + + // Warn if setting out-transition twice. This likely means a window is + // switching to another window twice which can leave the UI broken. + if (_IsTransitionOut(transition_type_) && _IsTransitionOut(t)) { + g_ui_v1->python->objs() + .Get(UIV1Python::ObjID::kDoubleTransitionOutWarningCall) + .Call(); + } + transition_type_ = t; // Scale transitions are simpler. - if (t == TRANSITION_IN_SCALE) { + if (t == TransitionType::kInScale) { transition_start_time_ = display_time_millisecs; dynamics_update_time_millisecs_ = display_time_millisecs; transitioning_ = true; transitioning_out_ = false; transition_scale_ = 0.0f; d_transition_scale_ = 0.0f; - } else if (t == TRANSITION_OUT_SCALE) { + } else if (t == TransitionType::kOutScale) { transition_start_time_ = display_time_millisecs; dynamics_update_time_millisecs_ = display_time_millisecs; transitioning_ = true; @@ -1195,7 +1220,7 @@ void ContainerWidget::SetTransition(TransitionType t) { // In case we're mid-transition, this avoids hitches. float y_offs = 2.0f; - if (t == TRANSITION_IN_LEFT) { + if (t == TransitionType::kInLeft) { transition_start_time_ = display_time_millisecs; transition_start_offset_ = screen_min_x - width_ - 100; transition_offset_x_smoothed_ = transition_start_offset_; @@ -1204,7 +1229,7 @@ void ContainerWidget::SetTransition(TransitionType t) { transitioning_ = true; dynamics_update_time_millisecs_ = display_time_millisecs; transitioning_out_ = false; - } else if (t == TRANSITION_IN_RIGHT) { + } else if (t == TransitionType::kInRight) { transition_start_time_ = display_time_millisecs; transition_start_offset_ = screen_max_x + 100; transition_offset_x_smoothed_ = transition_start_offset_; @@ -1213,7 +1238,7 @@ void ContainerWidget::SetTransition(TransitionType t) { transitioning_ = true; dynamics_update_time_millisecs_ = display_time_millisecs; transitioning_out_ = false; - } else if (t == TRANSITION_OUT_LEFT) { + } else if (t == TransitionType::kOutLeft) { transition_start_time_ = display_time_millisecs; transition_start_offset_ = transition_offset_x_; transition_target_offset_ = -2.0f * (screen_max_x - screen_min_x); @@ -1223,7 +1248,7 @@ void ContainerWidget::SetTransition(TransitionType t) { dynamics_update_time_millisecs_ = display_time_millisecs; transitioning_out_ = true; ignore_input_ = true; - } else if (t == TRANSITION_OUT_RIGHT) { + } else if (t == TransitionType::kOutRight) { transition_start_time_ = display_time_millisecs; transition_start_offset_ = transition_offset_x_; transition_target_offset_ = 2.0f * (screen_max_x - screen_min_x); @@ -1279,8 +1304,9 @@ void ContainerWidget::DeleteWidget(Widget* w) { assert(found); - // Special case: if we're the overlay stack and we've deleted our last widget, - // try to reselect whatever was last selected before the overlay stack. + // Special case: if we're the overlay stack and we've deleted our last + // widget, try to reselect whatever was last selected before the overlay + // stack. if (is_overlay_window_stack_) { if (widgets_.empty()) { // Eww this logic should be in some sort of controller. @@ -1298,8 +1324,9 @@ void ContainerWidget::DeleteWidget(Widget* w) { if ((**i).IsSelectable()) { // A change on the main or overlay window stack changes the global // selection (unless its on the main window stack and there's already - // something on the overlay stack) in all other cases we just shift our - // direct selected child (which may not affect the global selection). + // something on the overlay stack) in all other cases we just shift + // our direct selected child (which may not affect the global + // selection). if (is_window_stack_ && (is_overlay_window_stack_ || !g_ui_v1->root_widget() @@ -1322,8 +1349,8 @@ void ContainerWidget::DeleteWidget(Widget* w) { } auto ContainerWidget::GetTopmostToolbarInfluencingWidget() -> Widget* { - // Look for the first window that is accepting input (filters out windows that - // are transitioning out) and also set to affect the toolbar state. + // Look for the first window that is accepting input (filters out windows + // that are transitioning out) and also set to affect the toolbar state. for (auto w = widgets_.rbegin(); w != widgets_.rend(); ++w) { if ((**w).IsAcceptingInput() && (**w).toolbar_visibility() != ToolbarVisibility::kInherit) { @@ -1438,9 +1465,8 @@ void ContainerWidget::SetSelected(bool s, SelectionCause cause) { } } } else { - // if we're being deselected and we have a selected child, tell them they're - // deselected - // if (selected_widget_) { + // if we're being deselected and we have a selected child, tell them + // they're deselected if (selected_widget_) { // } } } @@ -1583,8 +1609,8 @@ void ContainerWidget::SelectDownWidget() { selected_widget_->GetCenter(&our_x, &our_y); w = GetClosestDownWidget(our_x, our_y, selected_widget_); if (!w) { - // If we found no viable children and we're under the main window stack, - // see if we should pass focus to a toolbar widget. + // If we found no viable children and we're under the main window + // stack, see if we should pass focus to a toolbar widget. if (IsInMainStack()) { float x = our_x; float y = our_y; @@ -1712,7 +1738,8 @@ void ContainerWidget::SelectLeftWidget() { float our_x, our_y; selected_widget_->GetCenter(&our_x, &our_y); w = GetClosestLeftWidget(our_x, our_y, selected_widget_); - // When we find no viable targets for an autoselect widget we do nothing. + // When we find no viable targets for an autoselect widget we do + // nothing. if (!w) { return; } @@ -1843,8 +1870,8 @@ void ContainerWidget::SelectNextWidget() { return; } else if (selected_widget_ == nullptr) { // NOLINT(bugprone-branch-clone) - // We've got no selection and we've scanned the whole list to no avail, - // fail. + // We've got no selection and we've scanned the whole list to no + // avail, fail. PrintExitListInstructions(old_last_prev_next_time); return; } else if (selection_loops()) { @@ -1997,4 +2024,8 @@ void ContainerWidget::OnLanguageChange() { } } +auto ContainerWidget::IsTransitioningOut() const -> bool { + return transitioning_out_; +} + } // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui_v1/widget/container_widget.h b/src/ballistica/ui_v1/widget/container_widget.h index 30946207..5f55bd7e 100644 --- a/src/ballistica/ui_v1/widget/container_widget.h +++ b/src/ballistica/ui_v1/widget/container_widget.h @@ -14,20 +14,21 @@ namespace ballistica::ui_v1 { // Base class for widgets that contain other widgets. class ContainerWidget : public Widget { public: - explicit ContainerWidget(float width = 0, float height = 0); + explicit ContainerWidget(float width = 0.0f, float height = 0.0f); ~ContainerWidget() override; void Draw(base::RenderPass* pass, bool transparent) override; auto HandleMessage(const base::WidgetMessage& m) -> bool override; - enum TransitionType { - TRANSITION_OUT_LEFT, - TRANSITION_OUT_RIGHT, - TRANSITION_IN_LEFT, - TRANSITION_IN_RIGHT, - TRANSITION_IN_SCALE, - TRANSITION_OUT_SCALE + enum class TransitionType { + kUnset, + kOutLeft, + kOutRight, + kInLeft, + kInRight, + kInScale, + kOutScale }; void SetTransition(TransitionType t); @@ -49,6 +50,7 @@ class ContainerWidget : public Widget { width_ = w; MarkForUpdate(); } + virtual void SetHeight(float h) { bg_dirty_ = glow_dirty_ = true; height_ = h; @@ -67,6 +69,7 @@ class ContainerWidget : public Widget { CheckLayout(); return width_; } + auto GetHeight() -> float override { CheckLayout(); return height_; @@ -76,8 +79,8 @@ class ContainerWidget : public Widget { auto HasKeySelectableChild() const -> bool; - void set_is_window_stack(bool a) { is_window_stack_ = a; } auto is_window_stack() const -> bool { return is_window_stack_; } + void set_is_window_stack(bool a) { is_window_stack_ = a; } auto GetChildCount() const -> int { assert(g_base->InLogicThread()); @@ -167,6 +170,8 @@ class ContainerWidget : public Widget { // if the topmost one is transitioning out, etc.) auto GetTopmostToolbarInfluencingWidget() -> Widget*; + auto IsTransitioningOut() const -> bool override; + protected: virtual void OnCancelCustom() {} void set_single_depth_root(bool s) { single_depth_root_ = s; } diff --git a/src/ballistica/ui_v1/widget/text_widget.cc b/src/ballistica/ui_v1/widget/text_widget.cc index 687c1689..67474f50 100644 --- a/src/ballistica/ui_v1/widget/text_widget.cc +++ b/src/ballistica/ui_v1/widget/text_widget.cc @@ -592,7 +592,7 @@ void TextWidget::Activate() { if (auto* call = on_activate_call_.Get()) { // Call this in the next cycle (don't wanna risk mucking with UI from // within a UI loop). - call->ScheduleWeakOnce(); + call->ScheduleWeak(); } // Bring up an editor if applicable. @@ -720,7 +720,7 @@ auto TextWidget::HandleMessage(const base::WidgetMessage& m) -> bool { claimed = true; // Call this in the next cycle (don't wanna risk mucking with UI // from within a UI loop) - call->ScheduleWeakOnce(); + call->ScheduleWeak(); } } break; diff --git a/src/ballistica/ui_v1/widget/widget.cc b/src/ballistica/ui_v1/widget/widget.cc index 257d2198..c94910a7 100644 --- a/src/ballistica/ui_v1/widget/widget.cc +++ b/src/ballistica/ui_v1/widget/widget.cc @@ -88,7 +88,7 @@ void Widget::SetSelected(bool s, SelectionCause cause) { if (selected_ && on_select_call_.Exists()) { // Call this in the next cycle (don't wanna risk mucking // with UI from within a UI loop). - on_select_call_->ScheduleWeakOnce(); + on_select_call_->ScheduleWeak(); } } @@ -238,4 +238,6 @@ auto Widget::IsAcceptingInput() const -> bool { return true; } void Widget::Activate() {} +auto Widget::IsTransitioningOut() const -> bool { return false; } + } // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui_v1/widget/widget.h b/src/ballistica/ui_v1/widget/widget.h index 56e7e18b..e6953c99 100644 --- a/src/ballistica/ui_v1/widget/widget.h +++ b/src/ballistica/ui_v1/widget/widget.h @@ -46,7 +46,8 @@ class Widget : public Object { // Whether the widget (or its children) is selectable in any way. virtual auto IsSelectable() -> bool; - // Whether the widget can be selected by default with direction/tab presses. + // Whether the widget can be selected by default with direction/tab + // presses. virtual auto IsSelectableViaKeys() -> bool; // Is the widget currently accepting input? @@ -83,8 +84,8 @@ class Widget : public Object { // If this widget is in a container, return it. auto parent_widget() const -> ContainerWidget* { return parent_widget_; } - // Return the container_widget containing this widget, or the owner-widget if - // there is no parent. + // Return the container_widget containing this widget, or the owner-widget + // if there is no parent. auto GetOwnerWidget() const -> Widget*; auto down_widget() const -> Widget* { return down_widget_.Get(); } @@ -116,25 +117,23 @@ class Widget : public Object { // redirecting them to transient per-window stuff). void set_neighbors_locked(bool locked) { neighbors_locked_ = locked; } - // Widgets normally draw with a local depth range of 0-1. - // It can be useful to limit drawing to a subsection of that region however - // (for manually resolving overlap issues with widgets at the same depth, - // etc). + // Widgets normally draw with a local depth range of 0-1. It can be useful + // to limit drawing to a subsection of that region however (for manually + // resolving overlap issues with widgets at the same depth, etc). void SetDepthRange(float minDepth, float maxDepth); auto depth_range_min() const -> float { return depth_range_min_; } auto depth_range_max() const -> float { return depth_range_max_; } - // For use by ContainerWidgets. - // (we probably should just this functionality to all widgets) + // For use by ContainerWidgets (we probably should just add this + // functionality to all widgets). void set_parent_widget(ContainerWidget* c) { parent_widget_ = c; } auto IsInMainStack() const -> bool; auto IsInOverlayStack() const -> bool; - // For use when embedding widgets inside others manually. - // This will allow proper selection states/etc to trickle down to the - // lowest-level child. + // For use when embedding widgets inside others manually. This will allow + // proper selection states/etc to trickle down to the lowest-level child. void set_owner_widget(Widget* o) { owner_widget_ = o; } virtual auto GetWidgetTypeName() -> std::string { return "widget"; } virtual auto HasChildren() const -> bool { return false; } @@ -167,22 +166,25 @@ class Widget : public Object { void ScreenPointToWidget(float* x, float* y) const; void WidgetPointToScreen(float* x, float* y) const; - // Draw-control parents are used to give one widget some basic visual control - // over others, allowing them to inherit things like draw-brightness and tilt - // shift (for cases such as images drawn over buttons). - // Ideally we'd probably want to extend the parent mechanism for this, but - // this works for now. + // Draw-control parents are used to give one widget some basic visual + // control over others, allowing them to inherit things like + // draw-brightness and tilt shift (for cases such as images drawn over + // buttons). Ideally we'd probably want to extend the parent mechanism for + // this, but this works for now. auto draw_control_parent() const -> Widget* { return draw_control_parent_.Get(); } void set_draw_control_parent(Widget* w) { draw_control_parent_ = w; } - // Can be used to ask link-parents how bright to draw. - // Note: make sure the value returned here does not get changed when draw() - // is run, since parts of draw-controlled children may query this before - // draw() and parts after. (and they need to line up visually) + // Can be used to ask link-parents how bright to draw. Note: make sure the + // value returned here does not get changed when draw() is run, since + // parts of draw-controlled children may query this before draw() and + // parts after. (and they need to line up visually) virtual auto GetDrawBrightness(millisecs_t current_time) const -> float; + /// Is this widget in the process of transitioning out before dying? + virtual auto IsTransitioningOut() const -> bool; + // Extra buffer added around widgets when they are centered-on. void set_show_buffer_top(float b) { show_buffer_top_ = b; } void set_show_buffer_bottom(float b) { show_buffer_bottom_ = b; } @@ -206,8 +208,8 @@ class Widget : public Object { virtual void OnLanguageChange() {} - // Primitive janktastic child culling for use by containers. - // (should really implement something more proper...) + // Primitive janktastic child culling for use by containers (should really + // implement something more proper). auto simple_culling_v() const -> float { return simple_culling_v_; } auto simple_culling_h() const -> float { return simple_culling_h_; } auto simple_culling_bottom() const -> float { return simple_culling_bottom_; } @@ -224,14 +226,17 @@ class Widget : public Object { private: auto GetPyWidget(bool new_ref) -> PyObject*; virtual void SetSelected(bool s, SelectionCause cause); + bool selected_{}; + bool visible_in_container_{true}; + bool neighbors_locked_{}; + bool auto_select_{}; + ToolbarVisibility toolbar_visibility_{ToolbarVisibility::kMenuMinimalNoBack}; float simple_culling_h_{-1.0f}; float simple_culling_v_{-1.0f}; float simple_culling_left_{}; float simple_culling_right_{}; float simple_culling_bottom_{}; float simple_culling_top_{}; - ToolbarVisibility toolbar_visibility_{ToolbarVisibility::kMenuMinimalNoBack}; - PyObject* py_ref_{}; float show_buffer_top_{20.0f}; float show_buffer_bottom_{20.0f}; float show_buffer_left_{20.0f}; @@ -241,12 +246,9 @@ class Widget : public Object { Object::WeakRef up_widget_; Object::WeakRef left_widget_; Object::WeakRef right_widget_; - bool neighbors_locked_{}; - bool auto_select_{}; ContainerWidget* parent_widget_{}; + PyObject* py_ref_{}; Widget* owner_widget_{}; - bool selected_{}; - bool visible_in_container_{true}; float tx_{}; float ty_{}; float stack_offset_x_{}; diff --git a/src/meta/bauiv1meta/pyembed/binding_ui_v1.py b/src/meta/bauiv1meta/pyembed/binding_ui_v1.py index 015d75ea..dc7f8c13 100644 --- a/src/meta/bauiv1meta/pyembed/binding_ui_v1.py +++ b/src/meta/bauiv1meta/pyembed/binding_ui_v1.py @@ -22,5 +22,6 @@ values = [ _hooks.quit_window, # kQuitWindowCall _hooks.device_menu_press, # kDeviceMenuPressCall _hooks.show_url_window, # kShowURLWindowCall + _hooks.double_transition_out_warning, # kDoubleTransitionOutWarningCall TextWidgetStringEditAdapter, # kTextWidgetStringEditAdapterClass ]