Skip to content

Commit

Permalink
Launcher: support Component icons inside apworlds (#3629)
Browse files Browse the repository at this point in the history
* 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 <beauxq@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

* 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 <beauxq@users.noreply.github.com>
qwint and beauxq authored Nov 30, 2024
1 parent 845a604 commit a537d8e
Showing 3 changed files with 42 additions and 4 deletions.
7 changes: 3 additions & 4 deletions Launcher.py
Original file line number Diff line number Diff line change
@@ -246,9 +246,8 @@ def launch(exe, in_terminal=False):


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 build_button(component: Component) -> Widget:
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)
38 changes: 38 additions & 0 deletions kvui.py
Original file line number Diff line number Diff line change
@@ -3,6 +3,8 @@
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.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.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 @@ def fix_heights(self):
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")

1 change: 1 addition & 0 deletions worlds/LauncherComponents.py
Original file line number Diff line number Diff line change
@@ -207,6 +207,7 @@ def install_apworld(apworld_path: str = "") -> None:
]


# 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'),

0 comments on commit a537d8e

Please sign in to comment.