Select Git revision
main_menu.py
main_menu.py 15.58 KiB
import os
import machine
import sys_display
import leds
from ctx import Context
from st3m.goose import Optional, List, Set
from st3m.ui.view import (
ViewManager,
ViewTransitionDirection,
ViewTransitionNone,
BaseView,
)
from st3m import InputState
from st3m import settings_menu as settings
import st3m.wifi
from st3m.ui import led_patterns
from st3m.ui.menu import (
MenuItem,
MenuItemBack,
MenuItemForeground,
MenuItemAction,
MenuItemLaunchPersistentView,
)
from st3m.application import (
BundleManager,
BundleMetadata,
MenuItemAppLaunch,
)
from st3m.about import About
from st3m.ui.elements.menus import OSMenu, SunMenu
from st3m.utils import rm_recursive, simplify_path
from st3m import application_settings
from st3m.ui import colours
import captouch
import bl00mbox
class ApplicationTinyMenu(BaseView):
def __init__(self, item, item_destructor=None) -> None:
super().__init__()
self._vm = None
self._index = 0
self._delete_confirm_index = 0
self._labels = ["fav", "boot", "delete"]
self.override_os_button_back = True
self._blend = 0
self._col_anim = 0
if not isinstance(item, MenuItemAppLaunch):
self.exit()
self._bundle = item._bundle
self._item_destructor = item_destructor
def draw(self, ctx):
phase = self._col_anim
phase = 2 - phase if phase > 1 else phase
highlight_col = colours.hsv_to_rgb(2.3 - phase, 1, 1)
if self._blend > 0:
ctx.rgba(0, 0, 0, 0.1)
ctx.rectangle(-120, 20, 240, 100).fill()
ctx.rectangle(-120, -120, 240, 100).fill()
self._blend -= 1
ctx.rgb(0, 0, 0)
ctx.round_rectangle(-24, 16, 8 + 80 * self._size, 8 + 60 * self._size, 3).fill()
ctx.rgb(0.8, 0.8, 0.8)
ctx.round_rectangle(-20, 20, 80 * self._size, 60 * self._size, 3).stroke()
if self._size < 1:
# no text -> draw moar frames animation moar good
return
if self._delete_confirm_index:
ypos = 42
ctx.font_size = 18
ctx.text_align = ctx.CENTER
ctx.move_to(20, ypos)
ctx.gray(1)
ctx.text("delete?")
ypos += 23
for k in [0, 1]:
text = ["yes", "no"][k]
ctx.move_to(20 + 30 * (2 * k - 1), ypos)
ctx.text_align = [ctx.LEFT, ctx.RIGHT][k]
if self._delete_confirm_index == 2 - k:
ctx.rgb(*highlight_col)
else:
ctx.gray(1)
ctx.text(text)
return
ypos = 37
ctx.font_size = 16
ctx.text_align = ctx.LEFT
for x, label in enumerate(self._labels):
ctx.move_to(-13, ypos)
if x == self._index:
ctx.rgb(*highlight_col)
else:
ctx.gray(1)
ctx.text(label)
marker_tag = None
if x == 0:
marker_tag = application_settings.ApplicationTagFav
if x == 1:
marker_tag = application_settings.ApplicationTagAutostart
if marker_tag:
ctx.save()
ctx.text_align = ctx.CENTER
ctx.move_to(22, ypos)
ctx.text("[")
ctx.move_to(50, ypos)
ctx.text("]")
for tag in self._bundle.tags:
if isinstance(tag, marker_tag):
ctx.translate(36, ypos)
ctx.move_to(0, 0)
width = ctx.text_width(tag.text)
if width > 22:
ctx.text("x")
else:
ctx.text(tag.text)
break
ctx.restore()
ypos += 18
def on_enter(self, vm: Optional[ViewManager]) -> None:
super().on_enter(vm)
self._blend = 5
self._vm = vm
self._size = 0
def exit(self):
self._vm.pop(ViewTransitionNone())
def think(self, ins: InputState, delta_ms: int) -> None:
self._col_anim += delta_ms / 1000
self._col_anim %= 2
if self._size < 1:
self._size += delta_ms / 150
if self._size > 1:
self._size = 1
super().think(ins, delta_ms)
if self.input.buttons.os.middle.released and self._vm:
if self._delete_confirm_index:
self._delete_confirm_index = 0
else:
self.exit()
elif self.input.buttons.app.middle.pressed:
if self._delete_confirm_index == 1:
self._delete_confirm_index = 0
elif self._delete_confirm_index == 2:
# delete
path = simplify_path(self._bundle.path)
if path.startswith("/sd/apps") or path.startswith("/flash/sys/apps"):
print(f"deleting {path}...")
rm_recursive(path)
else:
print(f"can't delete {path}")
application_settings.delete_app_data(self._bundle.path)
self._item_destructor()
self.exit()
elif self._index == 0:
# change tag
fav = False
for x, tag in enumerate(self._bundle.tags):
if isinstance(tag, application_settings.ApplicationTagFav):
fav = True
break
application_settings.set_app_field(self._bundle.path, "fav", not fav)
self._bundle.update_tags()
elif self._index == 1:
# set to autostart
autostart = False
for x, tag in enumerate(self._bundle.tags):
if isinstance(tag, application_settings.ApplicationTagAutostart):
autostart = True
break
if autostart:
application_settings.set_autostart(None, None)
else:
application_settings.set_autostart(
self._bundle.path, self._bundle.name
)
self._bundle.update_tags()
elif self._index == 2:
self._delete_confirm_index = 1
else:
lr_dir = self.input.buttons.app.right.pressed
lr_dir -= self.input.buttons.app.left.pressed
if lr_dir:
if self._delete_confirm_index:
self._delete_confirm_index %= 2
self._delete_confirm_index += 1
else:
self._index += lr_dir
self._index %= len(self._labels)
def restore_sys_defaults():
# fall back to system defaults on app exit
st3m.wifi._onoff_wifi_update()
# set the default graphics mode, this is a no-op if
# it is already set
sys_display.set_mode(0)
sys_display.fbconfig(240, 240, 0, 0)
leds.set_slew_rate(100)
leds.set_gamma(1.0, 1.0, 1.0)
leds.set_auto_update(False)
leds.set_brightness(settings.num_leds_brightness.value)
sys_display.set_backlight(settings.num_display_brightness.value)
led_patterns.set_menu_colors()
bl00mbox.Sys.foreground_clear()
# media.stop()
class RestoreMenu(OSMenu):
def _restore_sys_defaults(self) -> None:
if (
not self.vm
or not self.is_active()
or self.vm.direction != ViewTransitionDirection.BACKWARD
):
return
restore_sys_defaults()
def on_enter(self, vm: Optional[ViewManager]) -> None:
super().on_enter(vm)
self.captouch_active = settings.onoff_touch_os.value
self._restore_sys_defaults()
def on_enter_done(self):
# set the defaults again in case the app continued
# doing stuff during the transition
self._restore_sys_defaults()
leds.update()
class ApplicationMenu(RestoreMenu):
def __init__(self, items: List[MenuItem], name=None) -> None:
super().__init__(items)
self._button_latch = True
self.input.buttons.app.middle.repeat_enable(1000, 1000)
self._vm = None
self._update_tags()
self._name = name
def get_help(self):
if self._name is None:
ret = f"This is an app launcher menu."
else:
ret = f"This is the app launcher menu of the {self._name} category."
ret += "\n\n" + (
"You can long-press the app button to open a context menu that "
"allows you to delete apps, sort them to the top of the list "
'with the "<3" option or start them automatically at boot with the '
'"boot" option.'
)
ret += "\n\n" + super().get_help()
return ret
def _update_tags(self):
for item in self._items:
if isinstance(item, MenuItemAppLaunch):
for tag in item._bundle.tags:
if isinstance(tag, application_settings.ApplicationTagNew):
self.tags = [application_settings.ApplicationTagNew()]
return
self.tags = []
def _pop_target_item(self):
pos = self._scroll_controller.target_position()
self._items.pop(pos)
self._scroll_controller.set_position(pos - 1)
self._scroll_controller.set_item_count(len(self._items))
def _sort(self):
def sort_key(item):
if isinstance(item, MenuItemBack):
return (-999, "")
elif isinstance(item, MenuItemAppLaunch):
rating = 0
for tag in item._bundle.tags:
rating += tag.rating
return (-rating, item._bundle.name.lower())
else:
return (999, "")
current_item = self._items[self._scroll_controller.target_position()]
self._items.sort(key=sort_key)
self._scroll_controller.set_position(self._items.index(current_item))
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
if self._button_latch:
if not ins.buttons.app:
self._button_latch = False
else:
if self.input.buttons.app.middle.repeated and self._vm:
item = self._items[self._scroll_controller.target_position()]
self._vm.push(
ApplicationTinyMenu(item, self._pop_target_item),
ViewTransitionNone(),
)
elif self.input.buttons.app.middle.released or any(
[self.input.captouch.petals[x].whole.pressed for x in [3, 7]]
):
super().select()
def select(self) -> None:
pass
def on_enter(self, vm: Optional[ViewManager]) -> None:
super().on_enter(vm)
self._vm = vm
self._button_latch = True
self._sort()
self._update_tags()
for item in self._items:
if isinstance(item, MenuItemAppLaunch):
item._bundle.update_tags()
def _get_bundle_menu_kinds(mgr: BundleManager) -> Set[str]:
kinds: Set[str] = set()
for bundle in mgr.bundles.values():
kinds.update(bundle.menu_kinds())
return kinds
def _get_bundle_menu_entries(mgr: BundleManager, kind: str) -> List[MenuItem]:
entries: List[MenuItem] = []
ids = sorted(mgr.bundles.keys(), key=str.lower)
for id in ids:
bundle = mgr.bundles[id]
entries += bundle.menu_entries(kind)
return entries
def _yeet_local_changes() -> None:
os.remove("/flash/sys/.sys-installed")
machine.reset()
def _clear_autostart() -> None:
application_settings.set_autostart(None, None)
class MenuItemApplicationBack(MenuItemBack, MenuItemAppLaunch):
def __init__(self):
self._bundle = BundleMetadata(
"/++back_button",
{
"app": {"name": "back button", "category": "hidden", "menu": "hidden"},
"metadata": {"version": 0},
},
)
def draw(self, ctx):
ctx.save()
super().draw(ctx)
ctx.restore()
ctx.save()
ctx.rel_move_to(-5, -15)
for tag in self._bundle.tags:
tag.draw(ctx)
ctx.restore()
class MenuItemApplicationMenu(MenuItemForeground):
def draw(self, ctx: Context) -> None:
ctx.save()
super().draw(ctx)
ctx.rel_move_to(0, -5)
for tag in self._r.tags:
tag.draw(ctx)
ctx.restore()
class SystemMenu(RestoreMenu):
def get_help(self):
ret = (
"This is the system menu of flow3r, where you can access the app store, "
"change settings, update your firmware and more!"
)
ret += "\n\n" + super().get_help()
return ret
class MainMenu(SunMenu):
def __init__(self, bundles: Optional[list] = None) -> None:
if bundles:
self._bundles = bundles
else:
self._bundles = BundleManager()
self._bundles.update()
self.load_menu(reload_bundles=False)
def get_help(self):
ret = (
"Welcome to flow3r! This is the main menu, where you can select different "
"app categories or download apps and updates and change settings "
"in the System option."
)
ret += "\n\n" + super().get_help()
return ret
def load_menu(self, reload_bundles: bool = True) -> None:
"""
(Re)loads the menu.
Calling this after it has been loaded is a potential memory leak issue.
"""
if reload_bundles:
self._bundles.bundles = {}
self._bundles.update()
self.build_menu_items()
super().__init__(self._items)
def build_menu_items(self) -> None:
menu_settings = settings.build_menu()
menu_system = SystemMenu(
[
MenuItemBack(),
MenuItemForeground("Settings", menu_settings),
MenuItemAppLaunch(BundleMetadata("/flash/sys/apps/gr33nhouse")),
MenuItemAppLaunch(BundleMetadata("/flash/sys/apps/updat3r")),
MenuItemAction("Disk Mode (Flash)", machine.disk_mode_flash),
MenuItemAction("Disk Mode (SD)", machine.disk_mode_sd),
MenuItemAction("Yeet Local Changes", _yeet_local_changes),
MenuItemAction("Clear Autostart", _clear_autostart),
MenuItemAction("Reboot", machine.reset),
MenuItemLaunchPersistentView("About", About),
],
)
app_kinds = _get_bundle_menu_kinds(self._bundles)
menu_categories = ["Badge", "Music", "Media", "Apps", "Games", "Demos"]
for kind in app_kinds:
if kind not in ["Hidden", "System"] and kind not in menu_categories:
menu_categories.append(kind)
categories = [
MenuItemApplicationMenu(
kind, ApplicationMenu([MenuItemApplicationBack()] + entries, name=kind)
)
for kind in menu_categories
if (entries := _get_bundle_menu_entries(self._bundles, kind))
]
categories.append(MenuItemForeground("System", menu_system))
self._items = categories
# # self._scroll_controller = ScrollController()
# self._scroll_controller.set_item_count(len(categories))
def on_enter(self, vm):
super().on_enter(vm)
self.sensitivity = 1
self.captouch_active = settings.onoff_touch_os.value
if self.vm.direction == ViewTransitionDirection.FORWARD:
led_patterns.set_menu_colors()
leds.set_slew_rate(20)
leds.update()