Skip to content
Snippets Groups Projects
Select Git revision
  • 3eb69875e97bb234f211a7f584413b4a1492cfab
  • main default protected
  • blm_dev_chan
  • release/1.4.0 protected
  • widgets_draw
  • return_of_melodic_demo
  • task_cleanup
  • mixer2
  • dx/fb-save-restore
  • dx/dldldld
  • fpletz/flake
  • dx/jacksense-headset-mic-only
  • release/1.3.0 protected
  • fil3s-limit-filesize
  • allow-reloading-sunmenu
  • wifi-json-error-handling
  • app_text_viewer
  • shoegaze-fps
  • media_has_video_has_audio
  • fil3s-media
  • more-accurate-battery
  • v1.4.0
  • v1.3.0
  • v1.2.0
  • v1.2.0+rc1
  • v1.1.1
  • v1.1.0
  • v1.1.0+rc1
  • v1.0.0
  • v1.0.0+rc6
  • v1.0.0+rc5
  • v1.0.0+rc4
  • v1.0.0+rc3
  • v1.0.0+rc2
  • v1.0.0+rc1
35 results

main_menu.py

Blame
  • 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()