From a537d8eb65a0b85210504691ef420cfdb8d7f3b3 Mon Sep 17 00:00:00 2001 From: qwint Date: Fri, 29 Nov 2024 21:58:52 -0500 Subject: [PATCH] Launcher: support Component icons inside apworlds (#3629) * Add kivy overrides to allow AsyncImage source paths of the format ap:worlds.module/subpath/to/data.png that use pkgutil to load files from within an apworld * Apply suggestions from code review Co-authored-by: Doug Hoskisson * Apply suggestions from code review Co-authored-by: Doug Hoskisson * change original-load variable name for clarity per review * add comment to record pkgutil format * remove dependency on PIL * i hate typing --------- Co-authored-by: Doug Hoskisson --- Launcher.py | 7 +++---- kvui.py | 38 ++++++++++++++++++++++++++++++++++++ worlds/LauncherComponents.py | 1 + 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/Launcher.py b/Launcher.py index f04d67a5..0b8be232 100644 --- a/Launcher.py +++ b/Launcher.py @@ -246,9 +246,8 @@ refresh_components: Optional[Callable[[], None]] = None def run_gui(): - from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget + from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget, ApAsyncImage from kivy.core.window import Window - from kivy.uix.image import AsyncImage from kivy.uix.relativelayout import RelativeLayout class Launcher(App): @@ -281,8 +280,8 @@ def run_gui(): button.component = component button.bind(on_release=self.component_action) if component.icon != "icon": - image = AsyncImage(source=icon_paths[component.icon], - size=(38, 38), size_hint=(None, 1), pos=(5, 0)) + image = ApAsyncImage(source=icon_paths[component.icon], + size=(38, 38), size_hint=(None, 1), pos=(5, 0)) box_layout = RelativeLayout(size_hint_y=None, height=40) box_layout.add_widget(button) box_layout.add_widget(image) diff --git a/kvui.py b/kvui.py index dfe93593..d98fc7ed 100644 --- a/kvui.py +++ b/kvui.py @@ -3,6 +3,8 @@ import logging import sys import typing import re +import io +import pkgutil from collections import deque assert "kivy" not in sys.modules, "kvui should be imported before kivy for frozen compatibility" @@ -34,6 +36,7 @@ from kivy.app import App from kivy.core.window import Window from kivy.core.clipboard import Clipboard from kivy.core.text.markup import MarkupLabel +from kivy.core.image import ImageLoader, ImageLoaderBase, ImageData from kivy.base import ExceptionHandler, ExceptionManager from kivy.clock import Clock from kivy.factory import Factory @@ -61,6 +64,7 @@ from kivy.uix.recycleboxlayout import RecycleBoxLayout from kivy.uix.recycleview.layout import LayoutSelectionBehavior from kivy.animation import Animation from kivy.uix.popup import Popup +from kivy.uix.image import AsyncImage fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25) @@ -838,6 +842,40 @@ class HintLog(RecycleView): element.height = max_height +class ApAsyncImage(AsyncImage): + def is_uri(self, filename: str) -> bool: + if filename.startswith("ap:"): + return True + else: + return super().is_uri(filename) + + +class ImageLoaderPkgutil(ImageLoaderBase): + def load(self, filename: str) -> typing.List[ImageData]: + # take off the "ap:" prefix + module, path = filename[3:].split("/", 1) + data = pkgutil.get_data(module, path) + return self._bytes_to_data(data) + + def _bytes_to_data(self, data: typing.Union[bytes, bytearray]) -> typing.List[ImageData]: + loader = next(loader for loader in ImageLoader.loaders if loader.can_load_memory()) + return loader.load(loader, io.BytesIO(data)) + + +# grab the default loader method so we can override it but use it as a fallback +_original_image_loader_load = ImageLoader.load + + +def load_override(filename: str, default_load=_original_image_loader_load, **kwargs): + if filename.startswith("ap:"): + return ImageLoaderPkgutil(filename) + else: + return default_load(filename, **kwargs) + + +ImageLoader.load = load_override + + class E(ExceptionHandler): logger = logging.getLogger("Client") diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index 67806a73..7f178f17 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -207,6 +207,7 @@ components: List[Component] = [ ] +# if registering an icon from within an apworld, the format "ap:module.name/path/to/file.png" can be used icon_paths = { 'icon': local_path('data', 'icon.png'), 'mcicon': local_path('data', 'mcicon.png'),