Пример #1
0
    def __init__(self,
                 transition: Optional[str] = 'in_right',
                 origin_widget: ba.Widget = None):
        # pylint: disable=too-many-locals
        # pylint: disable=too-many-statements
        from bastd.ui.tabs import TabRow
        ba.set_analytics_screen('Watch Window')
        scale_origin: Optional[Tuple[float, float]]
        if origin_widget is not None:
            self._transition_out = 'out_scale'
            scale_origin = origin_widget.get_screen_space_center()
            transition = 'in_scale'
        else:
            self._transition_out = 'out_right'
            scale_origin = None
        ba.app.ui.set_main_menu_location('Watch')
        self._tab_data: Dict[str, Any] = {}
        self._my_replays_scroll_width: Optional[float] = None
        self._my_replays_watch_replay_button: Optional[ba.Widget] = None
        self._scrollwidget: Optional[ba.Widget] = None
        self._columnwidget: Optional[ba.Widget] = None
        self._my_replay_selected: Optional[str] = None
        self._my_replays_rename_window: Optional[ba.Widget] = None
        self._my_replay_rename_text: Optional[ba.Widget] = None
        self._r = 'watchWindow'
        uiscale = ba.app.ui.uiscale
        self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040
        x_inset = 100 if uiscale is ba.UIScale.SMALL else 0
        self._height = (578 if uiscale is ba.UIScale.SMALL else
                        670 if uiscale is ba.UIScale.MEDIUM else 800)
        self._current_tab: Optional[WatchWindow.TabID] = None
        extra_top = 20 if uiscale is ba.UIScale.SMALL else 0

        super().__init__(root_widget=ba.containerwidget(
            size=(self._width, self._height + extra_top),
            transition=transition,
            toolbar_visibility='menu_minimal',
            scale_origin_stack_offset=scale_origin,
            scale=(1.3 if uiscale is ba.UIScale.SMALL else
                   0.97 if uiscale is ba.UIScale.MEDIUM else 0.8),
            stack_offset=(0, -10) if uiscale is ba.UIScale.SMALL else (
                0, 15) if uiscale is ba.UIScale.MEDIUM else (0, 0)))

        if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
            ba.containerwidget(edit=self._root_widget,
                               on_cancel_call=self._back)
            self._back_button = None
        else:
            self._back_button = btn = ba.buttonwidget(
                parent=self._root_widget,
                autoselect=True,
                position=(70 + x_inset, self._height - 74),
                size=(140, 60),
                scale=1.1,
                label=ba.Lstr(resource='backText'),
                button_type='back',
                on_activate_call=self._back)
            ba.containerwidget(edit=self._root_widget, cancel_button=btn)
            ba.buttonwidget(edit=btn,
                            button_type='backSmall',
                            size=(60, 60),
                            label=ba.charstr(ba.SpecialChar.BACK))

        ba.textwidget(parent=self._root_widget,
                      position=(self._width * 0.5, self._height - 38),
                      size=(0, 0),
                      color=ba.app.ui.title_color,
                      scale=1.5,
                      h_align='center',
                      v_align='center',
                      text=ba.Lstr(resource=self._r + '.titleText'),
                      maxwidth=400)

        tabdefs = [
            (self.TabID.MY_REPLAYS,
             ba.Lstr(resource=self._r + '.myReplaysText')),
            # (self.TabID.TEST_TAB, ba.Lstr(value='Testing')),
        ]

        scroll_buffer_h = 130 + 2 * x_inset
        tab_buffer_h = 750 + 2 * x_inset

        self._tab_row = TabRow(self._root_widget,
                               tabdefs,
                               pos=(tab_buffer_h * 0.5, self._height - 130),
                               size=(self._width - tab_buffer_h, 50),
                               on_select_call=self._set_tab)

        if ba.app.ui.use_toolbars:
            first_tab = self._tab_row.tabs[tabdefs[0][0]]
            last_tab = self._tab_row.tabs[tabdefs[-1][0]]
            ba.widget(edit=last_tab.button,
                      right_widget=_ba.get_special_widget('party_button'))
            if uiscale is ba.UIScale.SMALL:
                bbtn = _ba.get_special_widget('back_button')
                ba.widget(edit=first_tab.button,
                          up_widget=bbtn,
                          left_widget=bbtn)

        self._scroll_width = self._width - scroll_buffer_h
        self._scroll_height = self._height - 180

        # Not actually using a scroll widget anymore; just an image.
        scroll_left = (self._width - self._scroll_width) * 0.5
        scroll_bottom = self._height - self._scroll_height - 79 - 48
        buffer_h = 10
        buffer_v = 4
        ba.imagewidget(parent=self._root_widget,
                       position=(scroll_left - buffer_h,
                                 scroll_bottom - buffer_v),
                       size=(self._scroll_width + 2 * buffer_h,
                             self._scroll_height + 2 * buffer_v),
                       texture=ba.gettexture('scrollWidget'),
                       model_transparent=ba.getmodel('softEdgeOutside'))
        self._tab_container: Optional[ba.Widget] = None

        self._restore_state()
Пример #2
0
class WatchWindow(ba.Window):
    """Window for watching replays."""
    class TabID(Enum):
        """Our available tab types."""
        MY_REPLAYS = 'my_replays'
        TEST_TAB = 'test_tab'

    def __init__(self,
                 transition: Optional[str] = 'in_right',
                 origin_widget: ba.Widget = None):
        # pylint: disable=too-many-locals
        # pylint: disable=too-many-statements
        from bastd.ui.tabs import TabRow
        ba.set_analytics_screen('Watch Window')
        scale_origin: Optional[Tuple[float, float]]
        if origin_widget is not None:
            self._transition_out = 'out_scale'
            scale_origin = origin_widget.get_screen_space_center()
            transition = 'in_scale'
        else:
            self._transition_out = 'out_right'
            scale_origin = None
        ba.app.ui.set_main_menu_location('Watch')
        self._tab_data: Dict[str, Any] = {}
        self._my_replays_scroll_width: Optional[float] = None
        self._my_replays_watch_replay_button: Optional[ba.Widget] = None
        self._scrollwidget: Optional[ba.Widget] = None
        self._columnwidget: Optional[ba.Widget] = None
        self._my_replay_selected: Optional[str] = None
        self._my_replays_rename_window: Optional[ba.Widget] = None
        self._my_replay_rename_text: Optional[ba.Widget] = None
        self._r = 'watchWindow'
        uiscale = ba.app.ui.uiscale
        self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040
        x_inset = 100 if uiscale is ba.UIScale.SMALL else 0
        self._height = (578 if uiscale is ba.UIScale.SMALL else
                        670 if uiscale is ba.UIScale.MEDIUM else 800)
        self._current_tab: Optional[WatchWindow.TabID] = None
        extra_top = 20 if uiscale is ba.UIScale.SMALL else 0

        super().__init__(root_widget=ba.containerwidget(
            size=(self._width, self._height + extra_top),
            transition=transition,
            toolbar_visibility='menu_minimal',
            scale_origin_stack_offset=scale_origin,
            scale=(1.3 if uiscale is ba.UIScale.SMALL else
                   0.97 if uiscale is ba.UIScale.MEDIUM else 0.8),
            stack_offset=(0, -10) if uiscale is ba.UIScale.SMALL else (
                0, 15) if uiscale is ba.UIScale.MEDIUM else (0, 0)))

        if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
            ba.containerwidget(edit=self._root_widget,
                               on_cancel_call=self._back)
            self._back_button = None
        else:
            self._back_button = btn = ba.buttonwidget(
                parent=self._root_widget,
                autoselect=True,
                position=(70 + x_inset, self._height - 74),
                size=(140, 60),
                scale=1.1,
                label=ba.Lstr(resource='backText'),
                button_type='back',
                on_activate_call=self._back)
            ba.containerwidget(edit=self._root_widget, cancel_button=btn)
            ba.buttonwidget(edit=btn,
                            button_type='backSmall',
                            size=(60, 60),
                            label=ba.charstr(ba.SpecialChar.BACK))

        ba.textwidget(parent=self._root_widget,
                      position=(self._width * 0.5, self._height - 38),
                      size=(0, 0),
                      color=ba.app.ui.title_color,
                      scale=1.5,
                      h_align='center',
                      v_align='center',
                      text=ba.Lstr(resource=self._r + '.titleText'),
                      maxwidth=400)

        tabdefs = [
            (self.TabID.MY_REPLAYS,
             ba.Lstr(resource=self._r + '.myReplaysText')),
            # (self.TabID.TEST_TAB, ba.Lstr(value='Testing')),
        ]

        scroll_buffer_h = 130 + 2 * x_inset
        tab_buffer_h = 750 + 2 * x_inset

        self._tab_row = TabRow(self._root_widget,
                               tabdefs,
                               pos=(tab_buffer_h * 0.5, self._height - 130),
                               size=(self._width - tab_buffer_h, 50),
                               on_select_call=self._set_tab)

        if ba.app.ui.use_toolbars:
            first_tab = self._tab_row.tabs[tabdefs[0][0]]
            last_tab = self._tab_row.tabs[tabdefs[-1][0]]
            ba.widget(edit=last_tab.button,
                      right_widget=_ba.get_special_widget('party_button'))
            if uiscale is ba.UIScale.SMALL:
                bbtn = _ba.get_special_widget('back_button')
                ba.widget(edit=first_tab.button,
                          up_widget=bbtn,
                          left_widget=bbtn)

        self._scroll_width = self._width - scroll_buffer_h
        self._scroll_height = self._height - 180

        # Not actually using a scroll widget anymore; just an image.
        scroll_left = (self._width - self._scroll_width) * 0.5
        scroll_bottom = self._height - self._scroll_height - 79 - 48
        buffer_h = 10
        buffer_v = 4
        ba.imagewidget(parent=self._root_widget,
                       position=(scroll_left - buffer_h,
                                 scroll_bottom - buffer_v),
                       size=(self._scroll_width + 2 * buffer_h,
                             self._scroll_height + 2 * buffer_v),
                       texture=ba.gettexture('scrollWidget'),
                       model_transparent=ba.getmodel('softEdgeOutside'))
        self._tab_container: Optional[ba.Widget] = None

        self._restore_state()

    def _set_tab(self, tab_id: TabID) -> None:
        # pylint: disable=too-many-locals

        if self._current_tab == tab_id:
            return
        self._current_tab = tab_id

        # Preserve our current tab between runs.
        cfg = ba.app.config
        cfg['Watch Tab'] = tab_id.value
        cfg.commit()

        # Update tab colors based on which is selected.
        # tabs.update_tab_button_colors(self._tab_buttons, tab)
        self._tab_row.update_appearance(tab_id)

        if self._tab_container:
            self._tab_container.delete()
        scroll_left = (self._width - self._scroll_width) * 0.5
        scroll_bottom = self._height - self._scroll_height - 79 - 48

        # A place where tabs can store data to get cleared when
        # switching to a different tab
        self._tab_data = {}

        uiscale = ba.app.ui.uiscale
        if tab_id is self.TabID.MY_REPLAYS:
            c_width = self._scroll_width
            c_height = self._scroll_height - 20
            sub_scroll_height = c_height - 63
            self._my_replays_scroll_width = sub_scroll_width = (
                680 if uiscale is ba.UIScale.SMALL else 640)

            self._tab_container = cnt = ba.containerwidget(
                parent=self._root_widget,
                position=(scroll_left, scroll_bottom +
                          (self._scroll_height - c_height) * 0.5),
                size=(c_width, c_height),
                background=False,
                selection_loops_to_parent=True)

            v = c_height - 30
            ba.textwidget(parent=cnt,
                          position=(c_width * 0.5, v),
                          color=(0.6, 1.0, 0.6),
                          scale=0.7,
                          size=(0, 0),
                          maxwidth=c_width * 0.9,
                          h_align='center',
                          v_align='center',
                          text=ba.Lstr(
                              resource='replayRenameWarningText',
                              subs=[('${REPLAY}',
                                     ba.Lstr(resource='replayNameDefaultText'))
                                    ]))

            b_width = 140 if uiscale is ba.UIScale.SMALL else 178
            b_height = (107 if uiscale is ba.UIScale.SMALL else
                        142 if uiscale is ba.UIScale.MEDIUM else 190)
            b_space_extra = (0 if uiscale is ba.UIScale.SMALL else
                             -2 if uiscale is ba.UIScale.MEDIUM else -5)

            b_color = (0.6, 0.53, 0.63)
            b_textcolor = (0.75, 0.7, 0.8)
            btnv = (c_height - (48 if uiscale is ba.UIScale.SMALL else
                                45 if uiscale is ba.UIScale.MEDIUM else 40) -
                    b_height)
            btnh = 40 if uiscale is ba.UIScale.SMALL else 40
            smlh = 190 if uiscale is ba.UIScale.SMALL else 225
            tscl = 1.0 if uiscale is ba.UIScale.SMALL else 1.2
            self._my_replays_watch_replay_button = btn1 = ba.buttonwidget(
                parent=cnt,
                size=(b_width, b_height),
                position=(btnh, btnv),
                button_type='square',
                color=b_color,
                textcolor=b_textcolor,
                on_activate_call=self._on_my_replay_play_press,
                text_scale=tscl,
                label=ba.Lstr(resource=self._r + '.watchReplayButtonText'),
                autoselect=True)
            ba.widget(edit=btn1, up_widget=self._tab_row.tabs[tab_id].button)
            if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
                ba.widget(edit=btn1,
                          left_widget=_ba.get_special_widget('back_button'))
            btnv -= b_height + b_space_extra
            ba.buttonwidget(parent=cnt,
                            size=(b_width, b_height),
                            position=(btnh, btnv),
                            button_type='square',
                            color=b_color,
                            textcolor=b_textcolor,
                            on_activate_call=self._on_my_replay_rename_press,
                            text_scale=tscl,
                            label=ba.Lstr(resource=self._r +
                                          '.renameReplayButtonText'),
                            autoselect=True)
            btnv -= b_height + b_space_extra
            ba.buttonwidget(parent=cnt,
                            size=(b_width, b_height),
                            position=(btnh, btnv),
                            button_type='square',
                            color=b_color,
                            textcolor=b_textcolor,
                            on_activate_call=self._on_my_replay_delete_press,
                            text_scale=tscl,
                            label=ba.Lstr(resource=self._r +
                                          '.deleteReplayButtonText'),
                            autoselect=True)

            v -= sub_scroll_height + 23
            self._scrollwidget = scrlw = ba.scrollwidget(
                parent=cnt,
                position=(smlh, v),
                size=(sub_scroll_width, sub_scroll_height))
            ba.containerwidget(edit=cnt, selected_child=scrlw)
            self._columnwidget = ba.columnwidget(parent=scrlw,
                                                 left_border=10,
                                                 border=2,
                                                 margin=0)

            ba.widget(edit=scrlw,
                      autoselect=True,
                      left_widget=btn1,
                      up_widget=self._tab_row.tabs[tab_id].button)
            ba.widget(edit=self._tab_row.tabs[tab_id].button,
                      down_widget=scrlw)

            self._my_replay_selected = None
            self._refresh_my_replays()

    def _no_replay_selected_error(self) -> None:
        ba.screenmessage(ba.Lstr(resource=self._r +
                                 '.noReplaySelectedErrorText'),
                         color=(1, 0, 0))
        ba.playsound(ba.getsound('error'))

    def _on_my_replay_play_press(self) -> None:
        if self._my_replay_selected is None:
            self._no_replay_selected_error()
            return
        _ba.increment_analytics_count('Replay watch')

        def do_it() -> None:
            try:
                # Reset to normal speed.
                _ba.set_replay_speed_exponent(0)
                _ba.fade_screen(True)
                assert self._my_replay_selected is not None
                _ba.new_replay_session(_ba.get_replays_dir() + '/' +
                                       self._my_replay_selected)
            except Exception:
                ba.print_exception('Error running replay session.')

                # Drop back into a fresh main menu session
                # in case we half-launched or something.
                from bastd import mainmenu
                _ba.new_host_session(mainmenu.MainMenuSession)

        _ba.fade_screen(False, endcall=ba.Call(ba.pushcall, do_it))
        ba.containerwidget(edit=self._root_widget, transition='out_left')

    def _on_my_replay_rename_press(self) -> None:
        if self._my_replay_selected is None:
            self._no_replay_selected_error()
            return
        c_width = 600
        c_height = 250
        uiscale = ba.app.ui.uiscale
        self._my_replays_rename_window = cnt = ba.containerwidget(
            scale=(1.8 if uiscale is ba.UIScale.SMALL else
                   1.55 if uiscale is ba.UIScale.MEDIUM else 1.0),
            size=(c_width, c_height),
            transition='in_scale')
        dname = self._get_replay_display_name(self._my_replay_selected)
        ba.textwidget(parent=cnt,
                      size=(0, 0),
                      h_align='center',
                      v_align='center',
                      text=ba.Lstr(resource=self._r + '.renameReplayText',
                                   subs=[('${REPLAY}', dname)]),
                      maxwidth=c_width * 0.8,
                      position=(c_width * 0.5, c_height - 60))
        self._my_replay_rename_text = txt = ba.textwidget(
            parent=cnt,
            size=(c_width * 0.8, 40),
            h_align='left',
            v_align='center',
            text=dname,
            editable=True,
            description=ba.Lstr(resource=self._r + '.replayNameText'),
            position=(c_width * 0.1, c_height - 140),
            autoselect=True,
            maxwidth=c_width * 0.7,
            max_chars=200)
        cbtn = ba.buttonwidget(
            parent=cnt,
            label=ba.Lstr(resource='cancelText'),
            on_activate_call=ba.Call(
                lambda c: ba.containerwidget(edit=c, transition='out_scale'),
                cnt),
            size=(180, 60),
            position=(30, 30),
            autoselect=True)
        okb = ba.buttonwidget(parent=cnt,
                              label=ba.Lstr(resource=self._r + '.renameText'),
                              size=(180, 60),
                              position=(c_width - 230, 30),
                              on_activate_call=ba.Call(
                                  self._rename_my_replay,
                                  self._my_replay_selected),
                              autoselect=True)
        ba.widget(edit=cbtn, right_widget=okb)
        ba.widget(edit=okb, left_widget=cbtn)
        ba.textwidget(edit=txt, on_return_press_call=okb.activate)
        ba.containerwidget(edit=cnt, cancel_button=cbtn, start_button=okb)

    def _rename_my_replay(self, replay: str) -> None:
        new_name = None
        try:
            if not self._my_replay_rename_text:
                return
            new_name_raw = cast(
                str, ba.textwidget(query=self._my_replay_rename_text))
            new_name = new_name_raw + '.brp'

            # Ignore attempts to change it to what it already is
            # (or what it looks like to the user).
            if (replay != new_name
                    and self._get_replay_display_name(replay) != new_name_raw):
                old_name_full = (_ba.get_replays_dir() + '/' +
                                 replay).encode('utf-8')
                new_name_full = (_ba.get_replays_dir() + '/' +
                                 new_name).encode('utf-8')
                # False alarm; ba.textwidget can return non-None val.
                # pylint: disable=unsupported-membership-test
                if os.path.exists(new_name_full):
                    ba.playsound(ba.getsound('error'))
                    ba.screenmessage(
                        ba.Lstr(resource=self._r +
                                '.replayRenameErrorAlreadyExistsText'),
                        color=(1, 0, 0))
                elif any(char in new_name_raw for char in ['/', '\\', ':']):
                    ba.playsound(ba.getsound('error'))
                    ba.screenmessage(ba.Lstr(resource=self._r +
                                             '.replayRenameErrorInvalidName'),
                                     color=(1, 0, 0))
                else:
                    _ba.increment_analytics_count('Replay rename')
                    os.rename(old_name_full, new_name_full)
                    self._refresh_my_replays()
                    ba.playsound(ba.getsound('gunCocking'))
        except Exception:
            ba.print_exception(
                f"Error renaming replay '{replay}' to '{new_name}'.")
            ba.playsound(ba.getsound('error'))
            ba.screenmessage(
                ba.Lstr(resource=self._r + '.replayRenameErrorText'),
                color=(1, 0, 0),
            )

        ba.containerwidget(edit=self._my_replays_rename_window,
                           transition='out_scale')

    def _on_my_replay_delete_press(self) -> None:
        from bastd.ui import confirm
        if self._my_replay_selected is None:
            self._no_replay_selected_error()
            return
        confirm.ConfirmWindow(
            ba.Lstr(resource=self._r + '.deleteConfirmText',
                    subs=[('${REPLAY}',
                           self._get_replay_display_name(
                               self._my_replay_selected))]),
            ba.Call(self._delete_replay, self._my_replay_selected), 450, 150)

    def _get_replay_display_name(self, replay: str) -> str:
        if replay.endswith('.brp'):
            replay = replay[:-4]
        if replay == '__lastReplay':
            return ba.Lstr(resource='replayNameDefaultText').evaluate()
        return replay

    def _delete_replay(self, replay: str) -> None:
        try:
            _ba.increment_analytics_count('Replay delete')
            os.remove((_ba.get_replays_dir() + '/' + replay).encode('utf-8'))
            self._refresh_my_replays()
            ba.playsound(ba.getsound('shieldDown'))
            if replay == self._my_replay_selected:
                self._my_replay_selected = None
        except Exception:
            ba.print_exception(f"Error deleting replay '{replay}'.")
            ba.playsound(ba.getsound('error'))
            ba.screenmessage(
                ba.Lstr(resource=self._r + '.replayDeleteErrorText'),
                color=(1, 0, 0),
            )

    def _on_my_replay_select(self, replay: str) -> None:
        self._my_replay_selected = replay

    def _refresh_my_replays(self) -> None:
        assert self._columnwidget is not None
        for child in self._columnwidget.get_children():
            child.delete()
        t_scale = 1.6
        try:
            names = os.listdir(_ba.get_replays_dir())

            # Ignore random other files in there.
            names = [n for n in names if n.endswith('.brp')]
            names.sort(key=lambda x: x.lower())
        except Exception:
            ba.print_exception('Error listing replays dir.')
            names = []

        assert self._my_replays_scroll_width is not None
        assert self._my_replays_watch_replay_button is not None
        for i, name in enumerate(names):
            txt = ba.textwidget(
                parent=self._columnwidget,
                size=(self._my_replays_scroll_width / t_scale, 30),
                selectable=True,
                color=(1.0, 1, 0.4) if name == '__lastReplay.brp' else
                (1, 1, 1),
                always_highlight=True,
                on_select_call=ba.Call(self._on_my_replay_select, name),
                on_activate_call=self._my_replays_watch_replay_button.activate,
                text=self._get_replay_display_name(name),
                h_align='left',
                v_align='center',
                corner_scale=t_scale,
                maxwidth=(self._my_replays_scroll_width / t_scale) * 0.93)
            if i == 0:
                ba.widget(
                    edit=txt,
                    up_widget=self._tab_row.tabs[self.TabID.MY_REPLAYS].button)

    def _save_state(self) -> None:
        try:
            sel = self._root_widget.get_selected_child()
            selected_tab_ids = [
                tab_id for tab_id, tab in self._tab_row.tabs.items()
                if sel == tab.button
            ]
            if sel == self._back_button:
                sel_name = 'Back'
            elif selected_tab_ids:
                assert len(selected_tab_ids) == 1
                sel_name = f'Tab:{selected_tab_ids[0].value}'
            elif sel == self._tab_container:
                sel_name = 'TabContainer'
            else:
                raise ValueError(f'unrecognized selection {sel}')
            ba.app.ui.window_states[self.__class__.__name__] = {
                'sel_name': sel_name
            }
        except Exception:
            ba.print_exception(f'Error saving state for {self}.')

    def _restore_state(self) -> None:
        from efro.util import enum_by_value
        try:
            sel: Optional[ba.Widget]
            sel_name = ba.app.ui.window_states.get(self.__class__.__name__,
                                                   {}).get('sel_name')
            assert isinstance(sel_name, (str, type(None)))
            try:
                current_tab = enum_by_value(self.TabID,
                                            ba.app.config.get('Watch Tab'))
            except ValueError:
                current_tab = self.TabID.MY_REPLAYS
            self._set_tab(current_tab)

            if sel_name == 'Back':
                sel = self._back_button
            elif sel_name == 'TabContainer':
                sel = self._tab_container
            elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
                try:
                    sel_tab_id = enum_by_value(self.TabID,
                                               sel_name.split(':')[-1])
                except ValueError:
                    sel_tab_id = self.TabID.MY_REPLAYS
                sel = self._tab_row.tabs[sel_tab_id].button
            else:
                if self._tab_container is not None:
                    sel = self._tab_container
                else:
                    sel = self._tab_row.tabs[current_tab].button
            ba.containerwidget(edit=self._root_widget, selected_child=sel)
        except Exception:
            ba.print_exception(f'Error restoring state for {self}.')

    def _back(self) -> None:
        from bastd.ui.mainmenu import MainMenuWindow
        self._save_state()
        ba.containerwidget(edit=self._root_widget,
                           transition=self._transition_out)
        ba.app.ui.set_main_menu_window(
            MainMenuWindow(transition='in_left').get_root_widget())
Пример #3
0
    def __init__(self,
                 transition: str = 'in_right',
                 modal: bool = False,
                 show_tab: StoreBrowserWindow.TabID = None,
                 on_close_call: Callable[[], Any] = None,
                 back_location: str = None,
                 origin_widget: ba.Widget = None):
        # pylint: disable=too-many-statements
        # pylint: disable=too-many-locals
        from bastd.ui.tabs import TabRow
        from ba import SpecialChar

        app = ba.app
        uiscale = app.ui.uiscale

        ba.set_analytics_screen('Store Window')

        scale_origin: Optional[Tuple[float, float]]

        # If they provided an origin-widget, scale up from that.
        if origin_widget is not None:
            self._transition_out = 'out_scale'
            scale_origin = origin_widget.get_screen_space_center()
            transition = 'in_scale'
        else:
            self._transition_out = 'out_right'
            scale_origin = None

        self.button_infos: Optional[Dict[str, Dict[str, Any]]] = None
        self.update_buttons_timer: Optional[ba.Timer] = None
        self._status_textwidget_update_timer = None

        self._back_location = back_location
        self._on_close_call = on_close_call
        self._show_tab = show_tab
        self._modal = modal
        self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040
        self._x_inset = x_inset = 100 if uiscale is ba.UIScale.SMALL else 0
        self._height = (578 if uiscale is ba.UIScale.SMALL else
                        645 if uiscale is ba.UIScale.MEDIUM else 800)
        self._current_tab: Optional[StoreBrowserWindow.TabID] = None
        extra_top = 30 if uiscale is ba.UIScale.SMALL else 0

        self._request: Any = None
        self._r = 'store'
        self._last_buy_time: Optional[float] = None

        super().__init__(root_widget=ba.containerwidget(
            size=(self._width, self._height + extra_top),
            transition=transition,
            toolbar_visibility='menu_full',
            scale=(1.3 if uiscale is ba.UIScale.SMALL else
                   0.9 if uiscale is ba.UIScale.MEDIUM else 0.8),
            scale_origin_stack_offset=scale_origin,
            stack_offset=((0, -5) if uiscale is ba.UIScale.SMALL else (
                0, 0) if uiscale is ba.UIScale.MEDIUM else (0, 0))))

        self._back_button = btn = ba.buttonwidget(
            parent=self._root_widget,
            position=(70 + x_inset, self._height - 74),
            size=(140, 60),
            scale=1.1,
            autoselect=True,
            label=ba.Lstr(resource='doneText' if self._modal else 'backText'),
            button_type=None if self._modal else 'back',
            on_activate_call=self._back)
        ba.containerwidget(edit=self._root_widget, cancel_button=btn)

        self._ticket_count_text: Optional[ba.Widget] = None
        self._get_tickets_button: Optional[ba.Widget] = None

        if ba.app.allow_ticket_purchases:
            self._get_tickets_button = ba.buttonwidget(
                parent=self._root_widget,
                size=(210, 65),
                on_activate_call=self._on_get_more_tickets_press,
                autoselect=True,
                scale=0.9,
                text_scale=1.4,
                left_widget=self._back_button,
                color=(0.7, 0.5, 0.85),
                textcolor=(0.2, 1.0, 0.2),
                label=ba.Lstr(resource='getTicketsWindow.titleText'))
        else:
            self._ticket_count_text = ba.textwidget(parent=self._root_widget,
                                                    size=(210, 64),
                                                    color=(0.2, 1.0, 0.2),
                                                    h_align='center',
                                                    v_align='center')

        # Move this dynamically to keep it out of the way of the party icon.
        self._update_get_tickets_button_pos()
        self._get_ticket_pos_update_timer = ba.Timer(
            1.0,
            ba.WeakCall(self._update_get_tickets_button_pos),
            repeat=True,
            timetype=ba.TimeType.REAL)
        if self._get_tickets_button:
            ba.widget(edit=self._back_button,
                      right_widget=self._get_tickets_button)
        self._ticket_text_update_timer = ba.Timer(
            1.0,
            ba.WeakCall(self._update_tickets_text),
            timetype=ba.TimeType.REAL,
            repeat=True)
        self._update_tickets_text()

        app = ba.app
        if app.platform in ['mac', 'ios'] and app.subplatform == 'appstore':
            ba.buttonwidget(
                parent=self._root_widget,
                position=(self._width * 0.5 - 70, 16),
                size=(230, 50),
                scale=0.65,
                on_activate_call=ba.WeakCall(self._restore_purchases),
                color=(0.35, 0.3, 0.4),
                selectable=False,
                textcolor=(0.55, 0.5, 0.6),
                label=ba.Lstr(
                    resource='getTicketsWindow.restorePurchasesText'))

        ba.textwidget(parent=self._root_widget,
                      position=(self._width * 0.5, self._height - 44),
                      size=(0, 0),
                      color=app.ui.title_color,
                      scale=1.5,
                      h_align='center',
                      v_align='center',
                      text=ba.Lstr(resource='storeText'),
                      maxwidth=420)

        if not self._modal:
            ba.buttonwidget(edit=self._back_button,
                            button_type='backSmall',
                            size=(60, 60),
                            label=ba.charstr(SpecialChar.BACK))

        scroll_buffer_h = 130 + 2 * x_inset
        tab_buffer_h = 250 + 2 * x_inset

        tabs_def = [
            (self.TabID.EXTRAS, ba.Lstr(resource=self._r + '.extrasText')),
            (self.TabID.MAPS, ba.Lstr(resource=self._r + '.mapsText')),
            (self.TabID.MINIGAMES,
             ba.Lstr(resource=self._r + '.miniGamesText')),
            (self.TabID.CHARACTERS,
             ba.Lstr(resource=self._r + '.charactersText')),
            (self.TabID.ICONS, ba.Lstr(resource=self._r + '.iconsText')),
        ]

        self._tab_row = TabRow(self._root_widget,
                               tabs_def,
                               pos=(tab_buffer_h * 0.5, self._height - 130),
                               size=(self._width - tab_buffer_h, 50),
                               on_select_call=self._set_tab)

        self._purchasable_count_widgets: Dict[StoreBrowserWindow.TabID,
                                              Dict[str, Any]] = {}

        # Create our purchasable-items tags and have them update over time.
        for tab_id, tab in self._tab_row.tabs.items():
            pos = tab.position
            size = tab.size
            button = tab.button
            rad = 10
            center = (pos[0] + 0.1 * size[0], pos[1] + 0.9 * size[1])
            img = ba.imagewidget(parent=self._root_widget,
                                 position=(center[0] - rad * 1.04,
                                           center[1] - rad * 1.15),
                                 size=(rad * 2.2, rad * 2.2),
                                 texture=ba.gettexture('circleShadow'),
                                 color=(1, 0, 0))
            txt = ba.textwidget(parent=self._root_widget,
                                position=center,
                                size=(0, 0),
                                h_align='center',
                                v_align='center',
                                maxwidth=1.4 * rad,
                                scale=0.6,
                                shadow=1.0,
                                flatness=1.0)
            rad = 20
            sale_img = ba.imagewidget(parent=self._root_widget,
                                      position=(center[0] - rad,
                                                center[1] - rad),
                                      size=(rad * 2, rad * 2),
                                      draw_controller=button,
                                      texture=ba.gettexture('circleZigZag'),
                                      color=(0.5, 0, 1.0))
            sale_title_text = ba.textwidget(parent=self._root_widget,
                                            position=(center[0],
                                                      center[1] + 0.24 * rad),
                                            size=(0, 0),
                                            h_align='center',
                                            v_align='center',
                                            draw_controller=button,
                                            maxwidth=1.4 * rad,
                                            scale=0.6,
                                            shadow=0.0,
                                            flatness=1.0,
                                            color=(0, 1, 0))
            sale_time_text = ba.textwidget(parent=self._root_widget,
                                           position=(center[0],
                                                     center[1] - 0.29 * rad),
                                           size=(0, 0),
                                           h_align='center',
                                           v_align='center',
                                           draw_controller=button,
                                           maxwidth=1.4 * rad,
                                           scale=0.4,
                                           shadow=0.0,
                                           flatness=1.0,
                                           color=(0, 1, 0))
            self._purchasable_count_widgets[tab_id] = {
                'img': img,
                'text': txt,
                'sale_img': sale_img,
                'sale_title_text': sale_title_text,
                'sale_time_text': sale_time_text
            }
        self._tab_update_timer = ba.Timer(1.0,
                                          ba.WeakCall(self._update_tabs),
                                          timetype=ba.TimeType.REAL,
                                          repeat=True)
        self._update_tabs()

        if self._get_tickets_button:
            last_tab_button = self._tab_row.tabs[tabs_def[-1][0]].button
            ba.widget(edit=self._get_tickets_button,
                      down_widget=last_tab_button)
            ba.widget(edit=last_tab_button,
                      up_widget=self._get_tickets_button,
                      right_widget=self._get_tickets_button)

        self._scroll_width = self._width - scroll_buffer_h
        self._scroll_height = self._height - 180

        self._scrollwidget: Optional[ba.Widget] = None
        self._status_textwidget: Optional[ba.Widget] = None
        self._restore_state()
Пример #4
0
class StoreBrowserWindow(ba.Window):
    """Window for browsing the store."""

    class TabID(Enum):
        """Our available tab types."""
        EXTRAS = 'extras'
        MAPS = 'maps'
        MINIGAMES = 'minigames'
        CHARACTERS = 'characters'
        ICONS = 'icons'

    def __init__(self,
                 transition: str = 'in_right',
                 modal: bool = False,
                 show_tab: StoreBrowserWindow.TabID = None,
                 on_close_call: Callable[[], Any] = None,
                 back_location: str = None,
                 origin_widget: ba.Widget = None):
        # pylint: disable=too-many-statements
        # pylint: disable=too-many-locals
        from bastd.ui.tabs import TabRow
        from ba import SpecialChar

        app = ba.app
        uiscale = app.ui.uiscale

        ba.set_analytics_screen('Store Window')

        scale_origin: Optional[Tuple[float, float]]

        # If they provided an origin-widget, scale up from that.
        if origin_widget is not None:
            self._transition_out = 'out_scale'
            scale_origin = origin_widget.get_screen_space_center()
            transition = 'in_scale'
        else:
            self._transition_out = 'out_right'
            scale_origin = None

        self.button_infos: Optional[Dict[str, Dict[str, Any]]] = None
        self.update_buttons_timer: Optional[ba.Timer] = None
        self._status_textwidget_update_timer = None

        self._back_location = back_location
        self._on_close_call = on_close_call
        self._show_tab = show_tab
        self._modal = modal
        self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040
        self._x_inset = x_inset = 100 if uiscale is ba.UIScale.SMALL else 0
        self._height = (578 if uiscale is ba.UIScale.SMALL else
                        645 if uiscale is ba.UIScale.MEDIUM else 800)
        self._current_tab: Optional[StoreBrowserWindow.TabID] = None
        extra_top = 30 if uiscale is ba.UIScale.SMALL else 0

        self._request: Any = None
        self._r = 'store'
        self._last_buy_time: Optional[float] = None

        super().__init__(root_widget=ba.containerwidget(
            size=(self._width, self._height + extra_top),
            transition=transition,
            toolbar_visibility='menu_full',
            scale=(1.3 if uiscale is ba.UIScale.SMALL else
                   0.9 if uiscale is ba.UIScale.MEDIUM else 0.8),
            scale_origin_stack_offset=scale_origin,
            stack_offset=((0, -5) if uiscale is ba.UIScale.SMALL else (
                0, 0) if uiscale is ba.UIScale.MEDIUM else (0, 0))))

        self._back_button = btn = ba.buttonwidget(
            parent=self._root_widget,
            position=(70 + x_inset, self._height - 74),
            size=(140, 60),
            scale=1.1,
            autoselect=True,
            label=ba.Lstr(resource='doneText' if self._modal else 'backText'),
            button_type=None if self._modal else 'back',
            on_activate_call=self._back)
        ba.containerwidget(edit=self._root_widget, cancel_button=btn)

        self._ticket_count_text: Optional[ba.Widget] = None
        self._get_tickets_button: Optional[ba.Widget] = None

        if ba.app.allow_ticket_purchases:
            self._get_tickets_button = ba.buttonwidget(
                parent=self._root_widget,
                size=(210, 65),
                on_activate_call=self._on_get_more_tickets_press,
                autoselect=True,
                scale=0.9,
                text_scale=1.4,
                left_widget=self._back_button,
                color=(0.7, 0.5, 0.85),
                textcolor=(0.2, 1.0, 0.2),
                label=ba.Lstr(resource='getTicketsWindow.titleText'))
        else:
            self._ticket_count_text = ba.textwidget(parent=self._root_widget,
                                                    size=(210, 64),
                                                    color=(0.2, 1.0, 0.2),
                                                    h_align='center',
                                                    v_align='center')

        # Move this dynamically to keep it out of the way of the party icon.
        self._update_get_tickets_button_pos()
        self._get_ticket_pos_update_timer = ba.Timer(
            1.0,
            ba.WeakCall(self._update_get_tickets_button_pos),
            repeat=True,
            timetype=ba.TimeType.REAL)
        if self._get_tickets_button:
            ba.widget(edit=self._back_button,
                      right_widget=self._get_tickets_button)
        self._ticket_text_update_timer = ba.Timer(
            1.0,
            ba.WeakCall(self._update_tickets_text),
            timetype=ba.TimeType.REAL,
            repeat=True)
        self._update_tickets_text()

        app = ba.app
        if app.platform in ['mac', 'ios'] and app.subplatform == 'appstore':
            ba.buttonwidget(
                parent=self._root_widget,
                position=(self._width * 0.5 - 70, 16),
                size=(230, 50),
                scale=0.65,
                on_activate_call=ba.WeakCall(self._restore_purchases),
                color=(0.35, 0.3, 0.4),
                selectable=False,
                textcolor=(0.55, 0.5, 0.6),
                label=ba.Lstr(
                    resource='getTicketsWindow.restorePurchasesText'))

        ba.textwidget(parent=self._root_widget,
                      position=(self._width * 0.5, self._height - 44),
                      size=(0, 0),
                      color=app.ui.title_color,
                      scale=1.5,
                      h_align='center',
                      v_align='center',
                      text=ba.Lstr(resource='storeText'),
                      maxwidth=420)

        if not self._modal:
            ba.buttonwidget(edit=self._back_button,
                            button_type='backSmall',
                            size=(60, 60),
                            label=ba.charstr(SpecialChar.BACK))

        scroll_buffer_h = 130 + 2 * x_inset
        tab_buffer_h = 250 + 2 * x_inset

        tabs_def = [
            (self.TabID.EXTRAS, ba.Lstr(resource=self._r + '.extrasText')),
            (self.TabID.MAPS, ba.Lstr(resource=self._r + '.mapsText')),
            (self.TabID.MINIGAMES,
             ba.Lstr(resource=self._r + '.miniGamesText')),
            (self.TabID.CHARACTERS,
             ba.Lstr(resource=self._r + '.charactersText')),
            (self.TabID.ICONS, ba.Lstr(resource=self._r + '.iconsText')),
        ]

        self._tab_row = TabRow(self._root_widget,
                               tabs_def,
                               pos=(tab_buffer_h * 0.5, self._height - 130),
                               size=(self._width - tab_buffer_h, 50),
                               on_select_call=self._set_tab)

        self._purchasable_count_widgets: Dict[StoreBrowserWindow.TabID,
                                              Dict[str, Any]] = {}

        # Create our purchasable-items tags and have them update over time.
        for tab_id, tab in self._tab_row.tabs.items():
            pos = tab.position
            size = tab.size
            button = tab.button
            rad = 10
            center = (pos[0] + 0.1 * size[0], pos[1] + 0.9 * size[1])
            img = ba.imagewidget(parent=self._root_widget,
                                 position=(center[0] - rad * 1.04,
                                           center[1] - rad * 1.15),
                                 size=(rad * 2.2, rad * 2.2),
                                 texture=ba.gettexture('circleShadow'),
                                 color=(1, 0, 0))
            txt = ba.textwidget(parent=self._root_widget,
                                position=center,
                                size=(0, 0),
                                h_align='center',
                                v_align='center',
                                maxwidth=1.4 * rad,
                                scale=0.6,
                                shadow=1.0,
                                flatness=1.0)
            rad = 20
            sale_img = ba.imagewidget(parent=self._root_widget,
                                      position=(center[0] - rad,
                                                center[1] - rad),
                                      size=(rad * 2, rad * 2),
                                      draw_controller=button,
                                      texture=ba.gettexture('circleZigZag'),
                                      color=(0.5, 0, 1.0))
            sale_title_text = ba.textwidget(parent=self._root_widget,
                                            position=(center[0],
                                                      center[1] + 0.24 * rad),
                                            size=(0, 0),
                                            h_align='center',
                                            v_align='center',
                                            draw_controller=button,
                                            maxwidth=1.4 * rad,
                                            scale=0.6,
                                            shadow=0.0,
                                            flatness=1.0,
                                            color=(0, 1, 0))
            sale_time_text = ba.textwidget(parent=self._root_widget,
                                           position=(center[0],
                                                     center[1] - 0.29 * rad),
                                           size=(0, 0),
                                           h_align='center',
                                           v_align='center',
                                           draw_controller=button,
                                           maxwidth=1.4 * rad,
                                           scale=0.4,
                                           shadow=0.0,
                                           flatness=1.0,
                                           color=(0, 1, 0))
            self._purchasable_count_widgets[tab_id] = {
                'img': img,
                'text': txt,
                'sale_img': sale_img,
                'sale_title_text': sale_title_text,
                'sale_time_text': sale_time_text
            }
        self._tab_update_timer = ba.Timer(1.0,
                                          ba.WeakCall(self._update_tabs),
                                          timetype=ba.TimeType.REAL,
                                          repeat=True)
        self._update_tabs()

        if self._get_tickets_button:
            last_tab_button = self._tab_row.tabs[tabs_def[-1][0]].button
            ba.widget(edit=self._get_tickets_button,
                      down_widget=last_tab_button)
            ba.widget(edit=last_tab_button,
                      up_widget=self._get_tickets_button,
                      right_widget=self._get_tickets_button)

        self._scroll_width = self._width - scroll_buffer_h
        self._scroll_height = self._height - 180

        self._scrollwidget: Optional[ba.Widget] = None
        self._status_textwidget: Optional[ba.Widget] = None
        self._restore_state()

    def _update_get_tickets_button_pos(self) -> None:
        uiscale = ba.app.ui.uiscale
        pos = (self._width - 252 - (self._x_inset +
                                    (47 if uiscale is ba.UIScale.SMALL
                                     and _ba.is_party_icon_visible() else 0)),
               self._height - 70)
        if self._get_tickets_button:
            ba.buttonwidget(edit=self._get_tickets_button, position=pos)
        if self._ticket_count_text:
            ba.textwidget(edit=self._ticket_count_text, position=pos)

    def _restore_purchases(self) -> None:
        from bastd.ui import account
        if _ba.get_account_state() != 'signed_in':
            account.show_sign_in_prompt()
        else:
            _ba.restore_purchases()

    def _update_tabs(self) -> None:
        from ba.internal import (get_available_sale_time,
                                 get_available_purchase_count)
        if not self._root_widget:
            return
        for tab_id, tab_data in list(self._purchasable_count_widgets.items()):
            sale_time = get_available_sale_time(tab_id.value)

            if sale_time is not None:
                ba.textwidget(edit=tab_data['sale_title_text'],
                              text=ba.Lstr(resource='store.saleText'))
                ba.textwidget(edit=tab_data['sale_time_text'],
                              text=ba.timestring(
                                  sale_time,
                                  centi=False,
                                  timeformat=ba.TimeFormat.MILLISECONDS))
                ba.imagewidget(edit=tab_data['sale_img'], opacity=1.0)
                count = 0
            else:
                ba.textwidget(edit=tab_data['sale_title_text'], text='')
                ba.textwidget(edit=tab_data['sale_time_text'], text='')
                ba.imagewidget(edit=tab_data['sale_img'], opacity=0.0)
                count = get_available_purchase_count(tab_id.value)

            if count > 0:
                ba.textwidget(edit=tab_data['text'], text=str(count))
                ba.imagewidget(edit=tab_data['img'], opacity=1.0)
            else:
                ba.textwidget(edit=tab_data['text'], text='')
                ba.imagewidget(edit=tab_data['img'], opacity=0.0)

    def _update_tickets_text(self) -> None:
        from ba import SpecialChar
        if not self._root_widget:
            return
        sval: Union[str, ba.Lstr]
        if _ba.get_account_state() == 'signed_in':
            sval = ba.charstr(SpecialChar.TICKET) + str(
                _ba.get_account_ticket_count())
        else:
            sval = ba.Lstr(resource='getTicketsWindow.titleText')
        if self._get_tickets_button:
            ba.buttonwidget(edit=self._get_tickets_button, label=sval)
        if self._ticket_count_text:
            ba.textwidget(edit=self._ticket_count_text, text=sval)

    def _set_tab(self, tab_id: TabID) -> None:
        if self._current_tab is tab_id:
            return
        self._current_tab = tab_id

        # We wanna preserve our current tab between runs.
        cfg = ba.app.config
        cfg['Store Tab'] = tab_id.value
        cfg.commit()

        # Update tab colors based on which is selected.
        self._tab_row.update_appearance(tab_id)

        # (Re)create scroll widget.
        if self._scrollwidget:
            self._scrollwidget.delete()

        self._scrollwidget = ba.scrollwidget(
            parent=self._root_widget,
            highlight=False,
            position=((self._width - self._scroll_width) * 0.5,
                      self._height - self._scroll_height - 79 - 48),
            size=(self._scroll_width, self._scroll_height),
            claims_left_right=True,
            claims_tab=True,
            selection_loops_to_parent=True)

        # NOTE: this stuff is modified by the _Store class.
        # Should maybe clean that up.
        self.button_infos = {}
        self.update_buttons_timer = None

        # Show status over top.
        if self._status_textwidget:
            self._status_textwidget.delete()
        self._status_textwidget = ba.textwidget(
            parent=self._root_widget,
            position=(self._width * 0.5, self._height * 0.5),
            size=(0, 0),
            color=(1, 0.7, 1, 0.5),
            h_align='center',
            v_align='center',
            text=ba.Lstr(resource=self._r + '.loadingText'),
            maxwidth=self._scroll_width * 0.9)

        class _Request:

            def __init__(self, window: StoreBrowserWindow):
                self._window = weakref.ref(window)
                data = {'tab': tab_id.value}
                ba.timer(0.1,
                         ba.WeakCall(self._on_response, data),
                         timetype=ba.TimeType.REAL)

            def _on_response(self, data: Optional[Dict[str, Any]]) -> None:
                # FIXME: clean this up.
                # pylint: disable=protected-access
                window = self._window()
                if window is not None and (window._request is self):
                    window._request = None
                    # noinspection PyProtectedMember
                    window._on_response(data)

        # Kick off a server request.
        self._request = _Request(self)

    # Actually start the purchase locally.
    def _purchase_check_result(self, item: str, is_ticket_purchase: bool,
                               result: Optional[Dict[str, Any]]) -> None:
        if result is None:
            ba.playsound(ba.getsound('error'))
            ba.screenmessage(
                ba.Lstr(resource='internal.unavailableNoConnectionText'),
                color=(1, 0, 0))
        else:
            if is_ticket_purchase:
                if result['allow']:
                    price = _ba.get_account_misc_read_val(
                        'price.' + item, None)
                    if (price is None or not isinstance(price, int)
                            or price <= 0):
                        print('Error; got invalid local price of', price,
                              'for item', item)
                        ba.playsound(ba.getsound('error'))
                    else:
                        ba.playsound(ba.getsound('click01'))
                        _ba.in_game_purchase(item, price)
                else:
                    if result['reason'] == 'versionTooOld':
                        ba.playsound(ba.getsound('error'))
                        ba.screenmessage(ba.Lstr(
                            resource='getTicketsWindow.versionTooOldText'),
                                         color=(1, 0, 0))
                    else:
                        ba.playsound(ba.getsound('error'))
                        ba.screenmessage(ba.Lstr(
                            resource='getTicketsWindow.unavailableText'),
                                         color=(1, 0, 0))
            # Real in-app purchase.
            else:
                if result['allow']:
                    _ba.purchase(item)
                else:
                    if result['reason'] == 'versionTooOld':
                        ba.playsound(ba.getsound('error'))
                        ba.screenmessage(ba.Lstr(
                            resource='getTicketsWindow.versionTooOldText'),
                                         color=(1, 0, 0))
                    else:
                        ba.playsound(ba.getsound('error'))
                        ba.screenmessage(ba.Lstr(
                            resource='getTicketsWindow.unavailableText'),
                                         color=(1, 0, 0))

    def _do_purchase_check(self,
                           item: str,
                           is_ticket_purchase: bool = False) -> None:
        from ba.internal import master_server_get

        # Here we ping the server to ask if it's valid for us to
        # purchase this. Better to fail now than after we've
        # paid locally.
        app = ba.app
        master_server_get(
            'bsAccountPurchaseCheck',
            {
                'item': item,
                'platform': app.platform,
                'subplatform': app.subplatform,
                'version': app.version,
                'buildNumber': app.build_number,
                'purchaseType': 'ticket' if is_ticket_purchase else 'real'
            },
            callback=ba.WeakCall(self._purchase_check_result, item,
                                 is_ticket_purchase),
        )

    def buy(self, item: str) -> None:
        """Attempt to purchase the provided item."""
        from ba.internal import (get_available_sale_time,
                                 get_store_item_name_translated)
        from bastd.ui import account
        from bastd.ui.confirm import ConfirmWindow
        from bastd.ui import getcurrency

        # Prevent pressing buy within a few seconds of the last press
        # (gives the buttons time to disable themselves and whatnot).
        curtime = ba.time(ba.TimeType.REAL)
        if self._last_buy_time is not None and (curtime -
                                                self._last_buy_time) < 2.0:
            ba.playsound(ba.getsound('error'))
        else:
            if _ba.get_account_state() != 'signed_in':
                account.show_sign_in_prompt()
            else:
                self._last_buy_time = curtime

                # Pro is an actual IAP; the rest are ticket purchases.
                if item == 'pro':
                    ba.playsound(ba.getsound('click01'))

                    # Purchase either pro or pro_sale depending on whether
                    # there is a sale going on.
                    self._do_purchase_check('pro' if get_available_sale_time(
                        'extras') is None else 'pro_sale')
                else:
                    price = _ba.get_account_misc_read_val(
                        'price.' + item, None)
                    our_tickets = _ba.get_account_ticket_count()
                    if price is not None and our_tickets < price:
                        ba.playsound(ba.getsound('error'))
                        getcurrency.show_get_tickets_prompt()
                    else:

                        def do_it() -> None:
                            self._do_purchase_check(item,
                                                    is_ticket_purchase=True)

                        ba.playsound(ba.getsound('swish'))
                        ConfirmWindow(
                            ba.Lstr(resource='store.purchaseConfirmText',
                                    subs=[
                                        ('${ITEM}',
                                         get_store_item_name_translated(item))
                                    ]),
                            width=400,
                            height=120,
                            action=do_it,
                            ok_text=ba.Lstr(resource='store.purchaseText',
                                            fallback_resource='okText'))

    def _print_already_own(self, charname: str) -> None:
        ba.screenmessage(ba.Lstr(resource=self._r + '.alreadyOwnText',
                                 subs=[('${NAME}', charname)]),
                         color=(1, 0, 0))
        ba.playsound(ba.getsound('error'))

    def update_buttons(self) -> None:
        """Update our buttons."""
        # pylint: disable=too-many-statements
        # pylint: disable=too-many-branches
        # pylint: disable=too-many-locals
        from ba.internal import get_available_sale_time
        from ba import SpecialChar
        if not self._root_widget:
            return
        import datetime
        sales_raw = _ba.get_account_misc_read_val('sales', {})
        sales = {}
        try:
            # Look at the current set of sales; filter any with time remaining.
            for sale_item, sale_info in list(sales_raw.items()):
                to_end = (datetime.datetime.utcfromtimestamp(sale_info['e']) -
                          datetime.datetime.utcnow()).total_seconds()
                if to_end > 0:
                    sales[sale_item] = {
                        'to_end': to_end,
                        'original_price': sale_info['op']
                    }
        except Exception:
            ba.print_exception('Error parsing sales.')

        assert self.button_infos is not None
        for b_type, b_info in self.button_infos.items():

            if b_type in ['upgrades.pro', 'pro']:
                purchased = _ba.app.accounts.have_pro()
            else:
                purchased = _ba.get_purchased(b_type)

            sale_opacity = 0.0
            sale_title_text: Union[str, ba.Lstr] = ''
            sale_time_text: Union[str, ba.Lstr] = ''

            if purchased:
                title_color = (0.8, 0.7, 0.9, 1.0)
                color = (0.63, 0.55, 0.78)
                extra_image_opacity = 0.5
                call = ba.WeakCall(self._print_already_own, b_info['name'])
                price_text = ''
                price_text_left = ''
                price_text_right = ''
                show_purchase_check = True
                description_color: Sequence[float] = (0.4, 1.0, 0.4, 0.4)
                description_color2: Sequence[float] = (0.0, 0.0, 0.0, 0.0)
                price_color = (0.5, 1, 0.5, 0.3)
            else:
                title_color = (0.7, 0.9, 0.7, 1.0)
                color = (0.4, 0.8, 0.1)
                extra_image_opacity = 1.0
                call = b_info['call'] if 'call' in b_info else None
                if b_type in ['upgrades.pro', 'pro']:
                    sale_time = get_available_sale_time('extras')
                    if sale_time is not None:
                        priceraw = _ba.get_price('pro')
                        price_text_left = (priceraw
                                           if priceraw is not None else '?')
                        priceraw = _ba.get_price('pro_sale')
                        price_text_right = (priceraw
                                            if priceraw is not None else '?')
                        sale_opacity = 1.0
                        price_text = ''
                        sale_title_text = ba.Lstr(resource='store.saleText')
                        sale_time_text = ba.timestring(
                            sale_time,
                            centi=False,
                            timeformat=ba.TimeFormat.MILLISECONDS)
                    else:
                        priceraw = _ba.get_price('pro')
                        price_text = priceraw if priceraw is not None else '?'
                        price_text_left = ''
                        price_text_right = ''
                else:
                    price = _ba.get_account_misc_read_val('price.' + b_type, 0)

                    # Color the button differently if we cant afford this.
                    if _ba.get_account_state() == 'signed_in':
                        if _ba.get_account_ticket_count() < price:
                            color = (0.6, 0.61, 0.6)
                    price_text = ba.charstr(ba.SpecialChar.TICKET) + str(
                        _ba.get_account_misc_read_val('price.' + b_type, '?'))
                    price_text_left = ''
                    price_text_right = ''

                    # TESTING:
                    if b_type in sales:
                        sale_opacity = 1.0
                        price_text_left = ba.charstr(SpecialChar.TICKET) + str(
                            sales[b_type]['original_price'])
                        price_text_right = price_text
                        price_text = ''
                        sale_title_text = ba.Lstr(resource='store.saleText')
                        sale_time_text = ba.timestring(
                            int(sales[b_type]['to_end'] * 1000),
                            centi=False,
                            timeformat=ba.TimeFormat.MILLISECONDS)

                description_color = (0.5, 1.0, 0.5)
                description_color2 = (0.3, 1.0, 1.0)
                price_color = (0.2, 1, 0.2, 1.0)
                show_purchase_check = False

            if 'title_text' in b_info:
                ba.textwidget(edit=b_info['title_text'], color=title_color)
            if 'purchase_check' in b_info:
                ba.imagewidget(edit=b_info['purchase_check'],
                               opacity=1.0 if show_purchase_check else 0.0)
            if 'price_widget' in b_info:
                ba.textwidget(edit=b_info['price_widget'],
                              text=price_text,
                              color=price_color)
            if 'price_widget_left' in b_info:
                ba.textwidget(edit=b_info['price_widget_left'],
                              text=price_text_left)
            if 'price_widget_right' in b_info:
                ba.textwidget(edit=b_info['price_widget_right'],
                              text=price_text_right)
            if 'price_slash_widget' in b_info:
                ba.imagewidget(edit=b_info['price_slash_widget'],
                               opacity=sale_opacity)
            if 'sale_bg_widget' in b_info:
                ba.imagewidget(edit=b_info['sale_bg_widget'],
                               opacity=sale_opacity)
            if 'sale_title_widget' in b_info:
                ba.textwidget(edit=b_info['sale_title_widget'],
                              text=sale_title_text)
            if 'sale_time_widget' in b_info:
                ba.textwidget(edit=b_info['sale_time_widget'],
                              text=sale_time_text)
            if 'button' in b_info:
                ba.buttonwidget(edit=b_info['button'],
                                color=color,
                                on_activate_call=call)
            if 'extra_backings' in b_info:
                for bck in b_info['extra_backings']:
                    ba.imagewidget(edit=bck,
                                   color=color,
                                   opacity=extra_image_opacity)
            if 'extra_images' in b_info:
                for img in b_info['extra_images']:
                    ba.imagewidget(edit=img, opacity=extra_image_opacity)
            if 'extra_texts' in b_info:
                for etxt in b_info['extra_texts']:
                    ba.textwidget(edit=etxt, color=description_color)
            if 'extra_texts_2' in b_info:
                for etxt in b_info['extra_texts_2']:
                    ba.textwidget(edit=etxt, color=description_color2)
            if 'descriptionText' in b_info:
                ba.textwidget(edit=b_info['descriptionText'],
                              color=description_color)

    def _on_response(self, data: Optional[Dict[str, Any]]) -> None:
        # pylint: disable=too-many-statements

        # clear status text..
        if self._status_textwidget:
            self._status_textwidget.delete()
            self._status_textwidget_update_timer = None

        if data is None:
            self._status_textwidget = ba.textwidget(
                parent=self._root_widget,
                position=(self._width * 0.5, self._height * 0.5),
                size=(0, 0),
                scale=1.3,
                transition_delay=0.1,
                color=(1, 0.3, 0.3, 1.0),
                h_align='center',
                v_align='center',
                text=ba.Lstr(resource=self._r + '.loadErrorText'),
                maxwidth=self._scroll_width * 0.9)
        else:

            class _Store:

                def __init__(self, store_window: StoreBrowserWindow,
                             sdata: Dict[str, Any], width: float):
                    from ba.internal import (get_store_item_display_size,
                                             get_store_layout)
                    self._store_window = store_window
                    self._width = width
                    store_data = get_store_layout()
                    self._tab = sdata['tab']
                    self._sections = copy.deepcopy(store_data[sdata['tab']])
                    self._height: Optional[float] = None

                    uiscale = ba.app.ui.uiscale

                    # Pre-calc a few things and add them to store-data.
                    for section in self._sections:
                        if self._tab == 'characters':
                            dummy_name = 'characters.foo'
                        elif self._tab == 'extras':
                            dummy_name = 'pro'
                        elif self._tab == 'maps':
                            dummy_name = 'maps.foo'
                        elif self._tab == 'icons':
                            dummy_name = 'icons.foo'
                        else:
                            dummy_name = ''
                        section['button_size'] = get_store_item_display_size(
                            dummy_name)
                        section['v_spacing'] = (-17 if self._tab
                                                == 'characters' else 0)
                        if 'title' not in section:
                            section['title'] = ''
                        section['x_offs'] = (130 if self._tab == 'extras' else
                                             270 if self._tab == 'maps' else 0)
                        section['y_offs'] = (
                            55 if (self._tab == 'extras'
                                   and uiscale is ba.UIScale.SMALL) else
                            -20 if self._tab == 'icons' else 0)

                def instantiate(self, scrollwidget: ba.Widget,
                                tab_button: ba.Widget) -> None:
                    """Create the store."""
                    # pylint: disable=too-many-locals
                    # pylint: disable=too-many-branches
                    # pylint: disable=too-many-nested-blocks
                    from bastd.ui.store import item as storeitemui
                    title_spacing = 40
                    button_border = 20
                    button_spacing = 4
                    boffs_h = 40
                    self._height = 80.0

                    # Calc total height.
                    for i, section in enumerate(self._sections):
                        if section['title'] != '':
                            assert self._height is not None
                            self._height += title_spacing
                        b_width, b_height = section['button_size']
                        b_column_count = int(
                            math.floor((self._width - boffs_h - 20) /
                                       (b_width + button_spacing)))
                        b_row_count = int(
                            math.ceil(
                                float(len(section['items'])) / b_column_count))
                        b_height_total = (
                            2 * button_border + b_row_count * b_height +
                            (b_row_count - 1) * section['v_spacing'])
                        self._height += b_height_total

                    assert self._height is not None
                    cnt2 = ba.containerwidget(parent=scrollwidget,
                                              scale=1.0,
                                              size=(self._width, self._height),
                                              background=False,
                                              claims_left_right=True,
                                              claims_tab=True,
                                              selection_loops_to_parent=True)
                    v = self._height - 20

                    if self._tab == 'characters':
                        txt = ba.Lstr(
                            resource='store.howToSwitchCharactersText',
                            subs=[
                                ('${SETTINGS}',
                                 ba.Lstr(
                                     resource='accountSettingsWindow.titleText'
                                 )),
                                ('${PLAYER_PROFILES}',
                                 ba.Lstr(
                                     resource='playerProfilesWindow.titleText')
                                 )
                            ])
                        ba.textwidget(parent=cnt2,
                                      text=txt,
                                      size=(0, 0),
                                      position=(self._width * 0.5,
                                                self._height - 28),
                                      h_align='center',
                                      v_align='center',
                                      color=(0.7, 1, 0.7, 0.4),
                                      scale=0.7,
                                      shadow=0,
                                      flatness=1.0,
                                      maxwidth=700,
                                      transition_delay=0.4)
                    elif self._tab == 'icons':
                        txt = ba.Lstr(
                            resource='store.howToUseIconsText',
                            subs=[
                                ('${SETTINGS}',
                                 ba.Lstr(resource='mainMenu.settingsText')),
                                ('${PLAYER_PROFILES}',
                                 ba.Lstr(
                                     resource='playerProfilesWindow.titleText')
                                 )
                            ])
                        ba.textwidget(parent=cnt2,
                                      text=txt,
                                      size=(0, 0),
                                      position=(self._width * 0.5,
                                                self._height - 28),
                                      h_align='center',
                                      v_align='center',
                                      color=(0.7, 1, 0.7, 0.4),
                                      scale=0.7,
                                      shadow=0,
                                      flatness=1.0,
                                      maxwidth=700,
                                      transition_delay=0.4)
                    elif self._tab == 'maps':
                        assert self._width is not None
                        assert self._height is not None
                        txt = ba.Lstr(resource='store.howToUseMapsText')
                        ba.textwidget(parent=cnt2,
                                      text=txt,
                                      size=(0, 0),
                                      position=(self._width * 0.5,
                                                self._height - 28),
                                      h_align='center',
                                      v_align='center',
                                      color=(0.7, 1, 0.7, 0.4),
                                      scale=0.7,
                                      shadow=0,
                                      flatness=1.0,
                                      maxwidth=700,
                                      transition_delay=0.4)

                    prev_row_buttons: Optional[List] = None
                    this_row_buttons = []

                    delay = 0.3
                    for section in self._sections:
                        if section['title'] != '':
                            ba.textwidget(
                                parent=cnt2,
                                position=(60, v - title_spacing * 0.8),
                                size=(0, 0),
                                scale=1.0,
                                transition_delay=delay,
                                color=(0.7, 0.9, 0.7, 1),
                                h_align='left',
                                v_align='center',
                                text=ba.Lstr(resource=section['title']),
                                maxwidth=self._width * 0.7)
                            v -= title_spacing
                        delay = max(0.100, delay - 0.100)
                        v -= button_border
                        b_width, b_height = section['button_size']
                        b_count = len(section['items'])
                        b_column_count = int(
                            math.floor((self._width - boffs_h - 20) /
                                       (b_width + button_spacing)))
                        col = 0
                        item: Dict[str, Any]
                        assert self._store_window.button_infos is not None
                        for i, item_name in enumerate(section['items']):
                            item = self._store_window.button_infos[
                                item_name] = {}
                            item['call'] = ba.WeakCall(self._store_window.buy,
                                                       item_name)
                            if 'x_offs' in section:
                                boffs_h2 = section['x_offs']
                            else:
                                boffs_h2 = 0

                            if 'y_offs' in section:
                                boffs_v2 = section['y_offs']
                            else:
                                boffs_v2 = 0
                            b_pos = (boffs_h + boffs_h2 +
                                     (b_width + button_spacing) * col,
                                     v - b_height + boffs_v2)
                            storeitemui.instantiate_store_item_display(
                                item_name,
                                item,
                                parent_widget=cnt2,
                                b_pos=b_pos,
                                boffs_h=boffs_h,
                                b_width=b_width,
                                b_height=b_height,
                                boffs_h2=boffs_h2,
                                boffs_v2=boffs_v2,
                                delay=delay)
                            btn = item['button']
                            delay = max(0.1, delay - 0.1)
                            this_row_buttons.append(btn)

                            # Wire this button to the equivalent in the
                            # previous row.
                            if prev_row_buttons is not None:
                                # pylint: disable=unsubscriptable-object
                                if len(prev_row_buttons) > col:
                                    ba.widget(edit=btn,
                                              up_widget=prev_row_buttons[col])
                                    ba.widget(edit=prev_row_buttons[col],
                                              down_widget=btn)

                                    # If we're the last button in our row,
                                    # wire any in the previous row past
                                    # our position to go to us if down is
                                    # pressed.
                                    if (col + 1 == b_column_count
                                            or i == b_count - 1):
                                        for b_prev in prev_row_buttons[col +
                                                                       1:]:
                                            ba.widget(edit=b_prev,
                                                      down_widget=btn)
                                else:
                                    ba.widget(edit=btn,
                                              up_widget=prev_row_buttons[-1])
                            else:
                                ba.widget(edit=btn, up_widget=tab_button)

                            col += 1
                            if col == b_column_count or i == b_count - 1:
                                prev_row_buttons = this_row_buttons
                                this_row_buttons = []
                                col = 0
                                v -= b_height
                                if i < b_count - 1:
                                    v -= section['v_spacing']

                        v -= button_border

                    # Set a timer to update these buttons periodically as long
                    # as we're alive (so if we buy one it will grey out, etc).
                    self._store_window.update_buttons_timer = ba.Timer(
                        0.5,
                        ba.WeakCall(self._store_window.update_buttons),
                        repeat=True,
                        timetype=ba.TimeType.REAL)

                    # Also update them immediately.
                    self._store_window.update_buttons()

            if self._current_tab in (self.TabID.EXTRAS, self.TabID.MINIGAMES,
                                     self.TabID.CHARACTERS, self.TabID.MAPS,
                                     self.TabID.ICONS):
                store = _Store(self, data, self._scroll_width)
                assert self._scrollwidget is not None
                store.instantiate(
                    scrollwidget=self._scrollwidget,
                    tab_button=self._tab_row.tabs[self._current_tab].button)
            else:
                cnt = ba.containerwidget(parent=self._scrollwidget,
                                         scale=1.0,
                                         size=(self._scroll_width,
                                               self._scroll_height * 0.95),
                                         background=False,
                                         claims_left_right=True,
                                         claims_tab=True,
                                         selection_loops_to_parent=True)
                self._status_textwidget = ba.textwidget(
                    parent=cnt,
                    position=(self._scroll_width * 0.5,
                              self._scroll_height * 0.5),
                    size=(0, 0),
                    scale=1.3,
                    transition_delay=0.1,
                    color=(1, 1, 0.3, 1.0),
                    h_align='center',
                    v_align='center',
                    text=ba.Lstr(resource=self._r + '.comingSoonText'),
                    maxwidth=self._scroll_width * 0.9)

    def _save_state(self) -> None:
        try:
            sel = self._root_widget.get_selected_child()
            selected_tab_ids = [
                tab_id for tab_id, tab in self._tab_row.tabs.items()
                if sel == tab.button
            ]
            if sel == self._get_tickets_button:
                sel_name = 'GetTickets'
            elif sel == self._scrollwidget:
                sel_name = 'Scroll'
            elif sel == self._back_button:
                sel_name = 'Back'
            elif selected_tab_ids:
                assert len(selected_tab_ids) == 1
                sel_name = f'Tab:{selected_tab_ids[0].value}'
            else:
                raise ValueError(f'unrecognized selection \'{sel}\'')
            ba.app.ui.window_states[self.__class__.__name__] = {
                'sel_name': sel_name,
            }
        except Exception:
            ba.print_exception(f'Error saving state for {self}.')

    def _restore_state(self) -> None:
        try:
            sel: Optional[ba.Widget]
            sel_name = ba.app.ui.window_states.get(self.__class__.__name__,
                                                   {}).get('sel_name')
            assert isinstance(sel_name, (str, type(None)))

            try:
                current_tab = ba.enum_by_value(self.TabID,
                                               ba.app.config.get('Store Tab'))
            except ValueError:
                current_tab = self.TabID.CHARACTERS

            if self._show_tab is not None:
                current_tab = self._show_tab
            if sel_name == 'GetTickets' and self._get_tickets_button:
                sel = self._get_tickets_button
            elif sel_name == 'Back':
                sel = self._back_button
            elif sel_name == 'Scroll':
                sel = self._scrollwidget
            elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
                try:
                    sel_tab_id = ba.enum_by_value(self.TabID,
                                                  sel_name.split(':')[-1])
                except ValueError:
                    sel_tab_id = self.TabID.CHARACTERS
                sel = self._tab_row.tabs[sel_tab_id].button
            else:
                sel = self._tab_row.tabs[current_tab].button

            # If we were requested to show a tab, select it too..
            if (self._show_tab is not None
                    and self._show_tab in self._tab_row.tabs):
                sel = self._tab_row.tabs[self._show_tab].button
            self._set_tab(current_tab)
            if sel is not None:
                ba.containerwidget(edit=self._root_widget, selected_child=sel)
        except Exception:
            ba.print_exception(f'Error restoring state for {self}.')

    def _on_get_more_tickets_press(self) -> None:
        # pylint: disable=cyclic-import
        from bastd.ui.account import show_sign_in_prompt
        from bastd.ui.getcurrency import GetCurrencyWindow
        if _ba.get_account_state() != 'signed_in':
            show_sign_in_prompt()
            return
        self._save_state()
        ba.containerwidget(edit=self._root_widget, transition='out_left')
        window = GetCurrencyWindow(
            from_modal_store=self._modal,
            store_back_location=self._back_location).get_root_widget()
        if not self._modal:
            ba.app.ui.set_main_menu_window(window)

    def _back(self) -> None:
        # pylint: disable=cyclic-import
        from bastd.ui.coop.browser import CoopBrowserWindow
        from bastd.ui.mainmenu import MainMenuWindow
        self._save_state()
        ba.containerwidget(edit=self._root_widget,
                           transition=self._transition_out)
        if not self._modal:
            if self._back_location == 'CoopBrowserWindow':
                ba.app.ui.set_main_menu_window(
                    CoopBrowserWindow(transition='in_left').get_root_widget())
            else:
                ba.app.ui.set_main_menu_window(
                    MainMenuWindow(transition='in_left').get_root_widget())
        if self._on_close_call is not None:
            self._on_close_call()
Пример #5
0
    def __init__(self,
                 transition: Optional[str] = 'in_right',
                 origin_widget: ba.Widget = None):
        # pylint: disable=too-many-statements
        # pylint: disable=too-many-locals
        # pylint: disable=cyclic-import
        from bastd.ui.gather.abouttab import AboutGatherTab
        from bastd.ui.gather.manualtab import ManualGatherTab
        from bastd.ui.gather.privatetab import PrivateGatherTab
        from bastd.ui.gather.publictab import PublicGatherTab
        from bastd.ui.gather.nearbytab import NearbyGatherTab

        ba.set_analytics_screen('Gather Window')
        scale_origin: Optional[tuple[float, float]]
        if origin_widget is not None:
            self._transition_out = 'out_scale'
            scale_origin = origin_widget.get_screen_space_center()
            transition = 'in_scale'
        else:
            self._transition_out = 'out_right'
            scale_origin = None
        ba.app.ui.set_main_menu_location('Gather')
        _ba.set_party_icon_always_visible(True)
        uiscale = ba.app.ui.uiscale
        self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040
        x_offs = 100 if uiscale is ba.UIScale.SMALL else 0
        self._height = (582 if uiscale is ba.UIScale.SMALL else
                        680 if uiscale is ba.UIScale.MEDIUM else 800)
        self._current_tab: Optional[GatherWindow.TabID] = None
        extra_top = 20 if uiscale is ba.UIScale.SMALL else 0
        self._r = 'gatherWindow'

        super().__init__(root_widget=ba.containerwidget(
            size=(self._width, self._height + extra_top),
            transition=transition,
            toolbar_visibility='menu_minimal',
            scale_origin_stack_offset=scale_origin,
            scale=(1.3 if uiscale is ba.UIScale.SMALL else
                   0.97 if uiscale is ba.UIScale.MEDIUM else 0.8),
            stack_offset=(0, -11) if uiscale is ba.UIScale.SMALL else (
                0, 0) if uiscale is ba.UIScale.MEDIUM else (0, 0)))

        if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
            ba.containerwidget(edit=self._root_widget,
                               on_cancel_call=self._back)
            self._back_button = None
        else:
            self._back_button = btn = ba.buttonwidget(
                parent=self._root_widget,
                position=(70 + x_offs, self._height - 74),
                size=(140, 60),
                scale=1.1,
                autoselect=True,
                label=ba.Lstr(resource='backText'),
                button_type='back',
                on_activate_call=self._back)
            ba.containerwidget(edit=self._root_widget, cancel_button=btn)
            ba.buttonwidget(edit=btn,
                            button_type='backSmall',
                            position=(70 + x_offs, self._height - 78),
                            size=(60, 60),
                            label=ba.charstr(ba.SpecialChar.BACK))

        condensed = uiscale is not ba.UIScale.LARGE
        t_offs_y = (0 if not condensed else
                    25 if uiscale is ba.UIScale.MEDIUM else 17)
        ba.textwidget(parent=self._root_widget,
                      position=(self._width * 0.5,
                                self._height - 42 + t_offs_y),
                      size=(0, 0),
                      color=ba.app.ui.title_color,
                      scale=(1.5 if not condensed else
                             1.0 if uiscale is ba.UIScale.MEDIUM else 0.6),
                      h_align='center',
                      v_align='center',
                      text=ba.Lstr(resource=self._r + '.titleText'),
                      maxwidth=550)

        scroll_buffer_h = 130 + 2 * x_offs
        tab_buffer_h = ((320 if condensed else 250) + 2 * x_offs)

        # Build up the set of tabs we want.
        tabdefs: list[tuple[GatherWindow.TabID, ba.Lstr]] = [
            (self.TabID.ABOUT, ba.Lstr(resource=self._r + '.aboutText'))
        ]
        if _ba.get_account_misc_read_val('enablePublicParties', True):
            tabdefs.append((self.TabID.INTERNET,
                            ba.Lstr(resource=self._r + '.publicText')))
        tabdefs.append(
            (self.TabID.PRIVATE, ba.Lstr(resource=self._r + '.privateText')))
        tabdefs.append(
            (self.TabID.NEARBY, ba.Lstr(resource=self._r + '.nearbyText')))
        tabdefs.append(
            (self.TabID.MANUAL, ba.Lstr(resource=self._r + '.manualText')))

        # On small UI, push our tabs up closer to the top of the screen to
        # save a bit of space.
        tabs_top_extra = 42 if condensed else 0
        self._tab_row = TabRow(self._root_widget,
                               tabdefs,
                               pos=(tab_buffer_h * 0.5,
                                    self._height - 130 + tabs_top_extra),
                               size=(self._width - tab_buffer_h, 50),
                               on_select_call=ba.WeakCall(self._set_tab))

        # Now instantiate handlers for these tabs.
        tabtypes: dict[GatherWindow.TabID, type[GatherTab]] = {
            self.TabID.ABOUT: AboutGatherTab,
            self.TabID.MANUAL: ManualGatherTab,
            self.TabID.PRIVATE: PrivateGatherTab,
            self.TabID.INTERNET: PublicGatherTab,
            self.TabID.NEARBY: NearbyGatherTab
        }
        self._tabs: dict[GatherWindow.TabID, GatherTab] = {}
        for tab_id in self._tab_row.tabs:
            tabtype = tabtypes.get(tab_id)
            if tabtype is not None:
                self._tabs[tab_id] = tabtype(self)

        if ba.app.ui.use_toolbars:
            ba.widget(edit=self._tab_row.tabs[tabdefs[-1][0]].button,
                      right_widget=_ba.get_special_widget('party_button'))
            if uiscale is ba.UIScale.SMALL:
                ba.widget(edit=self._tab_row.tabs[tabdefs[0][0]].button,
                          left_widget=_ba.get_special_widget('back_button'))

        self._scroll_width = self._width - scroll_buffer_h
        self._scroll_height = self._height - 180.0 + tabs_top_extra

        self._scroll_left = (self._width - self._scroll_width) * 0.5
        self._scroll_bottom = (self._height - self._scroll_height - 79 - 48 +
                               tabs_top_extra)
        buffer_h = 10
        buffer_v = 4

        # Not actually using a scroll widget anymore; just an image.
        ba.imagewidget(parent=self._root_widget,
                       position=(self._scroll_left - buffer_h,
                                 self._scroll_bottom - buffer_v),
                       size=(self._scroll_width + 2 * buffer_h,
                             self._scroll_height + 2 * buffer_v),
                       texture=ba.gettexture('scrollWidget'),
                       model_transparent=ba.getmodel('softEdgeOutside'))
        self._tab_container: Optional[ba.Widget] = None

        self._restore_state()
Пример #6
0
class GatherWindow(ba.Window):
    """Window for joining/inviting friends."""

    class TabID(Enum):
        """Our available tab types."""
        ABOUT = 'about'
        INTERNET = 'internet'
        PRIVATE = 'private'
        NEARBY = 'nearby'
        MANUAL = 'manual'

    def __init__(self,
                 transition: Optional[str] = 'in_right',
                 origin_widget: ba.Widget = None):
        # pylint: disable=too-many-statements
        # pylint: disable=too-many-locals
        # pylint: disable=cyclic-import
        from bastd.ui.gather.abouttab import AboutGatherTab
        from bastd.ui.gather.manualtab import ManualGatherTab
        from bastd.ui.gather.privatetab import PrivateGatherTab
        from bastd.ui.gather.publictab import PublicGatherTab
        from bastd.ui.gather.nearbytab import NearbyGatherTab

        ba.set_analytics_screen('Gather Window')
        scale_origin: Optional[tuple[float, float]]
        if origin_widget is not None:
            self._transition_out = 'out_scale'
            scale_origin = origin_widget.get_screen_space_center()
            transition = 'in_scale'
        else:
            self._transition_out = 'out_right'
            scale_origin = None
        ba.app.ui.set_main_menu_location('Gather')
        _ba.set_party_icon_always_visible(True)
        uiscale = ba.app.ui.uiscale
        self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040
        x_offs = 100 if uiscale is ba.UIScale.SMALL else 0
        self._height = (582 if uiscale is ba.UIScale.SMALL else
                        680 if uiscale is ba.UIScale.MEDIUM else 800)
        self._current_tab: Optional[GatherWindow.TabID] = None
        extra_top = 20 if uiscale is ba.UIScale.SMALL else 0
        self._r = 'gatherWindow'

        super().__init__(root_widget=ba.containerwidget(
            size=(self._width, self._height + extra_top),
            transition=transition,
            toolbar_visibility='menu_minimal',
            scale_origin_stack_offset=scale_origin,
            scale=(1.3 if uiscale is ba.UIScale.SMALL else
                   0.97 if uiscale is ba.UIScale.MEDIUM else 0.8),
            stack_offset=(0, -11) if uiscale is ba.UIScale.SMALL else (
                0, 0) if uiscale is ba.UIScale.MEDIUM else (0, 0)))

        if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
            ba.containerwidget(edit=self._root_widget,
                               on_cancel_call=self._back)
            self._back_button = None
        else:
            self._back_button = btn = ba.buttonwidget(
                parent=self._root_widget,
                position=(70 + x_offs, self._height - 74),
                size=(140, 60),
                scale=1.1,
                autoselect=True,
                label=ba.Lstr(resource='backText'),
                button_type='back',
                on_activate_call=self._back)
            ba.containerwidget(edit=self._root_widget, cancel_button=btn)
            ba.buttonwidget(edit=btn,
                            button_type='backSmall',
                            position=(70 + x_offs, self._height - 78),
                            size=(60, 60),
                            label=ba.charstr(ba.SpecialChar.BACK))

        condensed = uiscale is not ba.UIScale.LARGE
        t_offs_y = (0 if not condensed else
                    25 if uiscale is ba.UIScale.MEDIUM else 17)
        ba.textwidget(parent=self._root_widget,
                      position=(self._width * 0.5,
                                self._height - 42 + t_offs_y),
                      size=(0, 0),
                      color=ba.app.ui.title_color,
                      scale=(1.5 if not condensed else
                             1.0 if uiscale is ba.UIScale.MEDIUM else 0.6),
                      h_align='center',
                      v_align='center',
                      text=ba.Lstr(resource=self._r + '.titleText'),
                      maxwidth=550)

        scroll_buffer_h = 130 + 2 * x_offs
        tab_buffer_h = ((320 if condensed else 250) + 2 * x_offs)

        # Build up the set of tabs we want.
        tabdefs: list[tuple[GatherWindow.TabID, ba.Lstr]] = [
            (self.TabID.ABOUT, ba.Lstr(resource=self._r + '.aboutText'))
        ]
        if _ba.get_account_misc_read_val('enablePublicParties', True):
            tabdefs.append((self.TabID.INTERNET,
                            ba.Lstr(resource=self._r + '.publicText')))
        tabdefs.append(
            (self.TabID.PRIVATE, ba.Lstr(resource=self._r + '.privateText')))
        tabdefs.append(
            (self.TabID.NEARBY, ba.Lstr(resource=self._r + '.nearbyText')))
        tabdefs.append(
            (self.TabID.MANUAL, ba.Lstr(resource=self._r + '.manualText')))

        # On small UI, push our tabs up closer to the top of the screen to
        # save a bit of space.
        tabs_top_extra = 42 if condensed else 0
        self._tab_row = TabRow(self._root_widget,
                               tabdefs,
                               pos=(tab_buffer_h * 0.5,
                                    self._height - 130 + tabs_top_extra),
                               size=(self._width - tab_buffer_h, 50),
                               on_select_call=ba.WeakCall(self._set_tab))

        # Now instantiate handlers for these tabs.
        tabtypes: dict[GatherWindow.TabID, type[GatherTab]] = {
            self.TabID.ABOUT: AboutGatherTab,
            self.TabID.MANUAL: ManualGatherTab,
            self.TabID.PRIVATE: PrivateGatherTab,
            self.TabID.INTERNET: PublicGatherTab,
            self.TabID.NEARBY: NearbyGatherTab
        }
        self._tabs: dict[GatherWindow.TabID, GatherTab] = {}
        for tab_id in self._tab_row.tabs:
            tabtype = tabtypes.get(tab_id)
            if tabtype is not None:
                self._tabs[tab_id] = tabtype(self)

        if ba.app.ui.use_toolbars:
            ba.widget(edit=self._tab_row.tabs[tabdefs[-1][0]].button,
                      right_widget=_ba.get_special_widget('party_button'))
            if uiscale is ba.UIScale.SMALL:
                ba.widget(edit=self._tab_row.tabs[tabdefs[0][0]].button,
                          left_widget=_ba.get_special_widget('back_button'))

        self._scroll_width = self._width - scroll_buffer_h
        self._scroll_height = self._height - 180.0 + tabs_top_extra

        self._scroll_left = (self._width - self._scroll_width) * 0.5
        self._scroll_bottom = (self._height - self._scroll_height - 79 - 48 +
                               tabs_top_extra)
        buffer_h = 10
        buffer_v = 4

        # Not actually using a scroll widget anymore; just an image.
        ba.imagewidget(parent=self._root_widget,
                       position=(self._scroll_left - buffer_h,
                                 self._scroll_bottom - buffer_v),
                       size=(self._scroll_width + 2 * buffer_h,
                             self._scroll_height + 2 * buffer_v),
                       texture=ba.gettexture('scrollWidget'),
                       model_transparent=ba.getmodel('softEdgeOutside'))
        self._tab_container: Optional[ba.Widget] = None

        self._restore_state()

    def __del__(self) -> None:
        _ba.set_party_icon_always_visible(False)

    def playlist_select(self, origin_widget: ba.Widget) -> None:
        """Called by the private-hosting tab to select a playlist."""
        from bastd.ui.play import PlayWindow
        self._save_state()
        ba.containerwidget(edit=self._root_widget, transition='out_left')
        ba.app.ui.selecting_private_party_playlist = True
        ba.app.ui.set_main_menu_window(
            PlayWindow(origin_widget=origin_widget).get_root_widget())

    def _set_tab(self, tab_id: TabID) -> None:
        if self._current_tab is tab_id:
            return
        prev_tab_id = self._current_tab
        self._current_tab = tab_id

        # We wanna preserve our current tab between runs.
        cfg = ba.app.config
        cfg['Gather Tab'] = tab_id.value
        cfg.commit()

        # Update tab colors based on which is selected.
        self._tab_row.update_appearance(tab_id)

        if prev_tab_id is not None:
            prev_tab = self._tabs.get(prev_tab_id)
            if prev_tab is not None:
                prev_tab.on_deactivate()

        # Clear up prev container if it hasn't been done.
        if self._tab_container:
            self._tab_container.delete()

        tab = self._tabs.get(tab_id)
        if tab is not None:
            self._tab_container = tab.on_activate(
                self._root_widget,
                self._tab_row.tabs[tab_id].button,
                self._scroll_width,
                self._scroll_height,
                self._scroll_left,
                self._scroll_bottom,
            )
            return

    def _save_state(self) -> None:
        try:
            for tab in self._tabs.values():
                tab.save_state()

            sel = self._root_widget.get_selected_child()
            selected_tab_ids = [
                tab_id for tab_id, tab in self._tab_row.tabs.items()
                if sel == tab.button
            ]
            if sel == self._back_button:
                sel_name = 'Back'
            elif selected_tab_ids:
                assert len(selected_tab_ids) == 1
                sel_name = f'Tab:{selected_tab_ids[0].value}'
            elif sel == self._tab_container:
                sel_name = 'TabContainer'
            else:
                raise ValueError(f'unrecognized selection: \'{sel}\'')
            ba.app.ui.window_states[type(self)] = {
                'sel_name': sel_name,
            }
        except Exception:
            ba.print_exception(f'Error saving state for {self}.')

    def _restore_state(self) -> None:
        from efro.util import enum_by_value
        try:
            for tab in self._tabs.values():
                tab.restore_state()

            sel: Optional[ba.Widget]
            winstate = ba.app.ui.window_states.get(type(self), {})
            sel_name = winstate.get('sel_name', None)
            assert isinstance(sel_name, (str, type(None)))
            current_tab = self.TabID.ABOUT
            gather_tab_val = ba.app.config.get('Gather Tab')
            try:
                stored_tab = enum_by_value(self.TabID, gather_tab_val)
                if stored_tab in self._tab_row.tabs:
                    current_tab = stored_tab
            except ValueError:
                pass
            self._set_tab(current_tab)
            if sel_name == 'Back':
                sel = self._back_button
            elif sel_name == 'TabContainer':
                sel = self._tab_container
            elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
                try:
                    sel_tab_id = enum_by_value(self.TabID,
                                               sel_name.split(':')[-1])
                except ValueError:
                    sel_tab_id = self.TabID.ABOUT
                sel = self._tab_row.tabs[sel_tab_id].button
            else:
                sel = self._tab_row.tabs[current_tab].button
            ba.containerwidget(edit=self._root_widget, selected_child=sel)
        except Exception:
            ba.print_exception('Error restoring gather-win state.')

    def _back(self) -> None:
        from bastd.ui.mainmenu import MainMenuWindow
        self._save_state()
        ba.containerwidget(edit=self._root_widget,
                           transition=self._transition_out)
        ba.app.ui.set_main_menu_window(
            MainMenuWindow(transition='in_left').get_root_widget())