ui: category selection, plugin names, settings

This commit is contained in:
Rikko 2022-07-03 14:32:05 +05:30
parent cc29e6f8bc
commit 5957c6cfad
2 changed files with 671 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
reference.py

670
plugin_manager.py Normal file
View file

@ -0,0 +1,670 @@
# ba_meta require api 6
import ba
import _ba
import bastd
import urllib.request
import json
import asyncio
from typing import Union, Optional
INDEX_META = "https://raw.githubusercontent.com/bombsquad-community/mod-manager/main/index.json"
HEADERS = {
"User-Agent": _ba.env()["user_agent_string"],
}
async def send_network_request(request):
loop = asyncio.get_event_loop()
response = await loop.run_in_executor(None, urllib.request.urlopen, request)
return response
class Category:
def __init__(self, name, base_download_url, meta_url):
self.name = name
self.base_download_url = base_download_url
self.meta_url = meta_url
self._plugins = None
async def get_plugins(self):
if self._plugins is None:
request = urllib.request.Request(
self.meta_url,
headers=HEADERS
)
response = await send_network_request(request)
self._plugins = json.loads(response.read())
return self._plugins
async def refresh(self):
self._plugins = {}
return await self.get_plugins()
class CategoryAll(Category):
def __init__(self, plugins={}):
super().__init__(name="All", base_download_url=None, meta_url=None)
self._plugins = plugins
class PluginManager:
def __init__(self):
self.request_headers = HEADERS
self._index = {}
# @property
# def index(self):
# if not self._index:
# loop = asyncio.get_event_loop()
# self._index = loop.create_task(self.plugin_index())
# return self._index
# def plugin_index(self):
# loop = asyncio.get_event_loop()
# loop.create_task(self.plugin_index())
async def get_index(self):
if not self._index:
request = urllib.request.Request(
INDEX_META,
headers=HEADERS
)
response = await send_network_request(request)
self._index = json.loads(response.read())
return self._index
async def refresh(self):
self._index = {}
return await self.get_index()
class PluginManagerWindow(ba.Window, PluginManager):
def __init__(self, transition: str = "in_right", origin_widget: ba.Widget = None):
PluginManager.__init__(self)
self.categories = {}
self.category_selection_button = None
self.selected_category = None
# ba._asyncio._asyncio_event_loop.create_task(self.setup_plugin_categories())
ba._asyncio._asyncio_event_loop.create_task(self.plugin_index())
uiscale = ba.app.ui.uiscale
self._width = 650
self._height = 380 if uiscale is ba.UIScale.SMALL else 420 if uiscale is ba.UIScale.MEDIUM else 500
top_extra = 20 if uiscale is ba.UIScale.SMALL else 0
if origin_widget:
self._transition_out = "out_scale"
self._scale_origin = origin_widget.get_screen_space_center()
transition = "in_scale"
self._root_widget = ba.containerwidget(
size=(self._width, self._height + top_extra),
transition=transition,
toolbar_visibility="menu_minimal",
scale_origin_stack_offset=self._scale_origin,
parent=_ba.get_special_widget("overlay_stack"),
scale=1.9 if uiscale is ba.UIScale.SMALL else 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0,
stack_offset=(0, -25) if uiscale is ba.UIScale.SMALL else (0, 0)
)
# self._back_button = back_button = ba.buttonwidget(
# parent=self._root_widget,
# position=(self._width - 160, self._height - 60), size=(160, 68),
# button_type="back", scale=0.75, autoselect=True, text_scale=1.2,
# label="Back", on_activate_call=self._back)
# ba.containerwidget(edit=self._root_widget, cancel_button=back_button)
ba.textwidget(
parent=self._root_widget,
position=(-10, self._height - 50),
size=(self._width, 25),
text="Community Plugin Manager",
color=ba.app.ui.title_color,
scale=1.05,
h_align="center",
v_align="center",
maxwidth=270,
)
self._loading_text = ba.textwidget(
parent=self._root_widget,
position=(-10, self._height - 150),
size=(self._width, 25),
text="Loading...",
color=ba.app.ui.title_color,
scale=0.7,
h_align="center",
v_align="center",
maxwidth=270,
)
self._scrollwidget = ba.scrollwidget(parent=self._root_widget,
size=(420, 340),
position=(160, 40))
self._columnwidget = ba.columnwidget(parent=self._scrollwidget,
border=2,
margin=0)
# name = ba.textwidget(parent=self._columnwidget,
# size=(410, 30),
# selectable=True, always_highlight=True,
# color=(1,1,1),
# on_select_call=lambda: None,
# text="ColorScheme",
# on_activate_call=lambda: None,
# h_align='left', v_align='center',
# maxwidth=420)
# v = (self._height - 75) if uiscale is ba.UIScale.SMALL else (self._height - 59)
# h = 40
# b_textcolor = (0.75, 0.7, 0.8)
# b_color = (0.6, 0.53, 0.63)
# s = 1.0 if uiscale is ba.UIScale.SMALL else 1.27 if uiscale is ba.UIScale.MEDIUM else 1.57
# b_size = (90, 60 * s)
# v -= 63 * s
# self.reload_button = ba.buttonwidget(parent=self._root_widget,
# position=(h, v), size=b_size,
# on_activate_call=self._refresh,
# label="Reload List",
# button_type="square",
# color=b_color,
# textcolor=b_textcolor,
# autoselect=True, text_scale=0.7)
# v -= 63 * s
# self.info_button = ba.buttonwidget(parent=self._root_widget,
# position=(h, v), size=b_size,
# on_activate_call=self._get_info,
# label="Mod Info",
# button_type="square", color=b_color,
# textcolor=b_textcolor,
# autoselect=True, text_scale=0.7)
# v -= 63 * s
# self.sort_button = ba.buttonwidget(parent=self._root_widget,
# position=(h, v), size=b_size,
# on_activate_call=self._sort,
# label="Sort\nAlphabetical",
# button_type="square", color=b_color,
# textcolor=b_textcolor,
# text_scale=0.7, autoselect=True)
# v -= 63 * s
# self.settings_button = ba.buttonwidget(parent=self._root_widget,
# position=(h, v), size=b_size,
# on_activate_call=self._settings,
# label="Settings",
# button_type="square",
# color=b_color,
# textcolor=b_textcolor,
# autoselect=True, text_scale=0.7)
# self.column_pos_y = self._height - 75 - self.tab_height
# self._scroll_width = self._width - 180
# self._scroll_height = self._height - 120 - self.tab_height
# self._scrollwidget = ba.scrollwidget(parent=self._root_widget, size=(
# self._scroll_width, self._scroll_height), position=(
# 140, self.column_pos_y - self._scroll_height + 10))
# self._columnwidget = ba.columnwidget(parent=self._scrollwidget,
# border=2, margin=0)
# self._mod_selected = None
# self._refresh()
async def setup_plugin_categories(self, plugin_index):
self.categories["All"] = None
requests = []
for plugin_category in plugin_index["plugin_categories"]:
category = Category(
plugin_category["display_name"],
plugin_category["base_download_url"],
plugin_category["meta"],
)
self.categories[plugin_category["display_name"]] = category
request = category.get_plugins()
requests.append(request)
categories = await asyncio.gather(*requests)
all_plugins = {}
for plugins in categories:
all_plugins.update(plugins)
self.categories["All"] = CategoryAll(plugins=all_plugins)
async def plugin_index(self):
index = await super().get_index()
await asyncio.gather(
self.draw_settings_icon(),
self.setup_plugin_categories(index),
)
self._loading_text.delete()
await self.select_category("All")
await self.draw_search_bar()
async def draw_category_selection_button(self, label=None):
uiscale = ba.app.ui.uiscale
# v = (self._height - 75) if uiscale is ba.UIScale.SMALL else (self._height - 105)
v = 395
h = 440
# s = 1.0 if uiscale is ba.UIScale.SMALL else 1.27 if uiscale is ba.UIScale.MEDIUM else 1.57
s = 1.75
# b_size = (90, 60 * s)
b_size = (150, 30)
b_textcolor = (0.75, 0.7, 0.8)
b_color = (0.6, 0.53, 0.63)
if label is None:
label = self.selected_category
label = f"Category: {label}"
if self.category_selection_button is not None:
self.category_selection_button.delete()
loop = asyncio.get_event_loop()
self.category_selection_button = ba.buttonwidget(parent=self._root_widget,
position=(h, v),
size=b_size,
# on_activate_call=ba.Call(loop.create_task, self.show_categories()),
on_activate_call=self.show_categories,
label=label,
button_type="square",
color=b_color,
textcolor=b_textcolor,
autoselect=True,
text_scale=0.6)
async def draw_search_bar(self):
# TODO
ba.textwidget(parent=self._root_widget,
position=(200, 395),
scale=0.7,
# selectable=True,
always_highlight=True,
color=(0.4,0.4,0.4),
# on_select_call=lambda: None,
text="<Implement plugin search>",
on_activate_call=lambda: None,
h_align='left',
v_align='center',
maxwidth=420)
async def draw_settings_icon(self):
controller_button = ba.buttonwidget(parent=self._root_widget,
autoselect=True,
position=(600, 70),
size=(30, 30),
button_type="square",
label="",
on_activate_call=lambda: None)
ba.imagewidget(parent=self._root_widget,
position=(600, 70),
size=(30, 30),
color=(0.8, 0.95, 1),
texture=ba.gettexture("settingsIcon"),
draw_controller=controller_button)
async def draw_plugin_names(self):
uiscale = ba.app.ui.uiscale
v = (self._height - 75) if uiscale is ba.UIScale.SMALL else (self._height - 105)
h = 440
# s = 1.0 if uiscale is ba.UIScale.SMALL else 1.27 if uiscale is ba.UIScale.MEDIUM else 1.57
s = 1.75
# b_size = (90, 60 * s)
b_size = (150, 30)
b_textcolor = (0.75, 0.7, 0.8)
b_color = (0.6, 0.53, 0.63)
for plugin in self._columnwidget.get_children():
plugin.delete()
plugins = await self.categories[self.selected_category].get_plugins()
for plugin in plugins.keys():
ba.textwidget(parent=self._columnwidget,
size=(410, 30),
selectable=True, always_highlight=True,
color=(1,1,1),
on_select_call=lambda: None,
text=plugin,
on_activate_call=lambda: None,
h_align='left', v_align='center',
maxwidth=420)
def show_categories(self):
uiscale = ba.app.ui.uiscale
# On each new entry, change position to y -= 40.
value = bastd.ui.popup.PopupMenuWindow(
# position=(200, 40),
position=(200, 0),
scale=(2.3 if uiscale is ba.UIScale.SMALL else
1.65 if uiscale is ba.UIScale.MEDIUM else 1.23),
choices=self.categories.keys(),
current_choice=self.selected_category,
delegate=self)
async def select_category(self, category):
self.selected_category = category
await self.draw_category_selection_button(label=category)
await self.draw_plugin_names()
def popup_menu_selected_choice(self, window, choice):
loop = asyncio.get_event_loop()
loop.create_task(self.select_category(choice))
def popup_menu_closing(self, window):
pass
async def refresh(self):
pass
class NewAllSettingsWindow(ba.Window):
def __init__(self,
transition: str = "in_right",
origin_widget: ba.Widget = None):
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
import threading
# Preload some modules we use in a background thread so we won"t
# have a visual hitch when the user taps them.
threading.Thread(target=self._preload_modules).start()
ba.set_analytics_screen("Settings 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
uiscale = ba.app.ui.uiscale
width = 900 if uiscale is ba.UIScale.SMALL else 670
x_inset = 75 if uiscale is ba.UIScale.SMALL else 0
height = 435
self._r = "settingsWindow"
top_extra = 20 if uiscale is ba.UIScale.SMALL else 0
uiscale = ba.app.ui.uiscale
super().__init__(root_widget=ba.containerwidget(
size=(width, height + top_extra),
transition=transition,
toolbar_visibility="menu_minimal",
scale_origin_stack_offset=scale_origin,
scale=(1.75 if uiscale is ba.UIScale.SMALL else
1.35 if uiscale is ba.UIScale.MEDIUM else 1.0),
stack_offset=(0, -8) if uiscale is ba.UIScale.SMALL else (0, 0)))
if ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL:
self._back_button = None
ba.containerwidget(edit=self._root_widget,
on_cancel_call=self._do_back)
else:
self._back_button = btn = ba.buttonwidget(
parent=self._root_widget,
autoselect=True,
position=(40 + x_inset, height - 55),
size=(130, 60),
scale=0.8,
text_scale=1.2,
label=ba.Lstr(resource="backText"),
button_type="back",
on_activate_call=self._do_back)
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
ba.textwidget(parent=self._root_widget,
position=(0, height - 44),
size=(width, 25),
text=ba.Lstr(resource=self._r + ".titleText"),
color=ba.app.ui.title_color,
h_align="center",
v_align="center",
maxwidth=130)
if self._back_button is not None:
ba.buttonwidget(edit=self._back_button,
button_type="backSmall",
size=(60, 60),
label=ba.charstr(ba.SpecialChar.BACK))
v = height - 80
v -= 145
basew = 200
baseh = 160
x_offs = x_inset + (105 if uiscale is ba.UIScale.SMALL else
72) - basew # now unused
x_offs2 = x_offs + basew - 7
x_offs3 = x_offs + 2 * (basew - 7)
x_offs4 = x_offs + 3 * (basew - 7)
x_offs5 = x_offs2 + 0.5 * (basew - 7)
x_offs6 = x_offs5 + (basew - 7)
def _b_title(x: float, y: float, button: ba.Widget,
text: Union[str, ba.Lstr]) -> None:
ba.textwidget(parent=self._root_widget,
text=text,
position=(x + basew * 0.47, y + baseh * 0.22),
maxwidth=basew * 0.7, size=(0, 0),
h_align="center",
v_align="center",
draw_controller=button,
color=(0.7, 0.9, 0.7, 1.0))
ctb = self._controllers_button = ba.buttonwidget(parent=self._root_widget,
autoselect=True,
position=(x_offs2, v),
size=(basew, baseh),
button_type="square",
label="",
on_activate_call=self._do_controllers)
if ba.app.ui.use_toolbars and self._back_button is None:
bbtn = _ba.get_special_widget("back_button")
ba.widget(edit=ctb, left_widget=bbtn)
_b_title(x_offs2, v, ctb,
ba.Lstr(resource=self._r + ".controllersText"))
imgw = imgh = 130
ba.imagewidget(parent=self._root_widget,
position=(x_offs2 + basew * 0.49 - imgw * 0.5, v + 35),
size=(imgw, imgh),
texture=ba.gettexture("controllerIcon"),
draw_controller=ctb)
gfxb = self._graphics_button = ba.buttonwidget(parent=self._root_widget,
autoselect=True,
position=(x_offs3, v),
size=(basew, baseh),
button_type="square",
label="",
on_activate_call=self._do_graphics)
if ba.app.ui.use_toolbars:
pbtn = _ba.get_special_widget("party_button")
ba.widget(edit=gfxb, up_widget=pbtn, right_widget=pbtn)
_b_title(x_offs3, v, gfxb, ba.Lstr(resource=self._r + ".graphicsText"))
imgw = imgh = 110
ba.imagewidget(parent=self._root_widget,
position=(x_offs3 + basew * 0.49 - imgw * 0.5, v + 42),
size=(imgw, imgh),
texture=ba.gettexture("graphicsIcon"),
draw_controller=gfxb)
abtn = self._audio_button = ba.buttonwidget(parent=self._root_widget,
autoselect=True,
position=(x_offs4, v),
size=(basew, baseh),
button_type="square",
label="",
on_activate_call=self._do_audio)
_b_title(x_offs4, v, abtn, ba.Lstr(resource=self._r + ".audioText"))
imgw = imgh = 120
ba.imagewidget(parent=self._root_widget, position=(
x_offs4 + basew * 0.49 - imgw * 0.5 + 5, v + 35), size=(imgw, imgh),
color=(1, 1, 0), texture=ba.gettexture("audioIcon"),
draw_controller=abtn)
v -= (baseh - 5)
avb = self._advanced_button = ba.buttonwidget(parent=self._root_widget,
autoselect=True,
position=(x_offs5, v),
size=(basew, baseh),
button_type="square",
label="",
on_activate_call=self._do_advanced)
_b_title(x_offs5, v, avb, ba.Lstr(resource=self._r + ".advancedText"))
imgw = imgh = 120
ba.imagewidget(parent=self._root_widget,
position=(x_offs5 + basew * 0.49 - imgw * 0.5 + 5,
v + 35),
size=(imgw, imgh),
color=(0.8, 0.95, 1),
texture=ba.gettexture("advancedIcon"),
draw_controller=avb)
mmb = self._modmgr_button = ba.buttonwidget(parent=self._root_widget,
autoselect=True,
position=(x_offs6, v),
size=(basew, baseh),
button_type="square",
label="",
on_activate_call=self._do_modmanager)
_b_title(x_offs6, v, avb, ba.Lstr(value="Plugin Manager"))
imgw = imgh = 120
ba.imagewidget(parent=self._root_widget,
position=(x_offs6 + basew * 0.49 - imgw * 0.5 + 5,
v + 35),
size=(imgw, imgh),
color=(0.8, 0.95, 1),
texture=ba.gettexture("heart"),
draw_controller=mmb)
self._restore_state()
# noinspection PyUnresolvedReferences
@staticmethod
def _preload_modules() -> None:
"""Preload modules we use (called in bg thread)."""
import bastd.ui.mainmenu as _unused1
import bastd.ui.settings.controls as _unused2
import bastd.ui.settings.graphics as _unused3
import bastd.ui.settings.audio as _unused4
import bastd.ui.settings.advanced as _unused5
def _do_back(self) -> None:
# pylint: disable=cyclic-import
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())
def _do_controllers(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings.controls import ControlsSettingsWindow
self._save_state()
ba.containerwidget(edit=self._root_widget, transition="out_left")
ba.app.ui.set_main_menu_window(ControlsSettingsWindow(
origin_widget=self._controllers_button).get_root_widget())
def _do_graphics(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings.graphics import GraphicsSettingsWindow
self._save_state()
ba.containerwidget(edit=self._root_widget, transition="out_left")
ba.app.ui.set_main_menu_window(GraphicsSettingsWindow(
origin_widget=self._graphics_button).get_root_widget())
def _do_audio(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings.audio import AudioSettingsWindow
self._save_state()
ba.containerwidget(edit=self._root_widget, transition="out_left")
ba.app.ui.set_main_menu_window(AudioSettingsWindow(
origin_widget=self._audio_button).get_root_widget())
def _do_advanced(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.settings.advanced import AdvancedSettingsWindow
self._save_state()
ba.containerwidget(edit=self._root_widget, transition="out_left")
ba.app.ui.set_main_menu_window(AdvancedSettingsWindow(
origin_widget=self._advanced_button).get_root_widget())
def _do_modmanager(self) -> None:
self._save_state()
ba.containerwidget(edit=self._root_widget, transition="out_left")
ba.app.ui.set_main_menu_window(PluginManagerWindow(
origin_widget=self._advanced_button).get_root_widget())
def _save_state(self) -> None:
try:
sel = self._root_widget.get_selected_child()
if sel == self._controllers_button:
sel_name = "Controllers"
elif sel == self._graphics_button:
sel_name = "Graphics"
elif sel == self._audio_button:
sel_name = "Audio"
elif sel == self._advanced_button:
sel_name = "Advanced"
elif sel == self._modmgr_button:
sel_name = "Mod Manager"
elif sel == self._back_button:
sel_name = "Back"
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:
try:
sel_name = ba.app.ui.window_states.get(type(self),
{}).get("sel_name")
sel: Optional[ba.Widget]
if sel_name == "Controllers":
sel = self._controllers_button
elif sel_name == "Graphics":
sel = self._graphics_button
elif sel_name == "Audio":
sel = self._audio_button
elif sel_name == "Advanced":
sel = self._advanced_button
elif sel_name == "Mod Manager":
sel = self._modmgr_button
elif sel_name == "Back":
sel = self._back_button
else:
sel = self._controllers_button
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}.")
# ba_meta export plugin
class EntryPoint(ba.Plugin):
def on_app_launch(self) -> None:
"""Called when the app is being launched."""
from bastd.ui.settings import allsettings
allsettings.AllSettingsWindow = NewAllSettingsWindow
asyncio.set_event_loop(ba._asyncio._asyncio_event_loop)
loop = asyncio.get_event_loop()
# loop.create_task(do())
# pm = PluginManager()
# pm.plugin_index()
def on_app_pause(self) -> None:
"""Called after pausing game activity."""
print("pause")
def on_app_resume(self) -> None:
"""Called after the game continues."""
print("resume")
def on_app_shutdown(self) -> None:
"""Called before closing the application."""
print("shutdown")