bauiv1lib.playlist.mapselect
Provides UI for selecting maps in playlists.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Provides UI for selecting maps in playlists.""" 4 5from __future__ import annotations 6 7import math 8from typing import TYPE_CHECKING 9 10import bauiv1 as bui 11 12if TYPE_CHECKING: 13 from typing import Any, Callable 14 15 import bascenev1 as bs 16 17 18class PlaylistMapSelectWindow(bui.Window): 19 """Window to select a map.""" 20 21 def __init__( 22 self, 23 gametype: type[bs.GameActivity], 24 sessiontype: type[bs.Session], 25 config: dict[str, Any], 26 edit_info: dict[str, Any], 27 completion_call: Callable[[dict[str, Any] | None], Any], 28 transition: str = 'in_right', 29 ): 30 from bascenev1 import get_filtered_map_name 31 32 self._gametype = gametype 33 self._sessiontype = sessiontype 34 self._config = config 35 self._completion_call = completion_call 36 self._edit_info = edit_info 37 self._maps: list[tuple[str, bui.Texture]] = [] 38 try: 39 self._previous_map = get_filtered_map_name( 40 config['settings']['map'] 41 ) 42 except Exception: 43 self._previous_map = '' 44 45 assert bui.app.classic is not None 46 uiscale = bui.app.ui_v1.uiscale 47 width = 815 if uiscale is bui.UIScale.SMALL else 615 48 x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 49 height = ( 50 400 51 if uiscale is bui.UIScale.SMALL 52 else 480 if uiscale is bui.UIScale.MEDIUM else 600 53 ) 54 55 top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 56 super().__init__( 57 root_widget=bui.containerwidget( 58 size=(width, height + top_extra), 59 transition=transition, 60 scale=( 61 2.17 62 if uiscale is bui.UIScale.SMALL 63 else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0 64 ), 65 stack_offset=( 66 (0, -27) if uiscale is bui.UIScale.SMALL else (0, 0) 67 ), 68 ) 69 ) 70 71 self._cancel_button = btn = bui.buttonwidget( 72 parent=self._root_widget, 73 position=(38 + x_inset, height - 67), 74 size=(140, 50), 75 scale=0.9, 76 text_scale=1.0, 77 autoselect=True, 78 label=bui.Lstr(resource='cancelText'), 79 on_activate_call=self._cancel, 80 ) 81 82 bui.containerwidget(edit=self._root_widget, cancel_button=btn) 83 bui.textwidget( 84 parent=self._root_widget, 85 position=(width * 0.5, height - 46), 86 size=(0, 0), 87 maxwidth=260, 88 scale=1.1, 89 text=bui.Lstr( 90 resource='mapSelectTitleText', 91 subs=[('${GAME}', self._gametype.get_display_string())], 92 ), 93 color=bui.app.ui_v1.title_color, 94 h_align='center', 95 v_align='center', 96 ) 97 v = height - 70 98 self._scroll_width = width - (80 + 2 * x_inset) 99 self._scroll_height = height - 140 100 101 self._scrollwidget = bui.scrollwidget( 102 parent=self._root_widget, 103 position=(40 + x_inset, v - self._scroll_height), 104 size=(self._scroll_width, self._scroll_height), 105 ) 106 bui.containerwidget( 107 edit=self._root_widget, selected_child=self._scrollwidget 108 ) 109 bui.containerwidget(edit=self._scrollwidget, claims_left_right=True) 110 111 self._subcontainer: bui.Widget | None = None 112 self._refresh() 113 114 def _refresh(self, select_get_more_maps_button: bool = False) -> None: 115 # pylint: disable=too-many-statements 116 # pylint: disable=too-many-branches 117 # pylint: disable=too-many-locals 118 from bascenev1 import ( 119 get_map_class, 120 get_map_display_string, 121 ) 122 123 assert bui.app.classic is not None 124 store = bui.app.classic.store 125 # Kill old. 126 if self._subcontainer is not None: 127 self._subcontainer.delete() 128 129 mesh_opaque = bui.getmesh('level_select_button_opaque') 130 mesh_transparent = bui.getmesh('level_select_button_transparent') 131 132 self._maps = [] 133 map_list = self._gametype.get_supported_maps(self._sessiontype) 134 map_list_sorted = list(map_list) 135 map_list_sorted.sort() 136 unowned_maps = store.get_unowned_maps() 137 138 for mapname in map_list_sorted: 139 # Disallow ones we don't own. 140 if mapname in unowned_maps: 141 continue 142 map_tex_name = get_map_class(mapname).get_preview_texture_name() 143 if map_tex_name is not None: 144 try: 145 map_tex = bui.gettexture(map_tex_name) 146 self._maps.append((mapname, map_tex)) 147 except Exception: 148 print(f'Invalid map preview texture: "{map_tex_name}".') 149 else: 150 print('Error: no map preview texture for map:', mapname) 151 152 count = len(self._maps) 153 columns = 2 154 rows = int(math.ceil(float(count) / columns)) 155 button_width = 220 156 button_height = button_width * 0.5 157 button_buffer_h = 16 158 button_buffer_v = 19 159 self._sub_width = self._scroll_width * 0.95 160 self._sub_height = ( 161 5 + rows * (button_height + 2 * button_buffer_v) + 100 162 ) 163 self._subcontainer = bui.containerwidget( 164 parent=self._scrollwidget, 165 size=(self._sub_width, self._sub_height), 166 background=False, 167 ) 168 index = 0 169 mask_texture = bui.gettexture('mapPreviewMask') 170 h_offs = 130 if len(self._maps) == 1 else 0 171 for y in range(rows): 172 for x in range(columns): 173 pos = ( 174 x * (button_width + 2 * button_buffer_h) 175 + button_buffer_h 176 + h_offs, 177 self._sub_height 178 - (y + 1) * (button_height + 2 * button_buffer_v) 179 + 12, 180 ) 181 btn = bui.buttonwidget( 182 parent=self._subcontainer, 183 button_type='square', 184 size=(button_width, button_height), 185 autoselect=True, 186 texture=self._maps[index][1], 187 mask_texture=mask_texture, 188 mesh_opaque=mesh_opaque, 189 mesh_transparent=mesh_transparent, 190 label='', 191 color=(1, 1, 1), 192 on_activate_call=bui.Call( 193 self._select_with_delay, self._maps[index][0] 194 ), 195 position=pos, 196 ) 197 if x == 0: 198 bui.widget(edit=btn, left_widget=self._cancel_button) 199 if y == 0: 200 bui.widget(edit=btn, up_widget=self._cancel_button) 201 if x == columns - 1 and bui.app.ui_v1.use_toolbars: 202 bui.widget( 203 edit=btn, 204 right_widget=bui.get_special_widget('party_button'), 205 ) 206 207 bui.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60) 208 if self._maps[index][0] == self._previous_map: 209 bui.containerwidget( 210 edit=self._subcontainer, 211 selected_child=btn, 212 visible_child=btn, 213 ) 214 name = get_map_display_string(self._maps[index][0]) 215 bui.textwidget( 216 parent=self._subcontainer, 217 text=name, 218 position=(pos[0] + button_width * 0.5, pos[1] - 12), 219 size=(0, 0), 220 scale=0.5, 221 maxwidth=button_width, 222 draw_controller=btn, 223 h_align='center', 224 v_align='center', 225 color=(0.8, 0.8, 0.8, 0.8), 226 ) 227 index += 1 228 229 if index >= count: 230 break 231 if index >= count: 232 break 233 self._get_more_maps_button = btn = bui.buttonwidget( 234 parent=self._subcontainer, 235 size=(self._sub_width * 0.8, 60), 236 position=(self._sub_width * 0.1, 30), 237 label=bui.Lstr(resource='mapSelectGetMoreMapsText'), 238 on_activate_call=self._on_store_press, 239 color=(0.6, 0.53, 0.63), 240 textcolor=(0.75, 0.7, 0.8), 241 autoselect=True, 242 ) 243 bui.widget(edit=btn, show_buffer_top=30, show_buffer_bottom=30) 244 if select_get_more_maps_button: 245 bui.containerwidget( 246 edit=self._subcontainer, selected_child=btn, visible_child=btn 247 ) 248 249 def _on_store_press(self) -> None: 250 from bauiv1lib import account 251 from bauiv1lib.store.browser import StoreBrowserWindow 252 253 plus = bui.app.plus 254 assert plus is not None 255 256 if plus.get_v1_account_state() != 'signed_in': 257 account.show_sign_in_prompt() 258 return 259 StoreBrowserWindow( 260 modal=True, 261 show_tab=StoreBrowserWindow.TabID.MAPS, 262 on_close_call=self._on_store_close, 263 origin_widget=self._get_more_maps_button, 264 ) 265 266 def _on_store_close(self) -> None: 267 self._refresh(select_get_more_maps_button=True) 268 269 def _select(self, map_name: str) -> None: 270 from bauiv1lib.playlist.editgame import PlaylistEditGameWindow 271 272 # no-op if our underlying widget is dead or on its way out. 273 if not self._root_widget or self._root_widget.transitioning_out: 274 return 275 276 self._config['settings']['map'] = map_name 277 bui.containerwidget(edit=self._root_widget, transition='out_right') 278 assert bui.app.classic is not None 279 bui.app.ui_v1.set_main_menu_window( 280 PlaylistEditGameWindow( 281 self._gametype, 282 self._sessiontype, 283 self._config, 284 self._completion_call, 285 default_selection='map', 286 transition='in_left', 287 edit_info=self._edit_info, 288 ).get_root_widget(), 289 from_window=self._root_widget, 290 ) 291 292 def _select_with_delay(self, map_name: str) -> None: 293 bui.lock_all_input() 294 bui.apptimer(0.1, bui.unlock_all_input) 295 bui.apptimer(0.1, bui.WeakCall(self._select, map_name)) 296 297 def _cancel(self) -> None: 298 from bauiv1lib.playlist.editgame import PlaylistEditGameWindow 299 300 # no-op if our underlying widget is dead or on its way out. 301 if not self._root_widget or self._root_widget.transitioning_out: 302 return 303 304 bui.containerwidget(edit=self._root_widget, transition='out_right') 305 assert bui.app.classic is not None 306 bui.app.ui_v1.set_main_menu_window( 307 PlaylistEditGameWindow( 308 self._gametype, 309 self._sessiontype, 310 self._config, 311 self._completion_call, 312 default_selection='map', 313 transition='in_left', 314 edit_info=self._edit_info, 315 ).get_root_widget(), 316 from_window=self._root_widget, 317 )
class
PlaylistMapSelectWindow(bauiv1._uitypes.Window):
19class PlaylistMapSelectWindow(bui.Window): 20 """Window to select a map.""" 21 22 def __init__( 23 self, 24 gametype: type[bs.GameActivity], 25 sessiontype: type[bs.Session], 26 config: dict[str, Any], 27 edit_info: dict[str, Any], 28 completion_call: Callable[[dict[str, Any] | None], Any], 29 transition: str = 'in_right', 30 ): 31 from bascenev1 import get_filtered_map_name 32 33 self._gametype = gametype 34 self._sessiontype = sessiontype 35 self._config = config 36 self._completion_call = completion_call 37 self._edit_info = edit_info 38 self._maps: list[tuple[str, bui.Texture]] = [] 39 try: 40 self._previous_map = get_filtered_map_name( 41 config['settings']['map'] 42 ) 43 except Exception: 44 self._previous_map = '' 45 46 assert bui.app.classic is not None 47 uiscale = bui.app.ui_v1.uiscale 48 width = 815 if uiscale is bui.UIScale.SMALL else 615 49 x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 50 height = ( 51 400 52 if uiscale is bui.UIScale.SMALL 53 else 480 if uiscale is bui.UIScale.MEDIUM else 600 54 ) 55 56 top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 57 super().__init__( 58 root_widget=bui.containerwidget( 59 size=(width, height + top_extra), 60 transition=transition, 61 scale=( 62 2.17 63 if uiscale is bui.UIScale.SMALL 64 else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0 65 ), 66 stack_offset=( 67 (0, -27) if uiscale is bui.UIScale.SMALL else (0, 0) 68 ), 69 ) 70 ) 71 72 self._cancel_button = btn = bui.buttonwidget( 73 parent=self._root_widget, 74 position=(38 + x_inset, height - 67), 75 size=(140, 50), 76 scale=0.9, 77 text_scale=1.0, 78 autoselect=True, 79 label=bui.Lstr(resource='cancelText'), 80 on_activate_call=self._cancel, 81 ) 82 83 bui.containerwidget(edit=self._root_widget, cancel_button=btn) 84 bui.textwidget( 85 parent=self._root_widget, 86 position=(width * 0.5, height - 46), 87 size=(0, 0), 88 maxwidth=260, 89 scale=1.1, 90 text=bui.Lstr( 91 resource='mapSelectTitleText', 92 subs=[('${GAME}', self._gametype.get_display_string())], 93 ), 94 color=bui.app.ui_v1.title_color, 95 h_align='center', 96 v_align='center', 97 ) 98 v = height - 70 99 self._scroll_width = width - (80 + 2 * x_inset) 100 self._scroll_height = height - 140 101 102 self._scrollwidget = bui.scrollwidget( 103 parent=self._root_widget, 104 position=(40 + x_inset, v - self._scroll_height), 105 size=(self._scroll_width, self._scroll_height), 106 ) 107 bui.containerwidget( 108 edit=self._root_widget, selected_child=self._scrollwidget 109 ) 110 bui.containerwidget(edit=self._scrollwidget, claims_left_right=True) 111 112 self._subcontainer: bui.Widget | None = None 113 self._refresh() 114 115 def _refresh(self, select_get_more_maps_button: bool = False) -> None: 116 # pylint: disable=too-many-statements 117 # pylint: disable=too-many-branches 118 # pylint: disable=too-many-locals 119 from bascenev1 import ( 120 get_map_class, 121 get_map_display_string, 122 ) 123 124 assert bui.app.classic is not None 125 store = bui.app.classic.store 126 # Kill old. 127 if self._subcontainer is not None: 128 self._subcontainer.delete() 129 130 mesh_opaque = bui.getmesh('level_select_button_opaque') 131 mesh_transparent = bui.getmesh('level_select_button_transparent') 132 133 self._maps = [] 134 map_list = self._gametype.get_supported_maps(self._sessiontype) 135 map_list_sorted = list(map_list) 136 map_list_sorted.sort() 137 unowned_maps = store.get_unowned_maps() 138 139 for mapname in map_list_sorted: 140 # Disallow ones we don't own. 141 if mapname in unowned_maps: 142 continue 143 map_tex_name = get_map_class(mapname).get_preview_texture_name() 144 if map_tex_name is not None: 145 try: 146 map_tex = bui.gettexture(map_tex_name) 147 self._maps.append((mapname, map_tex)) 148 except Exception: 149 print(f'Invalid map preview texture: "{map_tex_name}".') 150 else: 151 print('Error: no map preview texture for map:', mapname) 152 153 count = len(self._maps) 154 columns = 2 155 rows = int(math.ceil(float(count) / columns)) 156 button_width = 220 157 button_height = button_width * 0.5 158 button_buffer_h = 16 159 button_buffer_v = 19 160 self._sub_width = self._scroll_width * 0.95 161 self._sub_height = ( 162 5 + rows * (button_height + 2 * button_buffer_v) + 100 163 ) 164 self._subcontainer = bui.containerwidget( 165 parent=self._scrollwidget, 166 size=(self._sub_width, self._sub_height), 167 background=False, 168 ) 169 index = 0 170 mask_texture = bui.gettexture('mapPreviewMask') 171 h_offs = 130 if len(self._maps) == 1 else 0 172 for y in range(rows): 173 for x in range(columns): 174 pos = ( 175 x * (button_width + 2 * button_buffer_h) 176 + button_buffer_h 177 + h_offs, 178 self._sub_height 179 - (y + 1) * (button_height + 2 * button_buffer_v) 180 + 12, 181 ) 182 btn = bui.buttonwidget( 183 parent=self._subcontainer, 184 button_type='square', 185 size=(button_width, button_height), 186 autoselect=True, 187 texture=self._maps[index][1], 188 mask_texture=mask_texture, 189 mesh_opaque=mesh_opaque, 190 mesh_transparent=mesh_transparent, 191 label='', 192 color=(1, 1, 1), 193 on_activate_call=bui.Call( 194 self._select_with_delay, self._maps[index][0] 195 ), 196 position=pos, 197 ) 198 if x == 0: 199 bui.widget(edit=btn, left_widget=self._cancel_button) 200 if y == 0: 201 bui.widget(edit=btn, up_widget=self._cancel_button) 202 if x == columns - 1 and bui.app.ui_v1.use_toolbars: 203 bui.widget( 204 edit=btn, 205 right_widget=bui.get_special_widget('party_button'), 206 ) 207 208 bui.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60) 209 if self._maps[index][0] == self._previous_map: 210 bui.containerwidget( 211 edit=self._subcontainer, 212 selected_child=btn, 213 visible_child=btn, 214 ) 215 name = get_map_display_string(self._maps[index][0]) 216 bui.textwidget( 217 parent=self._subcontainer, 218 text=name, 219 position=(pos[0] + button_width * 0.5, pos[1] - 12), 220 size=(0, 0), 221 scale=0.5, 222 maxwidth=button_width, 223 draw_controller=btn, 224 h_align='center', 225 v_align='center', 226 color=(0.8, 0.8, 0.8, 0.8), 227 ) 228 index += 1 229 230 if index >= count: 231 break 232 if index >= count: 233 break 234 self._get_more_maps_button = btn = bui.buttonwidget( 235 parent=self._subcontainer, 236 size=(self._sub_width * 0.8, 60), 237 position=(self._sub_width * 0.1, 30), 238 label=bui.Lstr(resource='mapSelectGetMoreMapsText'), 239 on_activate_call=self._on_store_press, 240 color=(0.6, 0.53, 0.63), 241 textcolor=(0.75, 0.7, 0.8), 242 autoselect=True, 243 ) 244 bui.widget(edit=btn, show_buffer_top=30, show_buffer_bottom=30) 245 if select_get_more_maps_button: 246 bui.containerwidget( 247 edit=self._subcontainer, selected_child=btn, visible_child=btn 248 ) 249 250 def _on_store_press(self) -> None: 251 from bauiv1lib import account 252 from bauiv1lib.store.browser import StoreBrowserWindow 253 254 plus = bui.app.plus 255 assert plus is not None 256 257 if plus.get_v1_account_state() != 'signed_in': 258 account.show_sign_in_prompt() 259 return 260 StoreBrowserWindow( 261 modal=True, 262 show_tab=StoreBrowserWindow.TabID.MAPS, 263 on_close_call=self._on_store_close, 264 origin_widget=self._get_more_maps_button, 265 ) 266 267 def _on_store_close(self) -> None: 268 self._refresh(select_get_more_maps_button=True) 269 270 def _select(self, map_name: str) -> None: 271 from bauiv1lib.playlist.editgame import PlaylistEditGameWindow 272 273 # no-op if our underlying widget is dead or on its way out. 274 if not self._root_widget or self._root_widget.transitioning_out: 275 return 276 277 self._config['settings']['map'] = map_name 278 bui.containerwidget(edit=self._root_widget, transition='out_right') 279 assert bui.app.classic is not None 280 bui.app.ui_v1.set_main_menu_window( 281 PlaylistEditGameWindow( 282 self._gametype, 283 self._sessiontype, 284 self._config, 285 self._completion_call, 286 default_selection='map', 287 transition='in_left', 288 edit_info=self._edit_info, 289 ).get_root_widget(), 290 from_window=self._root_widget, 291 ) 292 293 def _select_with_delay(self, map_name: str) -> None: 294 bui.lock_all_input() 295 bui.apptimer(0.1, bui.unlock_all_input) 296 bui.apptimer(0.1, bui.WeakCall(self._select, map_name)) 297 298 def _cancel(self) -> None: 299 from bauiv1lib.playlist.editgame import PlaylistEditGameWindow 300 301 # no-op if our underlying widget is dead or on its way out. 302 if not self._root_widget or self._root_widget.transitioning_out: 303 return 304 305 bui.containerwidget(edit=self._root_widget, transition='out_right') 306 assert bui.app.classic is not None 307 bui.app.ui_v1.set_main_menu_window( 308 PlaylistEditGameWindow( 309 self._gametype, 310 self._sessiontype, 311 self._config, 312 self._completion_call, 313 default_selection='map', 314 transition='in_left', 315 edit_info=self._edit_info, 316 ).get_root_widget(), 317 from_window=self._root_widget, 318 )
Window to select a map.
PlaylistMapSelectWindow( gametype: type[bascenev1._gameactivity.GameActivity], sessiontype: type[bascenev1._session.Session], config: dict[str, typing.Any], edit_info: dict[str, typing.Any], completion_call: Callable[[dict[str, Any] | None], Any], transition: str = 'in_right')
22 def __init__( 23 self, 24 gametype: type[bs.GameActivity], 25 sessiontype: type[bs.Session], 26 config: dict[str, Any], 27 edit_info: dict[str, Any], 28 completion_call: Callable[[dict[str, Any] | None], Any], 29 transition: str = 'in_right', 30 ): 31 from bascenev1 import get_filtered_map_name 32 33 self._gametype = gametype 34 self._sessiontype = sessiontype 35 self._config = config 36 self._completion_call = completion_call 37 self._edit_info = edit_info 38 self._maps: list[tuple[str, bui.Texture]] = [] 39 try: 40 self._previous_map = get_filtered_map_name( 41 config['settings']['map'] 42 ) 43 except Exception: 44 self._previous_map = '' 45 46 assert bui.app.classic is not None 47 uiscale = bui.app.ui_v1.uiscale 48 width = 815 if uiscale is bui.UIScale.SMALL else 615 49 x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 50 height = ( 51 400 52 if uiscale is bui.UIScale.SMALL 53 else 480 if uiscale is bui.UIScale.MEDIUM else 600 54 ) 55 56 top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 57 super().__init__( 58 root_widget=bui.containerwidget( 59 size=(width, height + top_extra), 60 transition=transition, 61 scale=( 62 2.17 63 if uiscale is bui.UIScale.SMALL 64 else 1.3 if uiscale is bui.UIScale.MEDIUM else 1.0 65 ), 66 stack_offset=( 67 (0, -27) if uiscale is bui.UIScale.SMALL else (0, 0) 68 ), 69 ) 70 ) 71 72 self._cancel_button = btn = bui.buttonwidget( 73 parent=self._root_widget, 74 position=(38 + x_inset, height - 67), 75 size=(140, 50), 76 scale=0.9, 77 text_scale=1.0, 78 autoselect=True, 79 label=bui.Lstr(resource='cancelText'), 80 on_activate_call=self._cancel, 81 ) 82 83 bui.containerwidget(edit=self._root_widget, cancel_button=btn) 84 bui.textwidget( 85 parent=self._root_widget, 86 position=(width * 0.5, height - 46), 87 size=(0, 0), 88 maxwidth=260, 89 scale=1.1, 90 text=bui.Lstr( 91 resource='mapSelectTitleText', 92 subs=[('${GAME}', self._gametype.get_display_string())], 93 ), 94 color=bui.app.ui_v1.title_color, 95 h_align='center', 96 v_align='center', 97 ) 98 v = height - 70 99 self._scroll_width = width - (80 + 2 * x_inset) 100 self._scroll_height = height - 140 101 102 self._scrollwidget = bui.scrollwidget( 103 parent=self._root_widget, 104 position=(40 + x_inset, v - self._scroll_height), 105 size=(self._scroll_width, self._scroll_height), 106 ) 107 bui.containerwidget( 108 edit=self._root_widget, selected_child=self._scrollwidget 109 ) 110 bui.containerwidget(edit=self._scrollwidget, claims_left_right=True) 111 112 self._subcontainer: bui.Widget | None = None 113 self._refresh()
Inherited Members
- bauiv1._uitypes.Window
- get_root_widget