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