def update_scores(self) -> None: """ update scoreboard and check for winners """ # FIXME: tidy this up # pylint: disable=too-many-nested-blocks have_scoring_team = False win_score = self._score_to_win for team in [self.teams[0], self._bot_team]: assert team is not None assert self._scoreboard is not None self._scoreboard.set_team_value(team, team.gamedata['score'], win_score) if team.gamedata['score'] >= win_score: if not have_scoring_team: self.scoring_team = team if team is self._bot_team: self.continue_or_end_game() else: ba.setmusic(ba.MusicType.VICTORY) # Completion achievements. assert self._bot_team is not None if self._preset in ['rookie', 'rookie_easy']: self._award_achievement('Rookie Football Victory', sound=False) if self._bot_team.gamedata['score'] == 0: self._award_achievement( 'Rookie Football Shutout', sound=False) elif self._preset in ['pro', 'pro_easy']: self._award_achievement('Pro Football Victory', sound=False) if self._bot_team.gamedata['score'] == 0: self._award_achievement('Pro Football Shutout', sound=False) elif self._preset in ['uber', 'uber_easy']: self._award_achievement('Uber Football Victory', sound=False) if self._bot_team.gamedata['score'] == 0: self._award_achievement( 'Uber Football Shutout', sound=False) if (not self._player_has_dropped_bomb and not self._player_has_punched): self._award_achievement('Got the Moves', sound=False) self._bots.stop_moving() self.show_zoom_message(ba.Lstr(resource='victoryText'), scale=1.0, duration=4.0) self.celebrate(10.0) assert self._starttime_ms is not None self._final_time_ms = int( ba.time(timeformat=ba.TimeFormat.MILLISECONDS) - self._starttime_ms) self._time_text_timer = None assert (self._time_text_input is not None and self._time_text_input.node) self._time_text_input.node.timemax = ( self._final_time_ms) # FIXME: Does this still need to be deferred? ba.pushcall(ba.Call(self.do_end, 'victory'))
def _connect(self, textwidget: ba.Widget, port_textwidget: ba.Widget) -> None: addr = cast(str, ba.textwidget(query=textwidget)) if addr == '': ba.screenmessage( ba.Lstr(resource='internal.invalidAddressErrorText'), color=(1, 0, 0)) ba.playsound(ba.getsound('error')) return try: port = int(cast(str, ba.textwidget(query=port_textwidget))) except ValueError: # EWWWW; this exception causes a dependency loop that won't # go away until the next cyclical collection, which can # keep us alive. Perhaps should rethink our garbage # collection strategy, but for now just explicitly running # a cycle. ba.pushcall(ba.garbage_collect) port = -1 if port > 65535 or port < 0: ba.screenmessage(ba.Lstr(resource='internal.invalidPortErrorText'), color=(1, 0, 0)) ba.playsound(ba.getsound('error')) return _HostLookupThread(name=addr, port=port, call=ba.WeakCall(self._host_lookup_result)).start()
def _print(text: str, color: tuple[float, float, float] = None) -> None: def _print_in_logic_thread() -> None: win = weakwin() if win is not None: win.print(text, (1.0, 1.0, 1.0) if color is None else color) ba.pushcall(_print_in_logic_thread, from_other_thread=True)
def _set_sub_tab(self, value: SubTabType, playsound: bool = False) -> None: assert self._container if playsound: ba.playsound(ba.getsound('click01')) # If switching from join to host, do a fresh state query. if self._state.sub_tab is SubTabType.JOIN and value is SubTabType.HOST: # Prevent taking any action until we've gotten a fresh state. self._waiting_for_initial_state = True # This will get a state query sent out immediately. self._last_hosting_state_query_time = None self._last_action_send_time = None # So we don't ignore response. self._update() self._state.sub_tab = value active_color = (0.6, 1.0, 0.6) inactive_color = (0.5, 0.4, 0.5) ba.textwidget( edit=self._join_sub_tab_text, color=active_color if value is SubTabType.JOIN else inactive_color) ba.textwidget( edit=self._host_sub_tab_text, color=active_color if value is SubTabType.HOST else inactive_color) self._refresh_sub_tab() # Kick off an update to get any needed messages sent/etc. ba.pushcall(self._update)
def on_player_leave(self, player: ba.Player) -> None: ba.TeamGameActivity.on_player_leave(self, player) # A player leaving disqualifies the team if 'Entire Team Must Finish' # is on (otherwise in teams mode everyone could just leave except the # leading player to win). if (isinstance(self.session, ba.DualTeamSession) and self.settings.get('Entire Team Must Finish')): ba.screenmessage(ba.Lstr( translate=('statements', '${TEAM} is disqualified because ${PLAYER} left'), subs=[('${TEAM}', player.team.name), ('${PLAYER}', player.get_name(full=True))]), color=(1, 1, 0)) player.team.gamedata['finished'] = True player.team.gamedata['time'] = None player.team.gamedata['lap'] = 0 ba.playsound(ba.getsound('boo')) for otherplayer in player.team.players: otherplayer.gamedata['lap'] = 0 otherplayer.gamedata['finished'] = True try: if otherplayer.actor is not None: otherplayer.actor.handlemessage(ba.DieMessage()) except Exception: ba.print_exception('Error sending diemessages') # Defer so team/player lists will be updated. ba.pushcall(self._check_end_game)
def handlemessage(self, msg: Any) -> Any: if isinstance(msg, PlayerSpazDeathMessage): # Augment standard behavior. super().handlemessage(msg) curtime = ba.time() # Record the player's moment of death. PlayerData.get(msg.spaz.player).death_time = curtime # In co-op mode, end the game the instant everyone dies # (more accurate looking). # In teams/ffa, allow a one-second fudge-factor so we can # get more draws if players die basically at the same time. if isinstance(self.session, ba.CoopSession): # Teams will still show up if we check now.. check in # the next cycle. ba.pushcall(self._check_end_game) # Also record this for a final setting of the clock. self._last_player_death_time = curtime else: ba.timer(1.0, self._check_end_game) else: # Default handler: super().handlemessage(msg)
def run(self) -> None: result: Optional[str] try: import socket result = socket.gethostbyname(self._name) except Exception: result = None ba.pushcall(lambda: self._call(result, self._port), from_other_thread=True)
def run() -> None: if os.path.exists(src) and os.path.exists(dst): if not os.path.exists(dst + os.path.sep + os.path.basename(src)): try: shutil.copy(src, dst) except: pass # pass permission errors if callback is not None: ba.pushcall(ba.Call(callback, True), from_other_thread=True) elif callback is not None: ba.pushcall(ba.Call(callback, False), from_other_thread=True)
def _uninstall_target(): try: bap.uninstall(self.pkginfo.name) except Exception as e: ba.print_exception() ba.pushcall(ba.Call(ba.screenmessage, f'Error: {e}', color=(1, 0, 0)), from_other_thread=True) else: ba.pushcall(ba.Call(ba.screenmessage, 'Done', color=(0, 1, 0)), from_other_thread=True) if self._parent: self._parent._push_refresh()
def _run_addr_fetch(self) -> None: try: # FIXME: Update this to work with IPv6. import socket sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.connect(('8.8.8.8', 80)) val = sock.getsockname()[0] sock.close() ba.pushcall( ba.Call( _safe_set_text, self._checking_state_text, val, ), from_other_thread=True, ) except Exception as exc: from efro.error import is_udp_network_error if is_udp_network_error(exc): ba.pushcall(ba.Call( _safe_set_text, self._checking_state_text, ba.Lstr(resource='gatherWindow.' 'noConnectionText'), False), from_other_thread=True) else: ba.pushcall(ba.Call( _safe_set_text, self._checking_state_text, ba.Lstr(resource='gatherWindow.' 'addressFetchErrorText'), False), from_other_thread=True) ba.pushcall(ba.Call(ba.print_error, 'error in AddrFetchThread: ' + str(exc)), from_other_thread=True)
def __del__(self) -> None: scoreboard = self._scoreboard() # Remove our team from the scoreboard if its still around. # (but deferred, in case we die in a sim step or something where # its illegal to modify nodes) if scoreboard is None: return try: ba.pushcall(ba.Call(scoreboard.remove_team, self._team_id)) except ba.ContextError: # This happens if we fire after the activity expires. # In that case we don't need to do anything. pass
def run(self) -> None: try: # FIXME: Update this to work with IPv6 at some point. import socket sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.connect(('8.8.8.8', 80)) val = sock.getsockname()[0] sock.close() ba.pushcall(ba.Call(self._call, val), from_other_thread=True) except Exception as exc: # Ignore expected network errors; log others. import errno if isinstance(exc, OSError) and exc.errno == errno.ENETUNREACH: pass else: ba.print_exception()
def run(self) -> None: try: # FIXME: Update this to work with IPv6 at some point. import socket sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.connect(('8.8.8.8', 80)) val = sock.getsockname()[0] sock.close() ba.pushcall(ba.Call(self._call, val), from_other_thread=True) except Exception as exc: from efro.error import is_udp_network_error # Ignore expected network errors; log others. if is_udp_network_error(exc): pass else: ba.print_exception()
def run(self) -> None: ba.app.ping_thread_count += 1 sock: Optional[socket.socket] = None try: import socket from ba.internal import get_ip_address_type socket_type = get_ip_address_type(self._address) sock = socket.socket(socket_type, socket.SOCK_DGRAM) sock.connect((self._address, self._port)) accessible = False starttime = time.time() # Send a few pings and wait a second for # a response. sock.settimeout(1) for _i in range(3): sock.send(b'\x0b') result: Optional[bytes] try: # 11: BA_PACKET_SIMPLE_PING result = sock.recv(10) except Exception: result = None if result == b'\x0c': # 12: BA_PACKET_SIMPLE_PONG accessible = True break time.sleep(1) ping = (time.time() - starttime) * 1000.0 ba.pushcall(ba.Call(self._call, self._address, self._port, ping if accessible else None), from_other_thread=True) except Exception as exc: from efro.error import is_udp_network_error if is_udp_network_error(exc): pass else: ba.print_exception('Error on gather ping', once=True) finally: try: if sock is not None: sock.close() except Exception: ba.print_exception('Error on gather ping cleanup', once=True) ba.app.ping_thread_count -= 1
def handlemessage(self, msg: Any) -> Any: # A player has died. if isinstance(msg, playerspaz.PlayerSpazDeathMessage): super().handlemessage(msg) # do standard stuff self.respawn_player(msg.spaz.player) # kick off a respawn # A spaz-bot has died. elif isinstance(msg, spazbot.SpazBotDeathMessage): # Unfortunately the bot-set will always tell us there are living # bots if we ask here (the currently-dying bot isn't officially # marked dead yet) ..so lets push a call into the event loop to # check once this guy has finished dying. ba.pushcall(self._check_if_won) else: # Let the base class handle anything we don't. super().handlemessage(msg)
def _restore_state(self) -> None: try: for tab in self._tabs.values(): tab.restore_state() sel: Optional[ba.Widget] winstate = ba.app.ui.window_states.get(self.__class__.__name__, {}) sel_name = winstate.get('sel_name', None) assert isinstance(sel_name, (str, type(None))) current_tab = self.TabID.ABOUT gather_tab_val = ba.app.config.get('Gather Tab') try: stored_tab = self.TabID(gather_tab_val) if stored_tab in self._tab_row.tabs: current_tab = stored_tab except ValueError: # EWWWW; this exception causes a dependency loop that won't # go away until the next cyclical collection, which can # keep us alive. Perhaps should rethink our garbage # collection strategy, but for now just explicitly running # a cycle. ba.pushcall(ba.garbage_collect) self._set_tab(current_tab) if sel_name == 'Back': sel = self._back_button elif sel_name == 'TabContainer': sel = self._tab_container elif isinstance(sel_name, str) and sel_name.startswith('Tab:'): try: sel_tab_id = self.TabID(sel_name.split(':')[-1]) except ValueError: sel_tab_id = self.TabID.ABOUT # EWWWW; this exception causes a dependency loop that won't # go away until the next cyclical collection, which can # keep us alive. Perhaps should rethink our garbage # collection strategy, but for now just explicitly running # a cycle. ba.pushcall(ba.garbage_collect) sel = self._tab_row.tabs[sel_tab_id].button else: sel = self._tab_row.tabs[current_tab].button ba.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: ba.print_exception('Error restoring gather-win state.')
def handlemessage(self, msg: Any) -> Any: # A player has died. if isinstance(msg, ba.PlayerDiedMessage): super().handlemessage(msg) # Augment standard behavior. self.respawn_player(msg.getplayer(Player)) # A spaz-bot has died. elif isinstance(msg, SpazBotDiedMessage): # Unfortunately the bot-set will always tell us there are living # bots if we ask here (the currently-dying bot isn't officially # marked dead yet) ..so lets push a call into the event loop to # check once this guy has finished dying. ba.pushcall(self._check_if_won) # Let the base class handle anything we don't. else: return super().handlemessage(msg) return None
def __del__(self) -> None: # ew. our destructor here may get called as part of an internal # widget tear-down. # running further widget calls here can quietly break stuff, so we # need to push a deferred call to kill these as necessary instead. # (should bulletproof internal widget code to give a clean error # in this case) def kill_widgets(widgets: Sequence[Optional[ba.Widget]]) -> None: for widget in widgets: if widget: widget.delete() ba.pushcall( ba.Call(kill_widgets, [ self._body_image, self._eyes_image, self._body_image_target, self._eyes_image_target, self._name_text ]))
def run(self) -> None: try: starttime = time.time() files = os.listdir(self._path) duration = time.time() - starttime min_time = 0.1 # Make sure this takes at least 1/10 second so the user # has time to see the selection highlight. if duration < min_time: time.sleep(min_time - duration) ba.pushcall(ba.Call(self._callback, files, None), from_other_thread=True) except Exception as exc: # Ignore permission-denied. if 'Errno 13' not in str(exc): ba.print_exception() nofiles: List[str] = [] ba.pushcall(ba.Call(self._callback, nofiles, str(exc)), from_other_thread=True)
def _capture_button(self, pos: Tuple[float, float], color: Tuple[float, float, float], texture: ba.Texture, button: str, scale: float = 1.0) -> None: base_size = 79 btn = ba.buttonwidget(parent=self._root_widget, autoselect=True, position=(pos[0] - base_size * 0.5 * scale, pos[1] - base_size * 0.5 * scale), size=(base_size * scale, base_size * scale), texture=texture, label='', color=color) # Do this deferred so it shows up on top of other buttons. (ew.) def doit() -> None: if not self._root_widget: return uiscale = 0.66 * scale * 2.0 maxwidth = 76.0 * scale txt = ba.textwidget(parent=self._root_widget, position=(pos[0] + 0.0 * scale, pos[1] - (57.0 - 18.0) * scale), color=(1, 1, 1, 0.3), size=(0, 0), h_align='center', v_align='top', scale=uiscale, maxwidth=maxwidth, text=self._input.get_button_name( self._settings[button])) ba.buttonwidget(edit=btn, autoselect=True, on_activate_call=ba.Call(AwaitKeyboardInputWindow, button, txt, self._settings)) ba.pushcall(doit)
def _test_v1_transaction() -> None: """Dummy fail test case.""" if _ba.get_v1_account_state() != 'signed_in': raise RuntimeError('Not signed in.') starttime = time.monotonic() # Gets set to True on success or string on error. results: list[Any] = [False] def _cb(cbresults: Any) -> None: # Simply set results here; our other thread acts on them. if not isinstance(cbresults, dict) or 'party_code' not in cbresults: results[0] = 'Unexpected transaction response' return results[0] = True # Success! def _do_it() -> None: # Fire off a transaction with a callback. _ba.add_transaction( { 'type': 'PRIVATE_PARTY_QUERY', 'expire_time': time.time() + 20, }, callback=_cb, ) _ba.run_transactions() ba.pushcall(_do_it, from_other_thread=True) while results[0] is False: time.sleep(0.01) if time.monotonic() - starttime > 10.0: raise RuntimeError('timed out') # If we got left a string, its an error. if isinstance(results[0], str): raise RuntimeError(results[0])
def _sync_target(self): try: bap.repo.sync() except Exception as e: ba.pushcall(ba.Call(ba.screenmessage, f"Error: {e}", color=(1, 0, 0)), from_other_thread=True) else: ba.pushcall(ba.Call(ba.screenmessage, "Done", color=(0, 1, 0)), from_other_thread=True) ba.pushcall(self._refresh, from_other_thread=True)
def _run_addr_fetch(self) -> None: try: # FIXME: Update this to work with IPv6. import socket sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.connect(('8.8.8.8', 80)) val = sock.getsockname()[0] sock.close() ba.pushcall( ba.Call( _safe_set_text, self._checking_state_text, val, ), from_other_thread=True, ) except Exception as exc: err_str = str(exc) # FIXME: Should look at exception types here, # not strings. if 'Network is unreachable' in err_str: ba.pushcall(ba.Call( _safe_set_text, self._checking_state_text, ba.Lstr(resource='gatherWindow.' 'noConnectionText'), False), from_other_thread=True) else: ba.pushcall(ba.Call( _safe_set_text, self._checking_state_text, ba.Lstr(resource='gatherWindow.' 'addressFetchErrorText'), False), from_other_thread=True) ba.pushcall(ba.Call(ba.print_error, 'error in AddrFetchThread: ' + str(exc)), from_other_thread=True)
def _install_target(): from bap.consts import CACHE_DIR import os try: for p in bap.repo.download(self.pkginfo.name, progress=True): ba.pushcall(ba.Call(ba.screenmessage, f'Downloading ({p}%)...'), from_other_thread=True) ba.pushcall(ba.Call(ba.screenmessage, 'Installing...', color=(1, 1, 1)), from_other_thread=True) bap.install(os.path.join(CACHE_DIR, self.pkginfo.name + '.bap'), upgrade=upgrade) except Exception as e: ba.print_exception() ba.pushcall(ba.Call(ba.screenmessage, f'Error: {e}', color=(1, 0, 0)), from_other_thread=True) else: ba.pushcall(ba.Call(ba.screenmessage, 'Done', color=(0, 1, 0)), from_other_thread=True)
def end_game(self) -> None: ba.pushcall(ba.Call(self.do_end, 'defeat')) ba.setmusic(None) ba.playsound(self._player_death_sound)
def end_game(self) -> None: # Tell our bots to celebrate just to rub it in. self._bots.final_celebrate() ba.setmusic(None) ba.pushcall(ba.WeakCall(self.do_end, 'defeat'))
def _call() -> None: result = call() if callback: ba.pushcall(ba.Call(callback, result), from_other_thread=True)
def _update_server_list(self) -> None: cur_time = ba.time(ba.TimeType.REAL) if self._first_server_list_rebuild_time is None: self._first_server_list_rebuild_time = cur_time # We get called quite often (for each ping response, etc) so we want # to limit our rebuilds to keep the UI responsive. # Let's update faster for the first few seconds, # then ease off to keep the list from jumping around. since_first = cur_time - self._first_server_list_rebuild_time wait_time = (1.0 if since_first < 2.0 else 2.5 if since_first < 10.0 else 5.0) if (not self._server_list_dirty and self._last_server_list_update_time is not None and cur_time - self._last_server_list_update_time < wait_time): return # If we somehow got here without the required UI being in place... columnwidget = self._join_list_column if not columnwidget: return self._last_server_list_update_time = cur_time self._server_list_dirty = False with ba.Context('ui'): # Now kill and recreate all widgets. for widget in columnwidget.get_children(): widget.delete() ordered_parties = self._get_ordered_parties() # If we've got a filter, filter them. if self._filter_value: # Let's do case-insensitive searching. filterval = self._filter_value.lower() ordered_parties = [ p for p in ordered_parties if filterval in p.name.lower() ] sub_scroll_width = 830 lineheight = 42 sub_scroll_height = lineheight * len(ordered_parties) + 50 ba.containerwidget(edit=columnwidget, size=(sub_scroll_width, sub_scroll_height)) # Ew; this rebuilding generates deferred selection callbacks # so we need to generated deferred ignore notices for ourself. def refresh_on() -> None: self._refreshing_list = True ba.pushcall(refresh_on) # Janky - allow escaping if there's nothing in us. ba.containerwidget(edit=self._host_scrollwidget, claims_up_down=(len(ordered_parties) > 0)) self._build_server_entry_lines(lineheight, ordered_parties, sub_scroll_height, sub_scroll_width) # So our selection callbacks can start firing.. def refresh_off() -> None: self._refreshing_list = False ba.pushcall(refresh_off)
def run(self) -> None: # pylint: disable=too-many-branches # pylint: disable=too-many-statements ba.app.ping_thread_count += 1 sock: Optional[socket.socket] = None try: import socket from ba.internal import get_ip_address_type socket_type = get_ip_address_type(self._address) sock = socket.socket(socket_type, socket.SOCK_DGRAM) sock.connect((self._address, self._port)) accessible = False starttime = time.time() # Send a few pings and wait a second for # a response. sock.settimeout(1) for _i in range(3): sock.send(b'\x0b') result: Optional[bytes] try: # 11: BA_PACKET_SIMPLE_PING result = sock.recv(10) except Exception: result = None if result == b'\x0c': # 12: BA_PACKET_SIMPLE_PONG accessible = True break time.sleep(1) ping = (time.time() - starttime) * 1000.0 ba.pushcall(ba.Call(self._call, self._address, self._port, ping if accessible else None), from_other_thread=True) except ConnectionRefusedError: # Fine, server; sorry we pinged you. Hmph. pass except OSError as exc: import errno # Ignore harmless errors. if exc.errno in { errno.EHOSTUNREACH, errno.ENETUNREACH, errno.EINVAL, errno.EPERM, errno.EACCES }: pass elif exc.errno == 10022: # Windows 'invalid argument' error. pass elif exc.errno == 10051: # Windows 'a socket operation was attempted # to an unreachable network' error. pass elif exc.errno == errno.EADDRNOTAVAIL: if self._port == 0: # This has happened. Ignore. pass elif ba.do_once(): print(f'Got EADDRNOTAVAIL on gather ping' f' for addr {self._address}' f' port {self._port}.') else: ba.print_exception( f'Error on gather ping ' f'(errno={exc.errno})', once=True) except Exception: ba.print_exception('Error on gather ping', once=True) finally: try: if sock is not None: sock.close() except Exception: ba.print_exception('Error on gather ping cleanup', once=True) ba.app.ping_thread_count -= 1
def _update_party_rows(self) -> None: columnwidget = self._join_list_column if not columnwidget: return assert self._join_text assert self._filter_text # Janky - allow escaping when there's nothing in our list. assert self._host_scrollwidget ba.containerwidget(edit=self._host_scrollwidget, claims_up_down=(len(self._parties_displayed) > 0)) # Clip if we have more UI rows than parties to show. clipcount = len(self._ui_rows) - len(self._parties_displayed) if clipcount > 0: clipcount = max(clipcount, 50) self._ui_rows = self._ui_rows[:-clipcount] # If we have no parties to show, we're done. if not self._parties_displayed: return sub_scroll_width = 830 lineheight = 42 sub_scroll_height = lineheight * len(self._parties_displayed) + 50 ba.containerwidget(edit=columnwidget, size=(sub_scroll_width, sub_scroll_height)) # Any time our height changes, reset the refresh back to the top # so we don't see ugly empty spaces appearing during initial list # filling. if sub_scroll_height != self._last_sub_scroll_height: self._refresh_ui_row = 0 self._last_sub_scroll_height = sub_scroll_height # Also note that we need to redisplay everything since its pos # will have changed.. :( for party in self._parties.values(): party.clean_display_index = None # Ew; this rebuilding generates deferred selection callbacks # so we need to push deferred notices so we know to ignore them. def refresh_on() -> None: self._refreshing_list = True ba.pushcall(refresh_on) # Ok, now here's the deal: we want to avoid creating/updating this # entire list at one time because it will lead to hitches. So we # refresh individual rows quickly in a loop. rowcount = min(12, len(self._parties_displayed)) party_vals_displayed = list(self._parties_displayed.values()) while rowcount > 0: refresh_row = self._refresh_ui_row % len(self._parties_displayed) if refresh_row >= len(self._ui_rows): self._ui_rows.append(UIRow()) refresh_row = len(self._ui_rows) - 1 # For the first few seconds after getting our first server-list, # refresh only the top section of the list; this allows the lowest # ping servers to show up more quickly. if self._first_valid_server_list_time is not None: if time.time() - self._first_valid_server_list_time < 4.0: if refresh_row > 40: refresh_row = 0 self._ui_rows[refresh_row].update( refresh_row, party_vals_displayed[refresh_row], sub_scroll_width=sub_scroll_width, sub_scroll_height=sub_scroll_height, lineheight=lineheight, columnwidget=columnwidget, join_text=self._join_text, existing_selection=self._selection, filter_text=self._filter_text, tab=self) self._refresh_ui_row = refresh_row + 1 rowcount -= 1 # So our selection callbacks can start firing.. def refresh_off() -> None: self._refreshing_list = False ba.pushcall(refresh_off)