bastd.ui.party

Provides party related UI.

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

Party list/chat window.

PartyWindow(origin: Sequence[float] = (0, 0))
 25    def __init__(self, origin: Sequence[float] = (0, 0)):
 26        _ba.set_party_window_open(True)
 27        self._r = 'partyWindow'
 28        self._popup_type: str | None = None
 29        self._popup_party_member_client_id: int | None = None
 30        self._popup_party_member_is_host: bool | None = None
 31        self._width = 500
 32        uiscale = ba.app.ui.uiscale
 33        self._height = (365 if uiscale is ba.UIScale.SMALL else
 34                        480 if uiscale is ba.UIScale.MEDIUM else 600)
 35        super().__init__(root_widget=ba.containerwidget(
 36            size=(self._width, self._height),
 37            transition='in_scale',
 38            color=(0.40, 0.55, 0.20),
 39            parent=_ba.get_special_widget('overlay_stack'),
 40            on_outside_click_call=self.close_with_sound,
 41            scale_origin_stack_offset=origin,
 42            scale=(2.0 if uiscale is ba.UIScale.SMALL else
 43                   1.35 if uiscale is ba.UIScale.MEDIUM else 1.0),
 44            stack_offset=(0, -10) if uiscale is ba.UIScale.SMALL else (
 45                240, 0) if uiscale is ba.UIScale.MEDIUM else (330, 20)))
 46
 47        self._cancel_button = ba.buttonwidget(parent=self._root_widget,
 48                                              scale=0.7,
 49                                              position=(30, self._height - 47),
 50                                              size=(50, 50),
 51                                              label='',
 52                                              on_activate_call=self.close,
 53                                              autoselect=True,
 54                                              color=(0.45, 0.63, 0.15),
 55                                              icon=ba.gettexture('crossOut'),
 56                                              iconscale=1.2)
 57        ba.containerwidget(edit=self._root_widget,
 58                           cancel_button=self._cancel_button)
 59
 60        self._menu_button = ba.buttonwidget(
 61            parent=self._root_widget,
 62            scale=0.7,
 63            position=(self._width - 60, self._height - 47),
 64            size=(50, 50),
 65            label='...',
 66            autoselect=True,
 67            button_type='square',
 68            on_activate_call=ba.WeakCall(self._on_menu_button_press),
 69            color=(0.55, 0.73, 0.25),
 70            iconscale=1.2)
 71
 72        info = _ba.get_connection_to_host_info()
 73        if info.get('name', '') != '':
 74            title = ba.Lstr(value=info['name'])
 75        else:
 76            title = ba.Lstr(resource=self._r + '.titleText')
 77
 78        self._title_text = ba.textwidget(parent=self._root_widget,
 79                                         scale=0.9,
 80                                         color=(0.5, 0.7, 0.5),
 81                                         text=title,
 82                                         size=(0, 0),
 83                                         position=(self._width * 0.5,
 84                                                   self._height - 29),
 85                                         maxwidth=self._width * 0.7,
 86                                         h_align='center',
 87                                         v_align='center')
 88
 89        self._empty_str = ba.textwidget(parent=self._root_widget,
 90                                        scale=0.75,
 91                                        size=(0, 0),
 92                                        position=(self._width * 0.5,
 93                                                  self._height - 65),
 94                                        maxwidth=self._width * 0.85,
 95                                        h_align='center',
 96                                        v_align='center')
 97
 98        self._scroll_width = self._width - 50
 99        self._scrollwidget = ba.scrollwidget(parent=self._root_widget,
100                                             size=(self._scroll_width,
101                                                   self._height - 200),
102                                             position=(30, 80),
103                                             color=(0.4, 0.6, 0.3))
104        self._columnwidget = ba.columnwidget(parent=self._scrollwidget,
105                                             border=2,
106                                             margin=0)
107        ba.widget(edit=self._menu_button, down_widget=self._columnwidget)
108
109        self._muted_text = ba.textwidget(
110            parent=self._root_widget,
111            position=(self._width * 0.5, self._height * 0.5),
112            size=(0, 0),
113            h_align='center',
114            v_align='center',
115            text=ba.Lstr(resource='chatMutedText'))
116        self._chat_texts: list[ba.Widget] = []
117
118        # add all existing messages if chat is not muted
119        if not ba.app.config.resolve('Chat Muted'):
120            msgs = _ba.get_chat_messages()
121            for msg in msgs:
122                self._add_msg(msg)
123
124        self._text_field = txt = ba.textwidget(
125            parent=self._root_widget,
126            editable=True,
127            size=(530, 40),
128            position=(44, 39),
129            text='',
130            maxwidth=494,
131            shadow=0.3,
132            flatness=1.0,
133            description=ba.Lstr(resource=self._r + '.chatMessageText'),
134            autoselect=True,
135            v_align='center',
136            corner_scale=0.7)
137
138        ba.widget(edit=self._scrollwidget,
139                  autoselect=True,
140                  left_widget=self._cancel_button,
141                  up_widget=self._cancel_button,
142                  down_widget=self._text_field)
143        ba.widget(edit=self._columnwidget,
144                  autoselect=True,
145                  up_widget=self._cancel_button,
146                  down_widget=self._text_field)
147        ba.containerwidget(edit=self._root_widget, selected_child=txt)
148        btn = ba.buttonwidget(parent=self._root_widget,
149                              size=(50, 35),
150                              label=ba.Lstr(resource=self._r + '.sendText'),
151                              button_type='square',
152                              autoselect=True,
153                              position=(self._width - 70, 35),
154                              on_activate_call=self._send_chat_message)
155        ba.textwidget(edit=txt, on_return_press_call=btn.activate)
156        self._name_widgets: list[ba.Widget] = []
157        self._roster: list[dict[str, Any]] | None = None
158        self._update_timer = ba.Timer(1.0,
159                                      ba.WeakCall(self._update),
160                                      repeat=True,
161                                      timetype=ba.TimeType.REAL)
162        self._update()
def on_chat_message(self, msg: str) -> None:
164    def on_chat_message(self, msg: str) -> None:
165        """Called when a new chat message comes through."""
166        if not ba.app.config.resolve('Chat Muted'):
167            self._add_msg(msg)

Called when a new chat message comes through.

def popup_menu_selected_choice(self, popup_window: bastd.ui.popup.PopupMenuWindow, choice: str) -> None:
347    def popup_menu_selected_choice(self, popup_window: popup.PopupMenuWindow,
348                                   choice: str) -> None:
349        """Called when a choice is selected in the popup."""
350        del popup_window  # unused
351        if self._popup_type == 'partyMemberPress':
352            if self._popup_party_member_is_host:
353                ba.playsound(ba.getsound('error'))
354                ba.screenmessage(
355                    ba.Lstr(resource='internal.cantKickHostError'),
356                    color=(1, 0, 0))
357            else:
358                assert self._popup_party_member_client_id is not None
359
360                # Ban for 5 minutes.
361                result = _ba.disconnect_client(
362                    self._popup_party_member_client_id, ban_time=5 * 60)
363                if not result:
364                    ba.playsound(ba.getsound('error'))
365                    ba.screenmessage(
366                        ba.Lstr(resource='getTicketsWindow.unavailableText'),
367                        color=(1, 0, 0))
368        elif self._popup_type == 'menu':
369            if choice in ('mute', 'unmute'):
370                cfg = ba.app.config
371                cfg['Chat Muted'] = (choice == 'mute')
372                cfg.apply_and_commit()
373                self._update()
374        else:
375            print(f'unhandled popup type: {self._popup_type}')

Called when a choice is selected in the popup.

def popup_menu_closing(self, popup_window: bastd.ui.popup.PopupWindow) -> None:
377    def popup_menu_closing(self, popup_window: popup.PopupWindow) -> None:
378        """Called when the popup is closing."""

Called when the popup is closing.

def close(self) -> None:
408    def close(self) -> None:
409        """Close the window."""
410        ba.containerwidget(edit=self._root_widget, transition='out_scale')

Close the window.

def close_with_sound(self) -> None:
412    def close_with_sound(self) -> None:
413        """Close the window and make a lovely sound."""
414        ba.playsound(ba.getsound('swish'))
415        self.close()

Close the window and make a lovely sound.

Inherited Members
ba.ui.Window
get_root_widget