bastd.ui.party
Provides party related UI.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Provides party related UI.""" 4 5from __future__ import annotations 6 7import math 8from typing import TYPE_CHECKING, cast 9 10import _ba 11import ba 12from bastd.ui import popup 13 14if TYPE_CHECKING: 15 from typing import Sequence, Any 16 17 18class PartyWindow(ba.Window): 19 """Party list/chat window.""" 20 21 def __del__(self) -> None: 22 _ba.set_party_window_open(False) 23 24 def __init__(self, origin: Sequence[float] = (0, 0)): 25 _ba.set_party_window_open(True) 26 self._r = 'partyWindow' 27 self._popup_type: str | None = None 28 self._popup_party_member_client_id: int | None = None 29 self._popup_party_member_is_host: bool | None = None 30 self._width = 500 31 uiscale = ba.app.ui.uiscale 32 self._height = (365 if uiscale is ba.UIScale.SMALL else 33 480 if uiscale is ba.UIScale.MEDIUM else 600) 34 super().__init__(root_widget=ba.containerwidget( 35 size=(self._width, self._height), 36 transition='in_scale', 37 color=(0.40, 0.55, 0.20), 38 parent=_ba.get_special_widget('overlay_stack'), 39 on_outside_click_call=self.close_with_sound, 40 scale_origin_stack_offset=origin, 41 scale=(2.0 if uiscale is ba.UIScale.SMALL else 42 1.35 if uiscale is ba.UIScale.MEDIUM else 1.0), 43 stack_offset=(0, -10) if uiscale is ba.UIScale.SMALL else ( 44 240, 0) if uiscale is ba.UIScale.MEDIUM else (330, 20))) 45 46 self._cancel_button = ba.buttonwidget(parent=self._root_widget, 47 scale=0.7, 48 position=(30, self._height - 47), 49 size=(50, 50), 50 label='', 51 on_activate_call=self.close, 52 autoselect=True, 53 color=(0.45, 0.63, 0.15), 54 icon=ba.gettexture('crossOut'), 55 iconscale=1.2) 56 ba.containerwidget(edit=self._root_widget, 57 cancel_button=self._cancel_button) 58 59 self._menu_button = ba.buttonwidget( 60 parent=self._root_widget, 61 scale=0.7, 62 position=(self._width - 60, self._height - 47), 63 size=(50, 50), 64 label='...', 65 autoselect=True, 66 button_type='square', 67 on_activate_call=ba.WeakCall(self._on_menu_button_press), 68 color=(0.55, 0.73, 0.25), 69 iconscale=1.2) 70 71 info = _ba.get_connection_to_host_info() 72 if info.get('name', '') != '': 73 title = ba.Lstr(value=info['name']) 74 else: 75 title = ba.Lstr(resource=self._r + '.titleText') 76 77 self._title_text = ba.textwidget(parent=self._root_widget, 78 scale=0.9, 79 color=(0.5, 0.7, 0.5), 80 text=title, 81 size=(0, 0), 82 position=(self._width * 0.5, 83 self._height - 29), 84 maxwidth=self._width * 0.7, 85 h_align='center', 86 v_align='center') 87 88 self._empty_str = ba.textwidget(parent=self._root_widget, 89 scale=0.75, 90 size=(0, 0), 91 position=(self._width * 0.5, 92 self._height - 65), 93 maxwidth=self._width * 0.85, 94 h_align='center', 95 v_align='center') 96 97 self._scroll_width = self._width - 50 98 self._scrollwidget = ba.scrollwidget(parent=self._root_widget, 99 size=(self._scroll_width, 100 self._height - 200), 101 position=(30, 80), 102 color=(0.4, 0.6, 0.3)) 103 self._columnwidget = ba.columnwidget(parent=self._scrollwidget, 104 border=2, 105 margin=0) 106 ba.widget(edit=self._menu_button, down_widget=self._columnwidget) 107 108 self._muted_text = ba.textwidget( 109 parent=self._root_widget, 110 position=(self._width * 0.5, self._height * 0.5), 111 size=(0, 0), 112 h_align='center', 113 v_align='center', 114 text=ba.Lstr(resource='chatMutedText')) 115 self._chat_texts: list[ba.Widget] = [] 116 117 # add all existing messages if chat is not muted 118 if not ba.app.config.resolve('Chat Muted'): 119 msgs = _ba.get_chat_messages() 120 for msg in msgs: 121 self._add_msg(msg) 122 123 self._text_field = txt = ba.textwidget( 124 parent=self._root_widget, 125 editable=True, 126 size=(530, 40), 127 position=(44, 39), 128 text='', 129 maxwidth=494, 130 shadow=0.3, 131 flatness=1.0, 132 description=ba.Lstr(resource=self._r + '.chatMessageText'), 133 autoselect=True, 134 v_align='center', 135 corner_scale=0.7) 136 137 ba.widget(edit=self._scrollwidget, 138 autoselect=True, 139 left_widget=self._cancel_button, 140 up_widget=self._cancel_button, 141 down_widget=self._text_field) 142 ba.widget(edit=self._columnwidget, 143 autoselect=True, 144 up_widget=self._cancel_button, 145 down_widget=self._text_field) 146 ba.containerwidget(edit=self._root_widget, selected_child=txt) 147 btn = ba.buttonwidget(parent=self._root_widget, 148 size=(50, 35), 149 label=ba.Lstr(resource=self._r + '.sendText'), 150 button_type='square', 151 autoselect=True, 152 position=(self._width - 70, 35), 153 on_activate_call=self._send_chat_message) 154 ba.textwidget(edit=txt, on_return_press_call=btn.activate) 155 self._name_widgets: list[ba.Widget] = [] 156 self._roster: list[dict[str, Any]] | None = None 157 self._update_timer = ba.Timer(1.0, 158 ba.WeakCall(self._update), 159 repeat=True, 160 timetype=ba.TimeType.REAL) 161 self._update() 162 163 def on_chat_message(self, msg: str) -> None: 164 """Called when a new chat message comes through.""" 165 if not ba.app.config.resolve('Chat Muted'): 166 self._add_msg(msg) 167 168 def _add_msg(self, msg: str) -> None: 169 txt = ba.textwidget(parent=self._columnwidget, 170 text=msg, 171 h_align='left', 172 v_align='center', 173 size=(0, 13), 174 scale=0.55, 175 maxwidth=self._scroll_width * 0.94, 176 shadow=0.3, 177 flatness=1.0) 178 self._chat_texts.append(txt) 179 if len(self._chat_texts) > 40: 180 first = self._chat_texts.pop(0) 181 first.delete() 182 ba.containerwidget(edit=self._columnwidget, visible_child=txt) 183 184 def _on_menu_button_press(self) -> None: 185 is_muted = ba.app.config.resolve('Chat Muted') 186 uiscale = ba.app.ui.uiscale 187 popup.PopupMenuWindow( 188 position=self._menu_button.get_screen_space_center(), 189 scale=(2.3 if uiscale is ba.UIScale.SMALL else 190 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23), 191 choices=['unmute' if is_muted else 'mute'], 192 choices_display=[ 193 ba.Lstr( 194 resource='chatUnMuteText' if is_muted else 'chatMuteText') 195 ], 196 current_choice='unmute' if is_muted else 'mute', 197 delegate=self) 198 self._popup_type = 'menu' 199 200 def _update(self) -> None: 201 # pylint: disable=too-many-locals 202 # pylint: disable=too-many-branches 203 # pylint: disable=too-many-statements 204 # pylint: disable=too-many-nested-blocks 205 206 # update muted state 207 if ba.app.config.resolve('Chat Muted'): 208 ba.textwidget(edit=self._muted_text, color=(1, 1, 1, 0.3)) 209 # clear any chat texts we're showing 210 if self._chat_texts: 211 while self._chat_texts: 212 first = self._chat_texts.pop() 213 first.delete() 214 else: 215 ba.textwidget(edit=self._muted_text, color=(1, 1, 1, 0.0)) 216 217 # update roster section 218 roster = _ba.get_game_roster() 219 if roster != self._roster: 220 self._roster = roster 221 222 # clear out old 223 for widget in self._name_widgets: 224 widget.delete() 225 self._name_widgets = [] 226 if not self._roster: 227 top_section_height = 60 228 ba.textwidget(edit=self._empty_str, 229 text=ba.Lstr(resource=self._r + '.emptyText')) 230 ba.scrollwidget(edit=self._scrollwidget, 231 size=(self._width - 50, 232 self._height - top_section_height - 110), 233 position=(30, 80)) 234 else: 235 columns = 1 if len( 236 self._roster) == 1 else 2 if len(self._roster) == 2 else 3 237 rows = int(math.ceil(float(len(self._roster)) / columns)) 238 c_width = (self._width * 0.9) / max(3, columns) 239 c_width_total = c_width * columns 240 c_height = 24 241 c_height_total = c_height * rows 242 for y in range(rows): 243 for x in range(columns): 244 index = y * columns + x 245 if index < len(self._roster): 246 t_scale = 0.65 247 pos = (self._width * 0.53 - c_width_total * 0.5 + 248 c_width * x - 23, 249 self._height - 65 - c_height * y - 15) 250 251 # if there are players present for this client, use 252 # their names as a display string instead of the 253 # client spec-string 254 try: 255 if self._roster[index]['players']: 256 # if there's just one, use the full name; 257 # otherwise combine short names 258 if len(self._roster[index] 259 ['players']) == 1: 260 p_str = self._roster[index]['players'][ 261 0]['name_full'] 262 else: 263 p_str = ('/'.join([ 264 entry['name'] for entry in 265 self._roster[index]['players'] 266 ])) 267 if len(p_str) > 25: 268 p_str = p_str[:25] + '...' 269 else: 270 p_str = self._roster[index][ 271 'display_string'] 272 except Exception: 273 ba.print_exception( 274 'Error calcing client name str.') 275 p_str = '???' 276 277 widget = ba.textwidget(parent=self._root_widget, 278 position=(pos[0], pos[1]), 279 scale=t_scale, 280 size=(c_width * 0.85, 30), 281 maxwidth=c_width * 0.85, 282 color=(1, 1, 283 1) if index == 0 else 284 (1, 1, 1), 285 selectable=True, 286 autoselect=True, 287 click_activate=True, 288 text=ba.Lstr(value=p_str), 289 h_align='left', 290 v_align='center') 291 self._name_widgets.append(widget) 292 293 # in newer versions client_id will be present and 294 # we can use that to determine who the host is. 295 # in older versions we assume the first client is 296 # host 297 if self._roster[index]['client_id'] is not None: 298 is_host = self._roster[index][ 299 'client_id'] == -1 300 else: 301 is_host = (index == 0) 302 303 # FIXME: Should pass client_id to these sort of 304 # calls; not spec-string (perhaps should wait till 305 # client_id is more readily available though). 306 ba.textwidget(edit=widget, 307 on_activate_call=ba.Call( 308 self._on_party_member_press, 309 self._roster[index]['client_id'], 310 is_host, widget)) 311 pos = (self._width * 0.53 - c_width_total * 0.5 + 312 c_width * x, 313 self._height - 65 - c_height * y) 314 315 # Make the assumption that the first roster 316 # entry is the server. 317 # FIXME: Shouldn't do this. 318 if is_host: 319 twd = min( 320 c_width * 0.85, 321 _ba.get_string_width( 322 p_str, suppress_warning=True) * 323 t_scale) 324 self._name_widgets.append( 325 ba.textwidget( 326 parent=self._root_widget, 327 position=(pos[0] + twd + 1, 328 pos[1] - 0.5), 329 size=(0, 0), 330 h_align='left', 331 v_align='center', 332 maxwidth=c_width * 0.96 - twd, 333 color=(0.1, 1, 0.1, 0.5), 334 text=ba.Lstr(resource=self._r + 335 '.hostText'), 336 scale=0.4, 337 shadow=0.1, 338 flatness=1.0)) 339 ba.textwidget(edit=self._empty_str, text='') 340 ba.scrollwidget(edit=self._scrollwidget, 341 size=(self._width - 50, 342 max(100, self._height - 139 - 343 c_height_total)), 344 position=(30, 80)) 345 346 def popup_menu_selected_choice(self, popup_window: popup.PopupMenuWindow, 347 choice: str) -> None: 348 """Called when a choice is selected in the popup.""" 349 del popup_window # unused 350 if self._popup_type == 'partyMemberPress': 351 if self._popup_party_member_is_host: 352 ba.playsound(ba.getsound('error')) 353 ba.screenmessage( 354 ba.Lstr(resource='internal.cantKickHostError'), 355 color=(1, 0, 0)) 356 else: 357 assert self._popup_party_member_client_id is not None 358 359 # Ban for 5 minutes. 360 result = _ba.disconnect_client( 361 self._popup_party_member_client_id, ban_time=5 * 60) 362 if not result: 363 ba.playsound(ba.getsound('error')) 364 ba.screenmessage( 365 ba.Lstr(resource='getTicketsWindow.unavailableText'), 366 color=(1, 0, 0)) 367 elif self._popup_type == 'menu': 368 if choice in ('mute', 'unmute'): 369 cfg = ba.app.config 370 cfg['Chat Muted'] = (choice == 'mute') 371 cfg.apply_and_commit() 372 self._update() 373 else: 374 print(f'unhandled popup type: {self._popup_type}') 375 376 def popup_menu_closing(self, popup_window: popup.PopupWindow) -> None: 377 """Called when the popup is closing.""" 378 379 def _on_party_member_press(self, client_id: int, is_host: bool, 380 widget: ba.Widget) -> None: 381 # if we're the host, pop up 'kick' options for all non-host members 382 if _ba.get_foreground_host_session() is not None: 383 kick_str = ba.Lstr(resource='kickText') 384 else: 385 # kick-votes appeared in build 14248 386 if (_ba.get_connection_to_host_info().get('build_number', 0) < 387 14248): 388 return 389 kick_str = ba.Lstr(resource='kickVoteText') 390 uiscale = ba.app.ui.uiscale 391 popup.PopupMenuWindow( 392 position=widget.get_screen_space_center(), 393 scale=(2.3 if uiscale is ba.UIScale.SMALL else 394 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23), 395 choices=['kick'], 396 choices_display=[kick_str], 397 current_choice='kick', 398 delegate=self) 399 self._popup_type = 'partyMemberPress' 400 self._popup_party_member_client_id = client_id 401 self._popup_party_member_is_host = is_host 402 403 def _send_chat_message(self) -> None: 404 _ba.chatmessage(cast(str, ba.textwidget(query=self._text_field))) 405 ba.textwidget(edit=self._text_field, text='') 406 407 def close(self) -> None: 408 """Close the window.""" 409 ba.containerwidget(edit=self._root_widget, transition='out_scale') 410 411 def close_with_sound(self) -> None: 412 """Close the window and make a lovely sound.""" 413 ba.playsound(ba.getsound('swish')) 414 self.close()
class
PartyWindow(ba.ui.Window):
19class PartyWindow(ba.Window): 20 """Party list/chat window.""" 21 22 def __del__(self) -> None: 23 _ba.set_party_window_open(False) 24 25 def __init__(self, origin: Sequence[float] = (0, 0)): 26 _ba.set_party_window_open(True) 27 self._r = 'partyWindow' 28 self._popup_type: str | None = None 29 self._popup_party_member_client_id: int | None = None 30 self._popup_party_member_is_host: bool | None = None 31 self._width = 500 32 uiscale = ba.app.ui.uiscale 33 self._height = (365 if uiscale is ba.UIScale.SMALL else 34 480 if uiscale is ba.UIScale.MEDIUM else 600) 35 super().__init__(root_widget=ba.containerwidget( 36 size=(self._width, self._height), 37 transition='in_scale', 38 color=(0.40, 0.55, 0.20), 39 parent=_ba.get_special_widget('overlay_stack'), 40 on_outside_click_call=self.close_with_sound, 41 scale_origin_stack_offset=origin, 42 scale=(2.0 if uiscale is ba.UIScale.SMALL else 43 1.35 if uiscale is ba.UIScale.MEDIUM else 1.0), 44 stack_offset=(0, -10) if uiscale is ba.UIScale.SMALL else ( 45 240, 0) if uiscale is ba.UIScale.MEDIUM else (330, 20))) 46 47 self._cancel_button = ba.buttonwidget(parent=self._root_widget, 48 scale=0.7, 49 position=(30, self._height - 47), 50 size=(50, 50), 51 label='', 52 on_activate_call=self.close, 53 autoselect=True, 54 color=(0.45, 0.63, 0.15), 55 icon=ba.gettexture('crossOut'), 56 iconscale=1.2) 57 ba.containerwidget(edit=self._root_widget, 58 cancel_button=self._cancel_button) 59 60 self._menu_button = ba.buttonwidget( 61 parent=self._root_widget, 62 scale=0.7, 63 position=(self._width - 60, self._height - 47), 64 size=(50, 50), 65 label='...', 66 autoselect=True, 67 button_type='square', 68 on_activate_call=ba.WeakCall(self._on_menu_button_press), 69 color=(0.55, 0.73, 0.25), 70 iconscale=1.2) 71 72 info = _ba.get_connection_to_host_info() 73 if info.get('name', '') != '': 74 title = ba.Lstr(value=info['name']) 75 else: 76 title = ba.Lstr(resource=self._r + '.titleText') 77 78 self._title_text = ba.textwidget(parent=self._root_widget, 79 scale=0.9, 80 color=(0.5, 0.7, 0.5), 81 text=title, 82 size=(0, 0), 83 position=(self._width * 0.5, 84 self._height - 29), 85 maxwidth=self._width * 0.7, 86 h_align='center', 87 v_align='center') 88 89 self._empty_str = ba.textwidget(parent=self._root_widget, 90 scale=0.75, 91 size=(0, 0), 92 position=(self._width * 0.5, 93 self._height - 65), 94 maxwidth=self._width * 0.85, 95 h_align='center', 96 v_align='center') 97 98 self._scroll_width = self._width - 50 99 self._scrollwidget = ba.scrollwidget(parent=self._root_widget, 100 size=(self._scroll_width, 101 self._height - 200), 102 position=(30, 80), 103 color=(0.4, 0.6, 0.3)) 104 self._columnwidget = ba.columnwidget(parent=self._scrollwidget, 105 border=2, 106 margin=0) 107 ba.widget(edit=self._menu_button, down_widget=self._columnwidget) 108 109 self._muted_text = ba.textwidget( 110 parent=self._root_widget, 111 position=(self._width * 0.5, self._height * 0.5), 112 size=(0, 0), 113 h_align='center', 114 v_align='center', 115 text=ba.Lstr(resource='chatMutedText')) 116 self._chat_texts: list[ba.Widget] = [] 117 118 # add all existing messages if chat is not muted 119 if not ba.app.config.resolve('Chat Muted'): 120 msgs = _ba.get_chat_messages() 121 for msg in msgs: 122 self._add_msg(msg) 123 124 self._text_field = txt = ba.textwidget( 125 parent=self._root_widget, 126 editable=True, 127 size=(530, 40), 128 position=(44, 39), 129 text='', 130 maxwidth=494, 131 shadow=0.3, 132 flatness=1.0, 133 description=ba.Lstr(resource=self._r + '.chatMessageText'), 134 autoselect=True, 135 v_align='center', 136 corner_scale=0.7) 137 138 ba.widget(edit=self._scrollwidget, 139 autoselect=True, 140 left_widget=self._cancel_button, 141 up_widget=self._cancel_button, 142 down_widget=self._text_field) 143 ba.widget(edit=self._columnwidget, 144 autoselect=True, 145 up_widget=self._cancel_button, 146 down_widget=self._text_field) 147 ba.containerwidget(edit=self._root_widget, selected_child=txt) 148 btn = ba.buttonwidget(parent=self._root_widget, 149 size=(50, 35), 150 label=ba.Lstr(resource=self._r + '.sendText'), 151 button_type='square', 152 autoselect=True, 153 position=(self._width - 70, 35), 154 on_activate_call=self._send_chat_message) 155 ba.textwidget(edit=txt, on_return_press_call=btn.activate) 156 self._name_widgets: list[ba.Widget] = [] 157 self._roster: list[dict[str, Any]] | None = None 158 self._update_timer = ba.Timer(1.0, 159 ba.WeakCall(self._update), 160 repeat=True, 161 timetype=ba.TimeType.REAL) 162 self._update() 163 164 def on_chat_message(self, msg: str) -> None: 165 """Called when a new chat message comes through.""" 166 if not ba.app.config.resolve('Chat Muted'): 167 self._add_msg(msg) 168 169 def _add_msg(self, msg: str) -> None: 170 txt = ba.textwidget(parent=self._columnwidget, 171 text=msg, 172 h_align='left', 173 v_align='center', 174 size=(0, 13), 175 scale=0.55, 176 maxwidth=self._scroll_width * 0.94, 177 shadow=0.3, 178 flatness=1.0) 179 self._chat_texts.append(txt) 180 if len(self._chat_texts) > 40: 181 first = self._chat_texts.pop(0) 182 first.delete() 183 ba.containerwidget(edit=self._columnwidget, visible_child=txt) 184 185 def _on_menu_button_press(self) -> None: 186 is_muted = ba.app.config.resolve('Chat Muted') 187 uiscale = ba.app.ui.uiscale 188 popup.PopupMenuWindow( 189 position=self._menu_button.get_screen_space_center(), 190 scale=(2.3 if uiscale is ba.UIScale.SMALL else 191 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23), 192 choices=['unmute' if is_muted else 'mute'], 193 choices_display=[ 194 ba.Lstr( 195 resource='chatUnMuteText' if is_muted else 'chatMuteText') 196 ], 197 current_choice='unmute' if is_muted else 'mute', 198 delegate=self) 199 self._popup_type = 'menu' 200 201 def _update(self) -> None: 202 # pylint: disable=too-many-locals 203 # pylint: disable=too-many-branches 204 # pylint: disable=too-many-statements 205 # pylint: disable=too-many-nested-blocks 206 207 # update muted state 208 if ba.app.config.resolve('Chat Muted'): 209 ba.textwidget(edit=self._muted_text, color=(1, 1, 1, 0.3)) 210 # clear any chat texts we're showing 211 if self._chat_texts: 212 while self._chat_texts: 213 first = self._chat_texts.pop() 214 first.delete() 215 else: 216 ba.textwidget(edit=self._muted_text, color=(1, 1, 1, 0.0)) 217 218 # update roster section 219 roster = _ba.get_game_roster() 220 if roster != self._roster: 221 self._roster = roster 222 223 # clear out old 224 for widget in self._name_widgets: 225 widget.delete() 226 self._name_widgets = [] 227 if not self._roster: 228 top_section_height = 60 229 ba.textwidget(edit=self._empty_str, 230 text=ba.Lstr(resource=self._r + '.emptyText')) 231 ba.scrollwidget(edit=self._scrollwidget, 232 size=(self._width - 50, 233 self._height - top_section_height - 110), 234 position=(30, 80)) 235 else: 236 columns = 1 if len( 237 self._roster) == 1 else 2 if len(self._roster) == 2 else 3 238 rows = int(math.ceil(float(len(self._roster)) / columns)) 239 c_width = (self._width * 0.9) / max(3, columns) 240 c_width_total = c_width * columns 241 c_height = 24 242 c_height_total = c_height * rows 243 for y in range(rows): 244 for x in range(columns): 245 index = y * columns + x 246 if index < len(self._roster): 247 t_scale = 0.65 248 pos = (self._width * 0.53 - c_width_total * 0.5 + 249 c_width * x - 23, 250 self._height - 65 - c_height * y - 15) 251 252 # if there are players present for this client, use 253 # their names as a display string instead of the 254 # client spec-string 255 try: 256 if self._roster[index]['players']: 257 # if there's just one, use the full name; 258 # otherwise combine short names 259 if len(self._roster[index] 260 ['players']) == 1: 261 p_str = self._roster[index]['players'][ 262 0]['name_full'] 263 else: 264 p_str = ('/'.join([ 265 entry['name'] for entry in 266 self._roster[index]['players'] 267 ])) 268 if len(p_str) > 25: 269 p_str = p_str[:25] + '...' 270 else: 271 p_str = self._roster[index][ 272 'display_string'] 273 except Exception: 274 ba.print_exception( 275 'Error calcing client name str.') 276 p_str = '???' 277 278 widget = ba.textwidget(parent=self._root_widget, 279 position=(pos[0], pos[1]), 280 scale=t_scale, 281 size=(c_width * 0.85, 30), 282 maxwidth=c_width * 0.85, 283 color=(1, 1, 284 1) if index == 0 else 285 (1, 1, 1), 286 selectable=True, 287 autoselect=True, 288 click_activate=True, 289 text=ba.Lstr(value=p_str), 290 h_align='left', 291 v_align='center') 292 self._name_widgets.append(widget) 293 294 # in newer versions client_id will be present and 295 # we can use that to determine who the host is. 296 # in older versions we assume the first client is 297 # host 298 if self._roster[index]['client_id'] is not None: 299 is_host = self._roster[index][ 300 'client_id'] == -1 301 else: 302 is_host = (index == 0) 303 304 # FIXME: Should pass client_id to these sort of 305 # calls; not spec-string (perhaps should wait till 306 # client_id is more readily available though). 307 ba.textwidget(edit=widget, 308 on_activate_call=ba.Call( 309 self._on_party_member_press, 310 self._roster[index]['client_id'], 311 is_host, widget)) 312 pos = (self._width * 0.53 - c_width_total * 0.5 + 313 c_width * x, 314 self._height - 65 - c_height * y) 315 316 # Make the assumption that the first roster 317 # entry is the server. 318 # FIXME: Shouldn't do this. 319 if is_host: 320 twd = min( 321 c_width * 0.85, 322 _ba.get_string_width( 323 p_str, suppress_warning=True) * 324 t_scale) 325 self._name_widgets.append( 326 ba.textwidget( 327 parent=self._root_widget, 328 position=(pos[0] + twd + 1, 329 pos[1] - 0.5), 330 size=(0, 0), 331 h_align='left', 332 v_align='center', 333 maxwidth=c_width * 0.96 - twd, 334 color=(0.1, 1, 0.1, 0.5), 335 text=ba.Lstr(resource=self._r + 336 '.hostText'), 337 scale=0.4, 338 shadow=0.1, 339 flatness=1.0)) 340 ba.textwidget(edit=self._empty_str, text='') 341 ba.scrollwidget(edit=self._scrollwidget, 342 size=(self._width - 50, 343 max(100, self._height - 139 - 344 c_height_total)), 345 position=(30, 80)) 346 347 def popup_menu_selected_choice(self, popup_window: popup.PopupMenuWindow, 348 choice: str) -> None: 349 """Called when a choice is selected in the popup.""" 350 del popup_window # unused 351 if self._popup_type == 'partyMemberPress': 352 if self._popup_party_member_is_host: 353 ba.playsound(ba.getsound('error')) 354 ba.screenmessage( 355 ba.Lstr(resource='internal.cantKickHostError'), 356 color=(1, 0, 0)) 357 else: 358 assert self._popup_party_member_client_id is not None 359 360 # Ban for 5 minutes. 361 result = _ba.disconnect_client( 362 self._popup_party_member_client_id, ban_time=5 * 60) 363 if not result: 364 ba.playsound(ba.getsound('error')) 365 ba.screenmessage( 366 ba.Lstr(resource='getTicketsWindow.unavailableText'), 367 color=(1, 0, 0)) 368 elif self._popup_type == 'menu': 369 if choice in ('mute', 'unmute'): 370 cfg = ba.app.config 371 cfg['Chat Muted'] = (choice == 'mute') 372 cfg.apply_and_commit() 373 self._update() 374 else: 375 print(f'unhandled popup type: {self._popup_type}') 376 377 def popup_menu_closing(self, popup_window: popup.PopupWindow) -> None: 378 """Called when the popup is closing.""" 379 380 def _on_party_member_press(self, client_id: int, is_host: bool, 381 widget: ba.Widget) -> None: 382 # if we're the host, pop up 'kick' options for all non-host members 383 if _ba.get_foreground_host_session() is not None: 384 kick_str = ba.Lstr(resource='kickText') 385 else: 386 # kick-votes appeared in build 14248 387 if (_ba.get_connection_to_host_info().get('build_number', 0) < 388 14248): 389 return 390 kick_str = ba.Lstr(resource='kickVoteText') 391 uiscale = ba.app.ui.uiscale 392 popup.PopupMenuWindow( 393 position=widget.get_screen_space_center(), 394 scale=(2.3 if uiscale is ba.UIScale.SMALL else 395 1.65 if uiscale is ba.UIScale.MEDIUM else 1.23), 396 choices=['kick'], 397 choices_display=[kick_str], 398 current_choice='kick', 399 delegate=self) 400 self._popup_type = 'partyMemberPress' 401 self._popup_party_member_client_id = client_id 402 self._popup_party_member_is_host = is_host 403 404 def _send_chat_message(self) -> None: 405 _ba.chatmessage(cast(str, ba.textwidget(query=self._text_field))) 406 ba.textwidget(edit=self._text_field, text='') 407 408 def close(self) -> None: 409 """Close the window.""" 410 ba.containerwidget(edit=self._root_widget, transition='out_scale') 411 412 def close_with_sound(self) -> None: 413 """Close the window and make a lovely sound.""" 414 ba.playsound(ba.getsound('swish')) 415 self.close()
Party list/chat window.
PartyWindow(origin: Sequence[float] = (0, 0))
25 def __init__(self, origin: Sequence[float] = (0, 0)): 26 _ba.set_party_window_open(True) 27 self._r = 'partyWindow' 28 self._popup_type: str | None = None 29 self._popup_party_member_client_id: int | None = None 30 self._popup_party_member_is_host: bool | None = None 31 self._width = 500 32 uiscale = ba.app.ui.uiscale 33 self._height = (365 if uiscale is ba.UIScale.SMALL else 34 480 if uiscale is ba.UIScale.MEDIUM else 600) 35 super().__init__(root_widget=ba.containerwidget( 36 size=(self._width, self._height), 37 transition='in_scale', 38 color=(0.40, 0.55, 0.20), 39 parent=_ba.get_special_widget('overlay_stack'), 40 on_outside_click_call=self.close_with_sound, 41 scale_origin_stack_offset=origin, 42 scale=(2.0 if uiscale is ba.UIScale.SMALL else 43 1.35 if uiscale is ba.UIScale.MEDIUM else 1.0), 44 stack_offset=(0, -10) if uiscale is ba.UIScale.SMALL else ( 45 240, 0) if uiscale is ba.UIScale.MEDIUM else (330, 20))) 46 47 self._cancel_button = ba.buttonwidget(parent=self._root_widget, 48 scale=0.7, 49 position=(30, self._height - 47), 50 size=(50, 50), 51 label='', 52 on_activate_call=self.close, 53 autoselect=True, 54 color=(0.45, 0.63, 0.15), 55 icon=ba.gettexture('crossOut'), 56 iconscale=1.2) 57 ba.containerwidget(edit=self._root_widget, 58 cancel_button=self._cancel_button) 59 60 self._menu_button = ba.buttonwidget( 61 parent=self._root_widget, 62 scale=0.7, 63 position=(self._width - 60, self._height - 47), 64 size=(50, 50), 65 label='...', 66 autoselect=True, 67 button_type='square', 68 on_activate_call=ba.WeakCall(self._on_menu_button_press), 69 color=(0.55, 0.73, 0.25), 70 iconscale=1.2) 71 72 info = _ba.get_connection_to_host_info() 73 if info.get('name', '') != '': 74 title = ba.Lstr(value=info['name']) 75 else: 76 title = ba.Lstr(resource=self._r + '.titleText') 77 78 self._title_text = ba.textwidget(parent=self._root_widget, 79 scale=0.9, 80 color=(0.5, 0.7, 0.5), 81 text=title, 82 size=(0, 0), 83 position=(self._width * 0.5, 84 self._height - 29), 85 maxwidth=self._width * 0.7, 86 h_align='center', 87 v_align='center') 88 89 self._empty_str = ba.textwidget(parent=self._root_widget, 90 scale=0.75, 91 size=(0, 0), 92 position=(self._width * 0.5, 93 self._height - 65), 94 maxwidth=self._width * 0.85, 95 h_align='center', 96 v_align='center') 97 98 self._scroll_width = self._width - 50 99 self._scrollwidget = ba.scrollwidget(parent=self._root_widget, 100 size=(self._scroll_width, 101 self._height - 200), 102 position=(30, 80), 103 color=(0.4, 0.6, 0.3)) 104 self._columnwidget = ba.columnwidget(parent=self._scrollwidget, 105 border=2, 106 margin=0) 107 ba.widget(edit=self._menu_button, down_widget=self._columnwidget) 108 109 self._muted_text = ba.textwidget( 110 parent=self._root_widget, 111 position=(self._width * 0.5, self._height * 0.5), 112 size=(0, 0), 113 h_align='center', 114 v_align='center', 115 text=ba.Lstr(resource='chatMutedText')) 116 self._chat_texts: list[ba.Widget] = [] 117 118 # add all existing messages if chat is not muted 119 if not ba.app.config.resolve('Chat Muted'): 120 msgs = _ba.get_chat_messages() 121 for msg in msgs: 122 self._add_msg(msg) 123 124 self._text_field = txt = ba.textwidget( 125 parent=self._root_widget, 126 editable=True, 127 size=(530, 40), 128 position=(44, 39), 129 text='', 130 maxwidth=494, 131 shadow=0.3, 132 flatness=1.0, 133 description=ba.Lstr(resource=self._r + '.chatMessageText'), 134 autoselect=True, 135 v_align='center', 136 corner_scale=0.7) 137 138 ba.widget(edit=self._scrollwidget, 139 autoselect=True, 140 left_widget=self._cancel_button, 141 up_widget=self._cancel_button, 142 down_widget=self._text_field) 143 ba.widget(edit=self._columnwidget, 144 autoselect=True, 145 up_widget=self._cancel_button, 146 down_widget=self._text_field) 147 ba.containerwidget(edit=self._root_widget, selected_child=txt) 148 btn = ba.buttonwidget(parent=self._root_widget, 149 size=(50, 35), 150 label=ba.Lstr(resource=self._r + '.sendText'), 151 button_type='square', 152 autoselect=True, 153 position=(self._width - 70, 35), 154 on_activate_call=self._send_chat_message) 155 ba.textwidget(edit=txt, on_return_press_call=btn.activate) 156 self._name_widgets: list[ba.Widget] = [] 157 self._roster: list[dict[str, Any]] | None = None 158 self._update_timer = ba.Timer(1.0, 159 ba.WeakCall(self._update), 160 repeat=True, 161 timetype=ba.TimeType.REAL) 162 self._update()
def
on_chat_message(self, msg: str) -> None:
164 def on_chat_message(self, msg: str) -> None: 165 """Called when a new chat message comes through.""" 166 if not ba.app.config.resolve('Chat Muted'): 167 self._add_msg(msg)
Called when a new chat message comes through.
def
close(self) -> None:
408 def close(self) -> None: 409 """Close the window.""" 410 ba.containerwidget(edit=self._root_widget, transition='out_scale')
Close the window.
def
close_with_sound(self) -> None:
412 def close_with_sound(self) -> None: 413 """Close the window and make a lovely sound.""" 414 ba.playsound(ba.getsound('swish')) 415 self.close()
Close the window and make a lovely sound.
Inherited Members
- ba.ui.Window
- get_root_widget