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