Core: KivyMD and Launcher overhaul (#3934)

Shifts the contents of `kvui.py`, and thus all CommonClient-based clients as well as Launcher, to using KivyMD. KivyMD is an extension for Kivy that is almost fully compatible with pre-existing Kivy components, while providing Material Design support for theming and overall visual design as well as useful pre-existing built in components such as Snackbars, Tooltips, and a built-in File Manager (not currently being used).

As a part of this shift, the launcher was completely overhauled, adding the ability to filter the list of components down to each type of component, the ability to define favorite components and filter to them, and add shortcuts for launcher components to the desktop. An optional description field was added to Component for display within the new launcher.

The theme (Light/Dark) and primary palette have also been exposed to users via client/user.kv.
This commit is contained in:
Silvris
2025-04-05 11:46:24 -05:00
committed by GitHub
parent c2d8f2443e
commit 503999cb32
11 changed files with 710 additions and 284 deletions

View File

@@ -1,5 +1,5 @@
"""
Archipelago launcher for bundled app.
Archipelago Launcher
* if run with APBP as argument, launch corresponding client.
* if run with executable as argument, run it passing argv[2:] as arguments
@@ -8,7 +8,7 @@ Archipelago launcher for bundled app.
Scroll down to components= to add components to the launcher as well as setup.py
"""
import os
import argparse
import itertools
import logging
@@ -20,10 +20,11 @@ import urllib.parse
import webbrowser
from os.path import isfile
from shutil import which
from typing import Callable, Optional, Sequence, Tuple, Union
from typing import Callable, Optional, Sequence, Tuple, Union, Any
if __name__ == "__main__":
import ModuleUpdate
ModuleUpdate.update()
import settings
@@ -105,7 +106,8 @@ components.extend([
Component("Generate Template Options", func=generate_yamls),
Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/")),
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
Component("Unrated/18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
Component("Unrated/18+ Discord Server", icon="discord",
func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
Component("Browse Files", func=browse_files),
])
@@ -114,7 +116,7 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
url = urllib.parse.urlparse(path)
queries = urllib.parse.parse_qs(url.query)
launch_args = (path, *launch_args)
client_component = None
client_component = []
text_client_component = None
if "game" in queries:
game = queries["game"][0]
@@ -122,49 +124,40 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
game = "Archipelago"
for component in components:
if component.supports_uri and component.game_name == game:
client_component = component
client_component.append(component)
elif component.display_name == "Text Client":
text_client_component = component
from kvui import MDButton, MDButtonText
from kivymd.uix.dialog import MDDialog, MDDialogHeadlineText, MDDialogContentContainer, MDDialogSupportingText
from kivymd.uix.divider import MDDivider
if client_component is None:
run_component(text_client_component, *launch_args)
return
else:
popup_text = MDDialogSupportingText(text="Select client to open and connect with.")
component_buttons = [MDDivider()]
for component in [text_client_component, *client_component]:
component_buttons.append(MDButton(
MDButtonText(text=component.display_name),
on_release=lambda *args, comp=component: run_component(comp, *launch_args),
style="text"
))
component_buttons.append(MDDivider())
from kvui import App, Button, BoxLayout, Label, Window
MDDialog(
# Headline
MDDialogHeadlineText(text="Connect to Multiworld"),
# Text
popup_text,
# Content
MDDialogContentContainer(
*component_buttons,
orientation="vertical"
),
class Popup(App):
def __init__(self):
self.title = "Connect to Multiworld"
self.icon = r"data/icon.png"
super().__init__()
def build(self):
layout = BoxLayout(orientation="vertical")
layout.add_widget(Label(text="Select client to open and connect with."))
button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4))
text_client_button = Button(
text=text_client_component.display_name,
on_release=lambda *args: run_component(text_client_component, *launch_args)
)
button_row.add_widget(text_client_button)
game_client_button = Button(
text=client_component.display_name,
on_release=lambda *args: run_component(client_component, *launch_args)
)
button_row.add_widget(game_client_button)
layout.add_widget(button_row)
return layout
def _stop(self, *largs):
# see run_gui Launcher _stop comment for details
self.root_window.close()
super()._stop(*largs)
Popup().run()
).open()
def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Component]]:
@@ -220,100 +213,163 @@ def launch(exe, in_terminal=False):
subprocess.Popen(exe)
def create_shortcut(button: Any, component: Component) -> None:
from pyshortcuts import make_shortcut
script = sys.argv[0]
wkdir = Utils.local_path()
script = f"{script} \"{component.display_name}\""
make_shortcut(script, name=f"Archipelago {component.display_name}", icon=local_path("data", "icon.ico"),
startmenu=False, terminal=False, working_dir=wkdir)
button.menu.dismiss()
refresh_components: Optional[Callable[[], None]] = None
def run_gui():
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget, ApAsyncImage
def run_gui(path: str, args: Any) -> None:
from kvui import (ThemedApp, MDFloatLayout, MDGridLayout, MDButton, MDLabel, MDButtonText, ScrollBox, ApAsyncImage)
from kivy.properties import ObjectProperty
from kivy.core.window import Window
from kivy.uix.relativelayout import RelativeLayout
from kivy.metrics import dp
from kivymd.uix.button import MDIconButton
from kivymd.uix.card import MDCard
from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.relativelayout import MDRelativeLayout
from kivymd.uix.snackbar import MDSnackbar, MDSnackbarText
class Launcher(App):
from kivy.lang.builder import Builder
class LauncherCard(MDCard):
component: Component | None
image: str
context_button: MDIconButton = ObjectProperty(None)
def __init__(self, *args, component: Component | None = None, image_path: str = "", **kwargs):
self.component = component
self.image = image_path
super().__init__(args, kwargs)
class Launcher(ThemedApp):
base_title: str = "Archipelago Launcher"
container: ContainerLayout
grid: GridLayout
_tool_layout: Optional[ScrollBox] = None
_client_layout: Optional[ScrollBox] = None
top_screen: MDFloatLayout = ObjectProperty(None)
navigation: MDGridLayout = ObjectProperty(None)
grid: MDGridLayout = ObjectProperty(None)
button_layout: ScrollBox = ObjectProperty(None)
cards: list[LauncherCard]
current_filter: Sequence[str | Type] | None
def __init__(self, ctx=None):
def __init__(self, ctx=None, path=None, args=None):
self.title = self.base_title + " " + Utils.__version__
self.ctx = ctx
self.icon = r"data/icon.png"
self.favorites = []
self.launch_uri = path
self.launch_args = args
self.cards = []
self.current_filter = (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC)
persistent = Utils.persistent_load()
if "launcher" in persistent:
if "favorites" in persistent["launcher"]:
self.favorites.extend(persistent["launcher"]["favorites"])
if "filter" in persistent["launcher"]:
if persistent["launcher"]["filter"]:
filters = []
for filter in persistent["launcher"]["filter"].split(", "):
if filter == "favorites":
filters.append(filter)
else:
filters.append(Type[filter])
self.current_filter = filters
super().__init__()
def _refresh_components(self) -> None:
def set_favorite(self, caller):
if caller.component.display_name in self.favorites:
self.favorites.remove(caller.component.display_name)
caller.icon = "star-outline"
else:
self.favorites.append(caller.component.display_name)
caller.icon = "star"
def build_button(component: Component) -> Widget:
def build_card(self, component: Component) -> LauncherCard:
"""
Builds a button widget for a given component.
Builds a card widget for a given component.
Args:
component (Component): The component associated with the button.
Returns:
None. The button is added to the parent grid layout.
:param component: The component associated with the button.
:return: The created Card Widget.
"""
button = Button(text=component.display_name, size_hint_y=None, height=40)
button.component = component
button.bind(on_release=self.component_action)
if component.icon != "icon":
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)
return box_layout
return button
button_card = LauncherCard(component=component,
image_path=icon_paths[component.icon])
def open_menu(caller):
caller.menu.open()
menu_items = [
{
"text": "Add shortcut on desktop",
"leading_icon": "laptop",
"on_release": lambda: create_shortcut(button_card.context_button, component)
}
]
button_card.context_button.menu = MDDropdownMenu(caller=button_card.context_button, items=menu_items)
button_card.context_button.bind(on_release=open_menu)
return button_card
def _refresh_components(self, type_filter: Sequence[str | Type] | None = None) -> None:
if not type_filter:
type_filter = [Type.CLIENT, Type.ADJUSTER, Type.TOOL, Type.MISC]
favorites = "favorites" in type_filter
# clear before repopulating
assert self._tool_layout and self._client_layout, "must call `build` first"
tool_children = reversed(self._tool_layout.layout.children)
assert self.button_layout, "must call `build` first"
tool_children = reversed(self.button_layout.layout.children)
for child in tool_children:
self._tool_layout.layout.remove_widget(child)
client_children = reversed(self._client_layout.layout.children)
for child in client_children:
self._client_layout.layout.remove_widget(child)
self.button_layout.layout.remove_widget(child)
_tools = {c.display_name: c for c in components if c.type == Type.TOOL}
_clients = {c.display_name: c for c in components if c.type == Type.CLIENT}
_adjusters = {c.display_name: c for c in components if c.type == Type.ADJUSTER}
_miscs = {c.display_name: c for c in components if c.type == Type.MISC}
cards = [card for card in self.cards if card.component.type in type_filter
or favorites and card.component.display_name in self.favorites]
for (tool, client) in itertools.zip_longest(itertools.chain(
_tools.items(), _miscs.items(), _adjusters.items()
), _clients.items()):
# column 1
if tool:
self._tool_layout.layout.add_widget(build_button(tool[1]))
# column 2
if client:
self._client_layout.layout.add_widget(build_button(client[1]))
self.current_filter = type_filter
for card in cards:
self.button_layout.layout.add_widget(card)
def filter_clients(self, caller):
self._refresh_components(caller.type)
def build(self):
self.container = ContainerLayout()
self.grid = GridLayout(cols=2)
self.container.add_widget(self.grid)
self.grid.add_widget(Label(text="General", size_hint_y=None, height=40))
self.grid.add_widget(Label(text="Clients", size_hint_y=None, height=40))
self._tool_layout = ScrollBox()
self._tool_layout.layout.orientation = "vertical"
self.grid.add_widget(self._tool_layout)
self._client_layout = ScrollBox()
self._client_layout.layout.orientation = "vertical"
self.grid.add_widget(self._client_layout)
self._refresh_components()
self.top_screen = Builder.load_file(Utils.local_path("data/launcher.kv"))
self.grid = self.top_screen.ids.grid
self.navigation = self.top_screen.ids.navigation
self.button_layout = self.top_screen.ids.button_layout
self.set_colors()
self.top_screen.md_bg_color = self.theme_cls.backgroundColor
global refresh_components
refresh_components = self._refresh_components
Window.bind(on_drop_file=self._on_drop_file)
return self.container
for component in components:
self.cards.append(self.build_card(component))
self._refresh_components(self.current_filter)
return self.top_screen
def on_start(self):
if self.launch_uri:
handle_uri(self.launch_uri, self.launch_args)
self.launch_uri = None
self.launch_args = None
@staticmethod
def component_action(button):
MDSnackbar(MDSnackbarText(text="Opening in a new window..."), y=dp(24), pos_hint={"center_x": 0.5},
size_hint_x=0.5).open()
if button.component.func:
button.component.func()
else:
@@ -333,7 +389,13 @@ def run_gui():
self.root_window.close()
super()._stop(*largs)
Launcher().run()
def on_stop(self):
Utils.persistent_store("launcher", "favorites", self.favorites)
Utils.persistent_store("launcher", "filter", ", ".join(filter.name if isinstance(filter, Type) else filter
for filter in self.current_filter))
super().on_stop()
Launcher(path=path, args=args).run()
# avoiding Launcher reference leak
# and don't try to do something with widgets after window closed
@@ -360,9 +422,7 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
path = args.get("Patch|Game|Component|url", None)
if path is not None:
if path.startswith("archipelago://"):
handle_uri(path, args.get("args", ()))
return
if not path.startswith("archipelago://"):
file, component = identify(path)
if file:
args['file'] = file
@@ -378,7 +438,7 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
elif "component" in args:
run_component(args["component"], *args["args"])
elif not args["update_settings"]:
run_gui()
run_gui(path, args.get("args", ()))
if __name__ == '__main__':
@@ -400,6 +460,7 @@ if __name__ == '__main__':
main(parser.parse_args())
from worlds.LauncherComponents import processes
for process in processes:
# we await all child processes to close before we tear down the process host
# this makes it feel like each one is its own program, as the Launcher is closed now

View File

@@ -529,9 +529,7 @@ class LinksAwakeningContext(CommonContext):
def run_gui(self) -> None:
import webbrowser
import kvui
from kvui import Button, GameManager
from kivy.uix.image import Image
from kvui import GameManager, ImageButton
class LADXManager(GameManager):
logging_pairs = [
@@ -544,16 +542,10 @@ class LinksAwakeningContext(CommonContext):
b = super().build()
if self.ctx.magpie_enabled:
button = Button(text="", size=(30, 30), size_hint_x=None,
button = ImageButton(texture=magpie_logo(), fit_mode="cover", image_size=(32, 32), size_hint_x=None,
on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1'))
image = Image(size=(16, 16), texture=magpie_logo())
button.add_widget(image)
def set_center(_, center):
image.center = center
button.bind(center=set_center)
self.connect_layout.add_widget(button)
return b
self.ui = LADXManager(self)

View File

@@ -214,17 +214,11 @@ class WargrooveContext(CommonContext):
def run_gui(self):
"""Import kivy UI system and start running it as self.ui_task."""
from kvui import GameManager, HoverBehavior, ServerToolTip
from kivy.uix.tabbedpanel import TabbedPanelItem
from kivymd.uix.tab import MDTabsItem, MDTabsItemText
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.uix.togglebutton import ToggleButton
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.image import AsyncImage, Image
from kivy.uix.stacklayout import StackLayout
from kivy.uix.label import Label
from kivy.properties import ColorProperty
from kivy.uix.image import Image
import pkgutil
class TrackerLayout(BoxLayout):

View File

@@ -14,23 +14,50 @@
salmon: "FA8072" # typically trap item
white: "FFFFFF" # not used, if you want to change the generic text color change color in Label
orange: "FF7700" # Used for command echo
<Label>:
color: "FFFFFF"
<TabbedPanel>:
tab_width: root.width / app.tab_count
# KivyMD theming parameters
theme_style: "Dark" # Light/Dark
primary_palette: "Green" # Many options
dynamic_scheme_name: "TONAL_SPOT"
<MDLabel>:
color: self.theme_cls.primaryColor
<TooltipLabel>:
text_size: self.width, None
size_hint_y: None
height: self.texture_size[1]
adaptive_height: True
font_size: dp(20)
markup: True
halign: "left"
<SelectableLabel>:
size_hint: 1, None
canvas.before:
Color:
rgba: (.0, 0.9, .1, .3) if self.selected else (0.2, 0.2, 0.2, 1)
rgba: (.0, 0.9, .1, .3) if self.selected else self.theme_cls.surfaceContainerLowColor
Rectangle:
size: self.size
pos: self.pos
<MarkupDropdownItem>
orientation: "vertical"
MDLabel:
text: root.text
valign: "center"
padding_x: "12dp"
shorten: True
shorten_from: "right"
theme_text_color: "Custom"
markup: True
text_color:
app.theme_cls.onSurfaceVariantColor \
if not root.text_color else \
root.text_color
MDDivider:
md_bg_color:
( \
app.theme_cls.outlineVariantColor \
if not root.divider_color \
else root.divider_color \
) \
if root.divider else \
(0, 0, 0, 0)
<UILog>:
messages: 1000 # amount of messages stored in client logs.
cols: 1
@@ -49,7 +76,7 @@
<HintLabel>:
canvas.before:
Color:
rgba: (.0, 0.9, .1, .3) if self.selected else (0.2, 0.2, 0.2, 1) if self.striped else (0.18, 0.18, 0.18, 1)
rgba: (.0, 0.9, .1, .3) if self.selected else self.theme_cls.surfaceContainerHighColor if self.striped else self.theme_cls.surfaceContainerLowColor
Rectangle:
size: self.size
pos: self.pos
@@ -152,3 +179,15 @@
height: dp(30)
multiline: False
write_tab: False
<ScrollBox>:
layout: layout
bar_width: "12dp"
scroll_wheel_distance: 40
do_scroll_x: False
MDBoxLayout:
id: layout
orientation: "vertical"
spacing: 10
size_hint_y: None
height: self.minimum_height

142
data/launcher.kv Normal file
View File

@@ -0,0 +1,142 @@
<LauncherCard>:
id: main
style: "filled"
padding: "4dp"
size_hint: 1, None
height: "75dp"
context_button: context
MDRelativeLayout:
ApAsyncImage:
source: main.image
size: (40, 40)
size_hint_y: None
pos_hint: {"center_x": 0.1, "center_y": 0.5}
MDLabel:
text: main.component.display_name
pos_hint:{"center_x": 0.5, "center_y": 0.85 if main.component.description else 0.65}
halign: "center"
font_style: "Title"
role: "medium"
theme_text_color: "Custom"
text_color: app.theme_cls.primaryColor
MDLabel:
text: main.component.description
pos_hint: {"center_x": 0.5, "center_y": 0.35}
halign: "center"
role: "small"
theme_text_color: "Custom"
text_color: app.theme_cls.primaryColor
MDIconButton:
component: main.component
icon: "star" if self.component.display_name in app.favorites else "star-outline"
style: "standard"
pos_hint:{"center_x": 0.85, "center_y": 0.8}
theme_text_color: "Custom"
text_color: app.theme_cls.primaryColor
on_release: app.set_favorite(self)
MDIconButton:
id: context
icon: "menu"
style: "standard"
pos_hint:{"center_x": 0.95, "center_y": 0.8}
theme_text_color: "Custom"
text_color: app.theme_cls.primaryColor
MDButton:
pos_hint:{"center_x": 0.9, "center_y": 0.25}
size_hint_y: None
height: "25dp"
component: main.component
on_release: app.component_action(self)
MDButtonText:
text: "Open"
#:import Type worlds.LauncherComponents.Type
MDFloatLayout:
id: top_screen
MDGridLayout:
id: grid
cols: 2
spacing: "5dp"
padding: "10dp"
MDGridLayout:
id: navigation
cols: 1
size_hint_x: 0.25
MDButton:
id: all
style: "text"
type: (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC)
on_release: app.filter_clients(self)
MDButtonIcon:
icon: "asterisk"
MDButtonText:
text: "All"
MDButton:
id: client
style: "text"
type: (Type.CLIENT, )
on_release: app.filter_clients(self)
MDButtonIcon:
icon: "controller"
MDButtonText:
text: "Client"
MDButton:
id: Tool
style: "text"
type: (Type.TOOL, )
on_release: app.filter_clients(self)
MDButtonIcon:
icon: "desktop-classic"
MDButtonText:
text: "Tool"
MDButton:
id: adjuster
style: "text"
type: (Type.ADJUSTER, )
on_release: app.filter_clients(self)
MDButtonIcon:
icon: "wrench"
MDButtonText:
text: "Adjuster"
MDButton:
id: misc
style: "text"
type: (Type.MISC, )
on_release: app.filter_clients(self)
MDButtonIcon:
icon: "dots-horizontal-circle-outline"
MDButtonText:
text: "Misc"
MDButton:
id: favorites
style: "text"
type: ("favorites", )
on_release: app.filter_clients(self)
MDButtonIcon:
icon: "star"
MDButtonText:
text: "Favorites"
MDNavigationDrawerDivider:
ScrollBox:
id: button_layout

458
kvui.py
View File

@@ -35,8 +35,7 @@ from kivy.config import Config
Config.set("input", "mouse", "mouse,disable_multitouch")
Config.set("kivy", "exit_on_escape", "0")
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
from kivy.app import App
from kivymd.uix.divider import MDDivider
from kivy.core.window import Window
from kivy.core.clipboard import Clipboard
from kivy.core.text.markup import MarkupLabel
@@ -46,30 +45,32 @@ from kivy.clock import Clock
from kivy.factory import Factory
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
from kivy.metrics import dp
from kivy.effects.scroll import ScrollEffect
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.layout import Layout
from kivy.uix.textinput import TextInput
from kivy.uix.scrollview import ScrollView
from kivy.uix.recycleview import RecycleView
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label
from kivy.uix.progressbar import ProgressBar
from kivy.uix.dropdown import DropDown
from kivy.utils import escape_markup
from kivy.lang import Builder
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.behaviors import FocusBehavior, ToggleButtonBehavior
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.dropdown import DropDown
from kivy.uix.image import AsyncImage
from kivymd.app import MDApp
from kivymd.uix.gridlayout import MDGridLayout
from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.tab.tab import MDTabsPrimary, MDTabsItem, MDTabsItemText, MDTabsCarousel
from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.menu.menu import MDDropdownTextItem
from kivymd.uix.dropdownitem import MDDropDownItem, MDDropDownItemText
from kivymd.uix.button import MDButton, MDButtonText, MDButtonIcon, MDIconButton
from kivymd.uix.label import MDLabel, MDIcon
from kivymd.uix.recycleview import MDRecycleView
from kivymd.uix.textfield.textfield import MDTextField
from kivymd.uix.progressindicator import MDLinearProgressIndicator
from kivymd.uix.scrollview import MDScrollView
from kivymd.uix.tooltip import MDTooltip, MDTooltipPlain
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
@@ -86,6 +87,85 @@ else:
remove_between_brackets = re.compile(r"\[.*?]")
class ThemedApp(MDApp):
def set_colors(self):
text_colors = KivyJSONtoTextParser.TextColors()
self.theme_cls.theme_style = getattr(text_colors, "theme_style", "Dark")
self.theme_cls.primary_palette = getattr(text_colors, "primary_palette", "Green")
self.theme_cls.dynamic_scheme_name = getattr(text_colors, "dynamic_scheme_name", "TONAL_SPOT")
self.theme_cls.dynamic_scheme_contrast = 0.0
class ImageIcon(MDButtonIcon, AsyncImage):
def __init__(self, *args, **kwargs):
super().__init__(args, kwargs)
self.image = AsyncImage(**kwargs)
self.add_widget(self.image)
def add_widget(self, widget, index=0, canvas=None):
return super(MDIcon, self).add_widget(widget)
class ImageButton(MDIconButton):
def __init__(self, **kwargs):
image_args = dict()
for kwarg in ("fit_mode", "image_size", "color", "source", "texture"):
val = kwargs.pop(kwarg, "None")
if val != "None":
image_args[kwarg.replace("image_", "")] = val
super().__init__()
self.image = AsyncImage(**image_args)
def set_center(button, center):
self.image.center_x = self.center_x
self.image.center_y = self.center_y
self.bind(center=set_center)
self.add_widget(self.image)
def add_widget(self, widget, index=0, canvas=None):
return super(MDIcon, self).add_widget(widget)
class ScrollBox(MDScrollView):
layout: MDBoxLayout = ObjectProperty(None)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# thanks kivymd
class ToggleButton(MDButton, ToggleButtonBehavior):
def __init__(self, *args, **kwargs):
super(ToggleButton, self).__init__(*args, **kwargs)
self.bind(state=self._update_bg)
def _update_bg(self, _, state: str):
if self.disabled:
return
if self.theme_bg_color == "Primary":
self.theme_bg_color = "Custom"
if state == "down":
self.md_bg_color = self.theme_cls.primaryColor
for child in self.children:
if child.theme_text_color == "Primary":
child.theme_text_color = "Custom"
if child.theme_icon_color == "Primary":
child.theme_icon_color = "Custom"
child.text_color = self.theme_cls.onPrimaryColor
child.icon_color = self.theme_cls.onPrimaryColor
else:
self.md_bg_color = self.theme_cls.surfaceContainerLowestColor
for child in self.children:
if child.theme_text_color == "Primary":
child.theme_text_color = "Custom"
if child.theme_icon_color == "Primary":
child.theme_icon_color = "Custom"
child.text_color = self.theme_cls.primaryColor
child.icon_color = self.theme_cls.primaryColor
# I was surprised to find this didn't already exist in kivy :(
class HoverBehavior(object):
"""originally from https://stackoverflow.com/a/605348110"""
@@ -125,7 +205,7 @@ class HoverBehavior(object):
Factory.register("HoverBehavior", HoverBehavior)
class ToolTip(Label):
class ToolTip(MDTooltipPlain):
pass
@@ -133,49 +213,30 @@ class ServerToolTip(ToolTip):
pass
class ScrollBox(ScrollView):
layout: BoxLayout
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.layout = BoxLayout(size_hint_y=None)
self.layout.bind(minimum_height=self.layout.setter("height"))
self.add_widget(self.layout)
self.effect_cls = ScrollEffect
self.bar_width = dp(12)
self.scroll_type = ["content", "bars"]
class HovererableLabel(HoverBehavior, Label):
class HovererableLabel(HoverBehavior, MDLabel):
pass
class TooltipLabel(HovererableLabel):
tooltip = None
class TooltipLabel(HovererableLabel, MDTooltip):
tooltip_display_delay = 0.1
def create_tooltip(self, text, x, y):
text = text.replace("<br>", "\n").replace("&amp;", "&").replace("&bl;", "[").replace("&br;", "]")
if self.tooltip:
# update
self.tooltip.children[0].text = text
else:
self.tooltip = FloatLayout()
tooltip_label = ToolTip(text=text)
self.tooltip.add_widget(tooltip_label)
fade_in_animation.start(self.tooltip)
App.get_running_app().root.add_widget(self.tooltip)
# handle left-side boundary to not render off-screen
x = max(x, 3 + self.tooltip.children[0].texture_size[0] / 2)
# position float layout
self.tooltip.x = x - self.tooltip.width / 2
self.tooltip.y = y - self.tooltip.height / 2 + 48
center_x, center_y = self.to_window(self.center_x, self.center_y)
self.shift_y = y - center_y
shift_x = center_x - x
if shift_x > 0:
self.shift_left = shift_x
else:
self.shift_right = shift_x
def remove_tooltip(self):
if self.tooltip:
App.get_running_app().root.remove_widget(self.tooltip)
self.tooltip = None
if self._tooltip:
# update
self._tooltip.text = text
else:
self._tooltip = ToolTip(text=text, pos_hint={})
self.display_tooltip()
def on_mouse_pos(self, window, pos):
if not self.get_root_window():
@@ -202,26 +263,26 @@ class TooltipLabel(HovererableLabel):
def on_leave(self):
self.remove_tooltip()
self._tooltip = None
class ServerLabel(HovererableLabel):
class ServerLabel(HovererableLabel, MDTooltip):
tooltip_display_delay = 0.1
def __init__(self, *args, **kwargs):
super(HovererableLabel, self).__init__(*args, **kwargs)
self.layout = FloatLayout()
self.popuplabel = ServerToolTip(text="Test")
self.layout.add_widget(self.popuplabel)
self._tooltip = ServerToolTip(text="Test")
def on_enter(self):
self.popuplabel.text = self.get_text()
App.get_running_app().root.add_widget(self.layout)
fade_in_animation.start(self.layout)
self._tooltip.text = self.get_text()
self.display_tooltip()
def on_leave(self):
App.get_running_app().root.remove_widget(self.layout)
self.animation_tooltip_dismiss()
@property
def ctx(self) -> context_type:
return App.get_running_app().ctx
return MDApp.get_running_app().ctx
def get_text(self):
if self.ctx.server:
@@ -262,11 +323,11 @@ class ServerLabel(HovererableLabel):
return "No current server connection. \nPlease connect to an Archipelago server."
class MainLayout(GridLayout):
class MainLayout(MDGridLayout):
pass
class ContainerLayout(FloatLayout):
class ContainerLayout(MDFloatLayout):
pass
@@ -286,6 +347,11 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
return super(SelectableLabel, self).refresh_view_attrs(
rv, index, data)
def on_size(self, instance_label, size: list) -> None:
super().on_size(instance_label, size)
if self.parent:
self.width = self.parent.width
def on_touch_down(self, touch):
""" Add selection on touch down """
if super(SelectableLabel, self).on_touch_down(touch):
@@ -297,9 +363,9 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
# Not a fan of the following few lines, but they work.
temp = MarkupLabel(text=self.text).markup
text = "".join(part for part in temp if not part.startswith("["))
cmdinput = App.get_running_app().textinput
cmdinput = MDApp.get_running_app().textinput
if not cmdinput.text:
input_text = get_input_text_from_response(text, App.get_running_app().last_autofillable_command)
input_text = get_input_text_from_response(text, MDApp.get_running_app().last_autofillable_command)
if input_text is not None:
cmdinput.text = input_text
@@ -311,29 +377,114 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
self.selected = is_selected
class AutocompleteHintInput(TextInput):
class MarkupDropdownTextItem(MDDropdownTextItem):
def __init__(self):
super().__init__()
for child in self.children:
if child.__class__ == MDLabel:
child.markup = True
print(self.text)
# Currently, this only lets us do markup on text that does not have any icons
# Create new TextItems as needed
class MarkupDropdown(MDDropdownMenu):
def on_items(self, instance, value: list) -> None:
"""
The method sets the class that will be used to create the menu item.
"""
items = []
viewclass = "MarkupDropdownTextItem"
for data in value:
if "viewclass" not in data:
if (
"leading_icon" not in data
and "trailing_icon" not in data
and "trailing_text" not in data
):
viewclass = "MarkupDropdownTextItem"
elif (
"leading_icon" in data
and "trailing_icon" not in data
and "trailing_text" not in data
):
viewclass = "MDDropdownLeadingIconItem"
elif (
"leading_icon" not in data
and "trailing_icon" in data
and "trailing_text" not in data
):
viewclass = "MDDropdownTrailingIconItem"
elif (
"leading_icon" not in data
and "trailing_icon" in data
and "trailing_text" in data
):
viewclass = "MDDropdownTrailingIconTextItem"
elif (
"leading_icon" in data
and "trailing_icon" in data
and "trailing_text" in data
):
viewclass = "MDDropdownLeadingTrailingIconTextItem"
elif (
"leading_icon" in data
and "trailing_icon" in data
and "trailing_text" not in data
):
viewclass = "MDDropdownLeadingTrailingIconItem"
elif (
"leading_icon" not in data
and "trailing_icon" not in data
and "trailing_text" in data
):
viewclass = "MDDropdownTrailingTextItem"
elif (
"leading_icon" in data
and "trailing_icon" not in data
and "trailing_text" in data
):
viewclass = "MDDropdownLeadingIconTrailingTextItem"
data["viewclass"] = viewclass
if "height" not in data:
data["height"] = dp(48)
items.append(data)
self._items = items
# Update items in view
if hasattr(self, "menu"):
self.menu.data = self._items
class AutocompleteHintInput(MDTextField):
min_chars = NumericProperty(3)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.dropdown = DropDown()
self.dropdown = MarkupDropdown(caller=self, position="bottom", border_margin=dp(24), width=self.width)
self.dropdown.bind(on_select=lambda instance, x: setattr(self, 'text', x))
self.bind(on_text_validate=self.on_message)
self.bind(width=lambda instance, x: setattr(self.dropdown, "width", x))
def on_message(self, instance):
App.get_running_app().commandprocessor("!hint "+instance.text)
MDApp.get_running_app().commandprocessor("!hint "+instance.text)
def on_text(self, instance, value):
if len(value) >= self.min_chars:
self.dropdown.clear_widgets()
ctx: context_type = App.get_running_app().ctx
self.dropdown.items.clear()
ctx: context_type = MDApp.get_running_app().ctx
if not ctx.game:
return
item_names = ctx.item_names._game_store[ctx.game].values()
def on_press(button: Button):
split_text = MarkupLabel(text=button.text).markup
def on_press(text):
split_text = MarkupLabel(text=text).markup
return self.dropdown.select("".join(text_frag for text_frag in split_text
if not text_frag.startswith("[")))
lowered = value.lower()
@@ -345,20 +496,29 @@ class AutocompleteHintInput(TextInput):
else:
text = escape_markup(item_name)
text = text[:index] + "[b]" + text[index:index+len(value)]+"[/b]"+text[index+len(value):]
btn = Button(text=text, size_hint_y=None, height=dp(30), markup=True)
btn.bind(on_release=on_press)
self.dropdown.add_widget(btn)
if not self.dropdown.attach_to:
self.dropdown.open(self)
self.dropdown.items.append({
"text": text,
"on_release": lambda: on_press(text),
"markup": True
})
if not self.dropdown.parent:
self.dropdown.open()
else:
self.dropdown.dismiss()
class HintLabel(RecycleDataViewBehavior, BoxLayout):
status_icons = {
HintStatus.HINT_NO_PRIORITY: "information",
HintStatus.HINT_PRIORITY: "exclamation-thick",
HintStatus.HINT_AVOID: "alert"
}
class HintLabel(RecycleDataViewBehavior, MDBoxLayout):
selected = BooleanProperty(False)
striped = BooleanProperty(False)
index = None
dropdown: DropDown
dropdown: MDDropdownMenu
def __init__(self):
super(HintLabel, self).__init__()
@@ -369,29 +529,28 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
self.entrance_text = ""
self.status_text = ""
self.hint = {}
for child in self.children:
child.bind(texture_size=self.set_height)
ctx = MDApp.get_running_app().ctx
menu_items = []
ctx = App.get_running_app().ctx
self.dropdown = DropDown()
for status in (HintStatus.HINT_NO_PRIORITY, HintStatus.HINT_PRIORITY, HintStatus.HINT_AVOID):
name = status_names[status]
status_button = MDDropDownItem(MDDropDownItemText(text=name), size_hint_y=None, height=dp(50))
status_button.status = status
menu_items.append({
"text": name,
"leading_icon": status_icons[status],
"on_release": lambda x=status: select(self, x)
})
def set_value(button):
self.dropdown.select(button.status)
self.dropdown = MDDropdownMenu(caller=self.ids["status"], items=menu_items)
def select(instance, data):
ctx.update_hint(self.hint["location"],
self.hint["finding_player"],
data)
for status in (HintStatus.HINT_NO_PRIORITY, HintStatus.HINT_PRIORITY, HintStatus.HINT_AVOID):
name = status_names[status]
status_button = Button(text=name, size_hint_y=None, height=dp(50))
status_button.status = status
status_button.bind(on_release=set_value)
self.dropdown.add_widget(status_button)
self.dropdown.bind(on_select=select)
self.dropdown.bind(on_release=self.dropdown.dismiss)
def set_height(self, instance, value):
self.height = max([child.texture_size[1] for child in self.children])
@@ -406,7 +565,6 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
self.entrance_text = data["entrance"]["text"]
self.status_text = data["status"]["text"]
self.hint = data["status"]["hint"]
self.height = self.minimum_height
return super(HintLabel, self).refresh_view_attrs(rv, index, data)
def on_touch_down(self, touch):
@@ -419,10 +577,10 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
if status_label.collide_point(*touch.pos):
if self.hint["status"] == HintStatus.HINT_FOUND:
return
ctx = App.get_running_app().ctx
ctx = MDApp.get_running_app().ctx
if ctx.slot_concerns_self(self.hint["receiving_player"]): # If this player owns this hint
# open a dropdown
self.dropdown.open(self.ids["status"])
self.dropdown.open()
elif self.selected:
self.parent.clear_selection()
else:
@@ -455,7 +613,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
else:
parent.sort_key = key
parent.reversed = False
App.get_running_app().update_hints()
MDApp.get_running_app().update_hints()
def apply_selection(self, rv, index, is_selected):
""" Respond to the selection of items in the view. """
@@ -463,7 +621,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
self.selected = is_selected
class ConnectBarTextInput(TextInput):
class ConnectBarTextInput(MDTextField):
def insert_text(self, substring, from_undo=False):
s = substring.replace("\n", "").replace("\r", "")
return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo)
@@ -473,7 +631,7 @@ def is_command_input(string: str) -> bool:
return len(string) > 0 and string[0] in "/!"
class CommandPromptTextInput(TextInput):
class CommandPromptTextInput(MDTextField):
MAXIMUM_HISTORY_MESSAGES = 50
def __init__(self, **kwargs) -> None:
@@ -521,7 +679,7 @@ class CommandPromptTextInput(TextInput):
class MessageBox(Popup):
class MessageBoxLabel(Label):
class MessageBoxLabel(MDLabel):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._label.refresh()
@@ -539,14 +697,31 @@ class MessageBox(Popup):
self.height += max(0, label.height - 18)
class GameManager(App):
class ClientTabs(MDTabsPrimary):
carousel: MDTabsCarousel
lock_swiping = True
def __init__(self, *args, **kwargs):
self.carousel = MDTabsCarousel(lock_swiping=True)
super().__init__(*args, MDDivider(size_hint_y=None, height=dp(4)), self.carousel, **kwargs)
self.size_hint_y = 1
def remove_tab(self, tab, content=None):
if content is None:
content = tab.content
self.ids.container.remove_widget(tab)
self.carousel.remove_widget(content)
self.on_size(self, self.size)
class GameManager(ThemedApp):
logging_pairs = [
("Client", "Archipelago"),
]
base_title: str = "Archipelago Client"
last_autofillable_command: str
main_area_container: GridLayout
main_area_container: MDGridLayout
""" subclasses can add more columns beside the tabs """
def __init__(self, ctx: context_type):
@@ -581,18 +756,26 @@ class GameManager(App):
return max(1, len(self.tabs.tab_list))
return 1
def on_start(self):
def on_start(*args):
self.root.md_bg_color = self.theme_cls.backgroundColor
super().on_start()
Clock.schedule_once(on_start)
def build(self) -> Layout:
self.set_colors()
self.container = ContainerLayout()
self.grid = MainLayout()
self.grid.cols = 1
self.connect_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
self.connect_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(70),
spacing=5, padding=(5, 10))
# top part
server_label = ServerLabel()
server_label = ServerLabel(halign="center")
self.connect_layout.add_widget(server_label)
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:",
size_hint_y=None,
height=dp(30), multiline=False, write_tab=False)
size_hint_y=None, role="medium",
height=dp(70), multiline=False, write_tab=False)
def connect_bar_validate(sender):
if not self.ctx.server:
@@ -600,26 +783,31 @@ class GameManager(App):
self.server_connect_bar.bind(on_text_validate=connect_bar_validate)
self.connect_layout.add_widget(self.server_connect_bar)
self.server_connect_button = Button(text="Connect", size=(dp(100), dp(30)), size_hint_y=None, size_hint_x=None)
self.server_connect_button = MDButton(MDButtonText(text="Connect"), style="filled", size=(dp(100), dp(70)),
size_hint_x=None, size_hint_y=None, radius=5, pos_hint={"center_y": 0.55})
self.server_connect_button.bind(on_press=self.connect_button_action)
self.server_connect_button.height = self.server_connect_bar.height
self.connect_layout.add_widget(self.server_connect_button)
self.grid.add_widget(self.connect_layout)
self.progressbar = ProgressBar(size_hint_y=None, height=3)
self.progressbar = MDLinearProgressIndicator(size_hint_y=None, height=3)
self.grid.add_widget(self.progressbar)
# middle part
self.tabs = TabbedPanel(size_hint_y=1)
self.tabs.default_tab_text = "All"
self.tabs = ClientTabs()
self.tabs.add_widget(MDTabsItem(MDTabsItemText(text="All" if len(self.logging_pairs) > 1 else "Archipelago")))
self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name)
for logger_name, name in
self.logging_pairs))
self.tabs.carousel.add_widget(self.tabs.default_tab_content)
for logger_name, display_name in self.logging_pairs:
bridge_logger = logging.getLogger(logger_name)
panel = TabbedPanelItem(text=display_name)
self.log_panels[display_name] = panel.content = UILog(bridge_logger)
self.log_panels[display_name] = UILog(bridge_logger)
if len(self.logging_pairs) > 1:
panel = MDTabsItem(MDTabsItemText(text=display_name))
panel.content = self.log_panels[display_name]
# show Archipelago tab if other logging is present
self.tabs.carousel.add_widget(panel.content)
self.tabs.add_widget(panel)
hint_panel = self.add_client_tab("Hints", HintLayout())
@@ -627,21 +815,20 @@ class GameManager(App):
self.log_panels["Hints"] = hint_panel.content
hint_panel.content.add_widget(self.hint_log)
if len(self.logging_pairs) == 1:
self.tabs.default_tab_text = "Archipelago"
self.main_area_container = GridLayout(size_hint_y=1, rows=1)
self.main_area_container = MDGridLayout(size_hint_y=1, rows=1)
self.main_area_container.add_widget(self.tabs)
self.grid.add_widget(self.main_area_container)
# bottom part
bottom_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
info_button = Button(size=(dp(100), dp(30)), text="Command:", size_hint_x=None)
bottom_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(70), spacing=5, padding=(5, 10))
info_button = MDButton(MDButtonText(text="Command:"), radius=5, style="filled", size=(dp(100), dp(70)),
size_hint_x=None, size_hint_y=None, pos_hint={"center_y": 0.575})
info_button.bind(on_release=self.command_button_action)
bottom_layout.add_widget(info_button)
self.textinput = CommandPromptTextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
self.textinput.bind(on_text_validate=self.on_message)
info_button.height = self.textinput.height
self.textinput.text_validate_unfocus = False
bottom_layout.add_widget(self.textinput)
self.grid.add_widget(bottom_layout)
@@ -662,24 +849,26 @@ class GameManager(App):
def add_client_tab(self, title: str, content: Widget) -> Widget:
"""Adds a new tab to the client window with a given title, and provides a given Widget as its content.
Returns the new tab widget, with the provided content being placed on the tab as content."""
new_tab = TabbedPanelItem(text=title)
new_tab = MDTabsItem(MDTabsItemText(text=title))
new_tab.content = content
self.tabs.add_widget(new_tab)
self.tabs.carousel.add_widget(new_tab.content)
return new_tab
def update_texts(self, dt):
if hasattr(self.tabs.content.children[0], "fix_heights"):
self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream
for slide in self.tabs.carousel.slides:
if hasattr(slide, "fix_heights"):
slide.fix_heights() # TODO: remove this when Kivy fixes this upstream
if self.ctx.server:
self.title = self.base_title + " " + Utils.__version__ + \
f" | Connected to: {self.ctx.server_address} " \
f"{'.'.join(str(e) for e in self.ctx.server_version)}"
self.server_connect_button.text = "Disconnect"
self.server_connect_button._button_text.text = "Disconnect"
self.server_connect_bar.readonly = True
self.progressbar.max = len(self.ctx.checked_locations) + len(self.ctx.missing_locations)
self.progressbar.value = len(self.ctx.checked_locations)
else:
self.server_connect_button.text = "Connect"
self.server_connect_button._button_text.text = "Connect"
self.server_connect_bar.readonly = False
self.title = self.base_title + " " + Utils.__version__
self.progressbar.value = 0
@@ -742,8 +931,8 @@ class GameManager(App):
def enable_energy_link(self):
if not hasattr(self, "energy_link_label"):
self.energy_link_label = Label(text="Energy Link: Standby",
size_hint_x=None, width=150)
self.energy_link_label = MDLabel(text="Energy Link: Standby",
size_hint_x=None, width=150, halign="center")
self.connect_layout.add_widget(self.energy_link_label)
def set_new_energy_link_value(self):
@@ -779,8 +968,9 @@ class LogtoUI(logging.Handler):
self.on_log(self.format(record))
class UILog(RecycleView):
class UILog(MDRecycleView):
messages: typing.ClassVar[int] # comes from kv file
adaptive_height = True
def __init__(self, *loggers_to_handle, **kwargs):
super(UILog, self).__init__(**kwargs)
@@ -807,13 +997,13 @@ class UILog(RecycleView):
element.height = element.texture_size[1]
class HintLayout(BoxLayout):
class HintLayout(MDBoxLayout):
orientation = "vertical"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
boxlayout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
boxlayout.add_widget(Label(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(30)))
boxlayout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(55))
boxlayout.add_widget(MDLabel(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(55)))
boxlayout.add_widget(AutocompleteHintInput())
self.add_widget(boxlayout)
@@ -846,8 +1036,7 @@ status_sort_weights: dict[HintStatus, int] = {
HintStatus.HINT_PRIORITY: 4,
}
class HintLog(RecycleView):
class HintLog(MDRecycleView):
header = {
"receiving": {"text": "[u]Receiving Player[/u]"},
"item": {"text": "[u]Item[/u]"},
@@ -858,7 +1047,7 @@ class HintLog(RecycleView):
"hint": {"receiving_player": -1, "location": -1, "finding_player": -1, "status": ""}},
"striped": True,
}
data: list[typing.Any]
sort_key: str = ""
reversed: bool = True
@@ -871,7 +1060,7 @@ class HintLog(RecycleView):
if not hints: # Fix the scrolling looking visually wrong in some edge cases
self.scroll_y = 1.0
data = []
ctx = App.get_running_app().ctx
ctx = MDApp.get_running_app().ctx
for hint in hints:
if not hint.get("status"): # Allows connecting to old servers
hint["status"] = HintStatus.HINT_FOUND if hint["found"] else HintStatus.HINT_UNSPECIFIED
@@ -935,7 +1124,8 @@ class ImageLoaderPkgutil(ImageLoaderBase):
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]:
@staticmethod
def _bytes_to_data(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))

View File

@@ -12,3 +12,6 @@ cython>=3.0.12
cymem>=2.0.11
orjson>=3.10.15
typing_extensions>=4.12.2
pyshortcuts>=1.9.1
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
kivymd>=2.0.1.dev0

View File

@@ -629,12 +629,13 @@ cx_Freeze.setup(
ext_modules=cythonize("_speedups.pyx"),
options={
"build_exe": {
"packages": ["worlds", "kivy", "cymem", "websockets"],
"packages": ["worlds", "kivy", "cymem", "websockets", "kivymd"],
"includes": [],
"excludes": ["numpy", "Cython", "PySide2", "PIL",
"pandas", "zstandard"],
"pandas"],
"zip_includes": [],
"zip_include_packages": ["*"],
"zip_exclude_packages": ["worlds", "sc2"],
"zip_exclude_packages": ["worlds", "sc2", "kivymd"],
"include_files": [], # broken in cx 6.14.0, we use more special sauce now
"include_msvcr": False,
"replace_paths": ["*."],

View File

@@ -9,7 +9,8 @@ from worlds.LauncherComponents import Component, SuffixIdentifier, Type, compone
if TYPE_CHECKING:
from SNIClient import SNIContext
component = Component('SNI Client', 'SNIClient', component_type=Type.CLIENT, file_identifier=SuffixIdentifier(".apsoe"))
component = Component('SNI Client', 'SNIClient', component_type=Type.CLIENT, file_identifier=SuffixIdentifier(".apsoe"),
description="A client for connecting to SNES consoles via Super Nintendo Interface.")
components.append(component)

View File

@@ -27,6 +27,8 @@ class Component:
"""
display_name: str
"""Used as the GUI button label and the component name in the CLI args"""
description: str
"""Optional description displayed on the GUI underneath the display name"""
type: Type
"""
Enum "Type" classification of component intent, for filtering in the Launcher GUI
@@ -58,8 +60,9 @@ class Component:
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None,
func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None,
game_name: Optional[str] = None, supports_uri: Optional[bool] = False):
game_name: Optional[str] = None, supports_uri: Optional[bool] = False, description: str = "") -> None:
self.display_name = display_name
self.description = description
self.script_name = script_name
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
self.icon = icon

View File

@@ -5,7 +5,7 @@ from NetUtils import JSONMessagePart
from kvui import GameManager, HoverBehavior, ServerToolTip, KivyJSONtoTextParser
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.tabbedpanel import TabbedPanelItem
from kivymd.uix.tab import MDTabsItem, MDTabsItemText
from kivy.uix.gridlayout import GridLayout
from kivy.lang import Builder
from kivy.uix.label import Label