# Released under the MIT License. See LICENSE for details.#"""Provide top level UI related functionality."""from__future__importannotationsimportosimportweakreffromdataclassesimportdataclassfromtypingimportTYPE_CHECKING,overrideimportbabaseimport_bauiv1ifTYPE_CHECKING:fromtypingimportAny,Typeimportbauiv1# Set environment variable BA_DEBUG_UI_CLEANUP_CHECKS to 1# to print detailed info about what is getting cleaned up when.DEBUG_UI_CLEANUP_CHECKS=os.environ.get('BA_DEBUG_UI_CLEANUP_CHECKS')=='1'classWindow:"""A basic window. Category: User Interface Classes """def__init__(self,root_widget:bauiv1.Widget,cleanupcheck:bool=True):self._root_widget=root_widget# Complain if we outlive our root widget.ifcleanupcheck:uicleanupcheck(self,root_widget)
[docs]defget_root_widget(self)->bauiv1.Widget:"""Return the root widget."""returnself._root_widget
@dataclassclassUICleanupCheck:"""Holds info about a uicleanupcheck target."""obj:weakref.refwidget:bauiv1.Widgetwidget_death_time:float|NoneclassUILocation:"""Defines a specific 'place' in the UI the user can navigate to. Category: User Interface Classes """def__init__(self)->None:passdefsave_state(self)->None:"""Serialize this instance's state to a dict."""defrestore_state(self)->None:"""Restore this instance's state from a dict."""defpush_location(self,location:str)->None:"""Push a new location to the stack and transition to it."""classUILocationWindow(UILocation):"""A UILocation consisting of a single root window widget. Category: User Interface Classes """def__init__(self)->None:super().__init__()self._root_widget:bauiv1.Widget|None=Nonedefget_root_widget(self)->bauiv1.Widget:"""Return the root widget for this window."""assertself._root_widgetisnotNonereturnself._root_widgetclassUIEntry:"""State for a UILocation on the stack."""def__init__(self,name:str,controller:UIController):self._name=nameself._state=Noneself._args=Noneself._instance:UILocation|None=Noneself._controller=weakref.ref(controller)defcreate(self)->None:"""Create an instance of our UI."""cls=self._get_class()self._instance=cls()defdestroy(self)->None:"""Transition out our UI if it exists."""ifself._instanceisNone:returnprint('WOULD TRANSITION OUT',self._name)def_get_class(self)->Type[UILocation]:"""Returns the UI class our name points to."""# pylint: disable=cyclic-import# TEMP HARD CODED - WILL REPLACE THIS WITH BA_META LOOKUPS.ifself._name=='mainmenu':# Shut pylint up.ifbool(False):returnUILocationraiseRuntimeError('FIXME UNIMPLEMENTED')# from bauiv1lib import mainmenu# return cast(Type[UILocation], mainmenu.MainMenuWindow)raiseValueError('unknown ui class '+str(self._name))classUIController:"""Wrangles bauiv1.UILocations. Category: User Interface Classes """def__init__(self)->None:# FIXME: document why we have separate stacks for game and menu...self._main_stack_game:list[UIEntry]=[]self._main_stack_menu:list[UIEntry]=[]# This points at either the game or menu stack.self._main_stack:list[UIEntry]|None=None# There's only one of these since we don't need to preserve its state# between sessions.self._dialog_stack:list[UIEntry]=[]defshow_main_menu(self,in_game:bool=True)->None:"""Show the main menu, clearing other UIs from location stacks."""self._main_stack=[]self._dialog_stack=[]self._main_stack=(self._main_stack_gameifin_gameelseself._main_stack_menu)self._main_stack.append(UIEntry('mainmenu',self))self._update_ui()def_update_ui(self)->None:"""Instantiate the topmost ui in our stacks."""# First tell any existing UIs to get outta here.forstackin(self._dialog_stack,self._main_stack):assertstackisnotNoneforentryinstack:entry.destroy()# Now create the topmost one if there is one.entrynew=(self._dialog_stack[-1]ifself._dialog_stackelseself._main_stack[-1]ifself._main_stackelseNone)ifentrynewisnotNone:entrynew.create()defuicleanupcheck(obj:Any,widget:bauiv1.Widget)->None:"""Add a check to ensure a widget-owning object gets cleaned up properly. Category: User Interface Functions This adds a check which will print an error message if the provided object still exists ~5 seconds after the provided bauiv1.Widget dies. This is a good sanity check for any sort of object that wraps or controls a bauiv1.Widget. For instance, a 'Window' class instance has no reason to still exist once its root container bauiv1.Widget has fully transitioned out and been destroyed. Circular references or careless strong referencing can lead to such objects never getting destroyed, however, and this helps detect such cases to avoid memory leaks. """ifDEBUG_UI_CLEANUP_CHECKS:print(f'adding uicleanup to {obj}')ifnotisinstance(widget,_bauiv1.Widget):raiseTypeError('widget arg is not a bauiv1.Widget')ifbool(False):deffoobar()->None:"""Just testing."""ifDEBUG_UI_CLEANUP_CHECKS:print('uicleanupcheck widget dying...')widget.add_delete_callback(foobar)assertbabase.app.classicisnotNonebabase.app.ui_v1.cleanupchecks.append(UICleanupCheck(obj=weakref.ref(obj),widget=widget,widget_death_time=None))defui_upkeep()->None:"""Run UI cleanup checks, etc. should be called periodically."""assertbabase.app.classicisnotNoneui=babase.app.ui_v1remainingchecks=[]now=babase.apptime()forcheckinui.cleanupchecks:obj=check.obj()# If the object has died, ignore and don't re-add.ifobjisNone:ifDEBUG_UI_CLEANUP_CHECKS:print('uicleanupcheck object is dead; hooray!')continue# If the widget hadn't died yet, note if it has.ifcheck.widget_death_timeisNone:remainingchecks.append(check)ifnotcheck.widget:check.widget_death_time=nowelse:# Widget was already dead; complain if its been too long.ifnow-check.widget_death_time>5.0:print('WARNING:',obj,'is still alive 5 second after its widget died;'' you might have a memory leak. See efro.debug module'' for tools to help debug this.',)else:remainingchecks.append(check)ui.cleanupchecks=remainingchecksclassTextWidgetStringEditAdapter(babase.StringEditAdapter):"""A StringEditAdapter subclass for editing our text widgets."""def__init__(self,text_widget:bauiv1.Widget)->None:self.widget=text_widget# Ugly hacks to pull values from widgets. Really need to clean# up that api.description:Any=_bauiv1.textwidget(query_description=text_widget)assertisinstance(description,str)initial_text:Any=_bauiv1.textwidget(query=text_widget)assertisinstance(initial_text,str)max_length:Any=_bauiv1.textwidget(query_max_chars=text_widget)assertisinstance(max_length,int)screen_space_center=text_widget.get_screen_space_center()super().__init__(description,initial_text,max_length,screen_space_center)@overridedef_do_apply(self,new_text:str)->None:ifself.widget:_bauiv1.textwidget(edit=self.widget,text=new_text,adapter_finished=True)@overridedef_do_cancel(self)->None:ifself.widget:_bauiv1.textwidget(edit=self.widget,adapter_finished=True)