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 popup_menu_selected_choice(self, popup_window: bauiv1lib.popup.PopupMenuWindow, choice: str) -> None:
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}')

Called when a choice is selected in the popup.

def popup_menu_closing(self, popup_window: bauiv1lib.popup.PopupWindow) -> None:
474    def popup_menu_closing(self, popup_window: PopupWindow) -> None:
475        """Called when the popup is closing."""

Called when the popup is closing.

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