mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 12:11:33 -06:00
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:
287
Launcher.py
287
Launcher.py
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Archipelago launcher for bundled app.
|
Archipelago Launcher
|
||||||
|
|
||||||
* if run with APBP as argument, launch corresponding client.
|
* if run with APBP as argument, launch corresponding client.
|
||||||
* if run with executable as argument, run it passing argv[2:] as arguments
|
* 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
|
Scroll down to components= to add components to the launcher as well as setup.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
@@ -20,10 +20,11 @@ import urllib.parse
|
|||||||
import webbrowser
|
import webbrowser
|
||||||
from os.path import isfile
|
from os.path import isfile
|
||||||
from shutil import which
|
from shutil import which
|
||||||
from typing import Callable, Optional, Sequence, Tuple, Union
|
from typing import Callable, Optional, Sequence, Tuple, Union, Any
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
|
|
||||||
ModuleUpdate.update()
|
ModuleUpdate.update()
|
||||||
|
|
||||||
import settings
|
import settings
|
||||||
@@ -105,7 +106,8 @@ components.extend([
|
|||||||
Component("Generate Template Options", func=generate_yamls),
|
Component("Generate Template Options", func=generate_yamls),
|
||||||
Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/")),
|
Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/")),
|
||||||
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
|
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),
|
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)
|
url = urllib.parse.urlparse(path)
|
||||||
queries = urllib.parse.parse_qs(url.query)
|
queries = urllib.parse.parse_qs(url.query)
|
||||||
launch_args = (path, *launch_args)
|
launch_args = (path, *launch_args)
|
||||||
client_component = None
|
client_component = []
|
||||||
text_client_component = None
|
text_client_component = None
|
||||||
if "game" in queries:
|
if "game" in queries:
|
||||||
game = queries["game"][0]
|
game = queries["game"][0]
|
||||||
@@ -122,49 +124,40 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
|||||||
game = "Archipelago"
|
game = "Archipelago"
|
||||||
for component in components:
|
for component in components:
|
||||||
if component.supports_uri and component.game_name == game:
|
if component.supports_uri and component.game_name == game:
|
||||||
client_component = component
|
client_component.append(component)
|
||||||
elif component.display_name == "Text Client":
|
elif component.display_name == "Text Client":
|
||||||
text_client_component = component
|
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:
|
if client_component is None:
|
||||||
run_component(text_client_component, *launch_args)
|
run_component(text_client_component, *launch_args)
|
||||||
return
|
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):
|
).open()
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Component]]:
|
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)
|
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
|
refresh_components: Optional[Callable[[], None]] = None
|
||||||
|
|
||||||
|
|
||||||
def run_gui():
|
def run_gui(path: str, args: Any) -> None:
|
||||||
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget, ApAsyncImage
|
from kvui import (ThemedApp, MDFloatLayout, MDGridLayout, MDButton, MDLabel, MDButtonText, ScrollBox, ApAsyncImage)
|
||||||
|
from kivy.properties import ObjectProperty
|
||||||
from kivy.core.window import Window
|
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"
|
base_title: str = "Archipelago Launcher"
|
||||||
container: ContainerLayout
|
top_screen: MDFloatLayout = ObjectProperty(None)
|
||||||
grid: GridLayout
|
navigation: MDGridLayout = ObjectProperty(None)
|
||||||
_tool_layout: Optional[ScrollBox] = None
|
grid: MDGridLayout = ObjectProperty(None)
|
||||||
_client_layout: Optional[ScrollBox] = 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.title = self.base_title + " " + Utils.__version__
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
self.icon = r"data/icon.png"
|
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__()
|
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 card widget for a given component.
|
||||||
|
|
||||||
|
:param component: The component associated with the button.
|
||||||
|
|
||||||
|
:return: The created Card Widget.
|
||||||
"""
|
"""
|
||||||
Builds a button widget for a given component.
|
button_card = LauncherCard(component=component,
|
||||||
|
image_path=icon_paths[component.icon])
|
||||||
|
|
||||||
Args:
|
def open_menu(caller):
|
||||||
component (Component): The component associated with the button.
|
caller.menu.open()
|
||||||
|
|
||||||
Returns:
|
menu_items = [
|
||||||
None. The button is added to the parent grid layout.
|
{
|
||||||
|
"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
|
||||||
button = Button(text=component.display_name, size_hint_y=None, height=40)
|
|
||||||
button.component = component
|
def _refresh_components(self, type_filter: Sequence[str | Type] | None = None) -> None:
|
||||||
button.bind(on_release=self.component_action)
|
if not type_filter:
|
||||||
if component.icon != "icon":
|
type_filter = [Type.CLIENT, Type.ADJUSTER, Type.TOOL, Type.MISC]
|
||||||
image = ApAsyncImage(source=icon_paths[component.icon],
|
favorites = "favorites" in type_filter
|
||||||
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
|
|
||||||
|
|
||||||
# clear before repopulating
|
# clear before repopulating
|
||||||
assert self._tool_layout and self._client_layout, "must call `build` first"
|
assert self.button_layout, "must call `build` first"
|
||||||
tool_children = reversed(self._tool_layout.layout.children)
|
tool_children = reversed(self.button_layout.layout.children)
|
||||||
for child in tool_children:
|
for child in tool_children:
|
||||||
self._tool_layout.layout.remove_widget(child)
|
self.button_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)
|
|
||||||
|
|
||||||
_tools = {c.display_name: c for c in components if c.type == Type.TOOL}
|
cards = [card for card in self.cards if card.component.type in type_filter
|
||||||
_clients = {c.display_name: c for c in components if c.type == Type.CLIENT}
|
or favorites and card.component.display_name in self.favorites]
|
||||||
_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}
|
|
||||||
|
|
||||||
for (tool, client) in itertools.zip_longest(itertools.chain(
|
self.current_filter = type_filter
|
||||||
_tools.items(), _miscs.items(), _adjusters.items()
|
|
||||||
), _clients.items()):
|
for card in cards:
|
||||||
# column 1
|
self.button_layout.layout.add_widget(card)
|
||||||
if tool:
|
|
||||||
self._tool_layout.layout.add_widget(build_button(tool[1]))
|
def filter_clients(self, caller):
|
||||||
# column 2
|
self._refresh_components(caller.type)
|
||||||
if client:
|
|
||||||
self._client_layout.layout.add_widget(build_button(client[1]))
|
|
||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
self.container = ContainerLayout()
|
self.top_screen = Builder.load_file(Utils.local_path("data/launcher.kv"))
|
||||||
self.grid = GridLayout(cols=2)
|
self.grid = self.top_screen.ids.grid
|
||||||
self.container.add_widget(self.grid)
|
self.navigation = self.top_screen.ids.navigation
|
||||||
self.grid.add_widget(Label(text="General", size_hint_y=None, height=40))
|
self.button_layout = self.top_screen.ids.button_layout
|
||||||
self.grid.add_widget(Label(text="Clients", size_hint_y=None, height=40))
|
self.set_colors()
|
||||||
self._tool_layout = ScrollBox()
|
self.top_screen.md_bg_color = self.theme_cls.backgroundColor
|
||||||
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()
|
|
||||||
|
|
||||||
global refresh_components
|
global refresh_components
|
||||||
refresh_components = self._refresh_components
|
refresh_components = self._refresh_components
|
||||||
|
|
||||||
Window.bind(on_drop_file=self._on_drop_file)
|
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
|
@staticmethod
|
||||||
def component_action(button):
|
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:
|
if button.component.func:
|
||||||
button.component.func()
|
button.component.func()
|
||||||
else:
|
else:
|
||||||
@@ -333,7 +389,13 @@ def run_gui():
|
|||||||
self.root_window.close()
|
self.root_window.close()
|
||||||
super()._stop(*largs)
|
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
|
# avoiding Launcher reference leak
|
||||||
# and don't try to do something with widgets after window closed
|
# and don't try to do something with widgets after window closed
|
||||||
@@ -360,16 +422,14 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
|||||||
|
|
||||||
path = args.get("Patch|Game|Component|url", None)
|
path = args.get("Patch|Game|Component|url", None)
|
||||||
if path is not None:
|
if path is not None:
|
||||||
if path.startswith("archipelago://"):
|
if not path.startswith("archipelago://"):
|
||||||
handle_uri(path, args.get("args", ()))
|
file, component = identify(path)
|
||||||
return
|
if file:
|
||||||
file, component = identify(path)
|
args['file'] = file
|
||||||
if file:
|
if component:
|
||||||
args['file'] = file
|
args['component'] = component
|
||||||
if component:
|
if not component:
|
||||||
args['component'] = component
|
logging.warning(f"Could not identify Component responsible for {path}")
|
||||||
if not component:
|
|
||||||
logging.warning(f"Could not identify Component responsible for {path}")
|
|
||||||
|
|
||||||
if args["update_settings"]:
|
if args["update_settings"]:
|
||||||
update_settings()
|
update_settings()
|
||||||
@@ -378,7 +438,7 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
|||||||
elif "component" in args:
|
elif "component" in args:
|
||||||
run_component(args["component"], *args["args"])
|
run_component(args["component"], *args["args"])
|
||||||
elif not args["update_settings"]:
|
elif not args["update_settings"]:
|
||||||
run_gui()
|
run_gui(path, args.get("args", ()))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
@@ -400,6 +460,7 @@ if __name__ == '__main__':
|
|||||||
main(parser.parse_args())
|
main(parser.parse_args())
|
||||||
|
|
||||||
from worlds.LauncherComponents import processes
|
from worlds.LauncherComponents import processes
|
||||||
|
|
||||||
for process in processes:
|
for process in processes:
|
||||||
# we await all child processes to close before we tear down the process host
|
# 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
|
# this makes it feel like each one is its own program, as the Launcher is closed now
|
||||||
|
@@ -529,9 +529,7 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
|
|
||||||
def run_gui(self) -> None:
|
def run_gui(self) -> None:
|
||||||
import webbrowser
|
import webbrowser
|
||||||
import kvui
|
from kvui import GameManager, ImageButton
|
||||||
from kvui import Button, GameManager
|
|
||||||
from kivy.uix.image import Image
|
|
||||||
|
|
||||||
class LADXManager(GameManager):
|
class LADXManager(GameManager):
|
||||||
logging_pairs = [
|
logging_pairs = [
|
||||||
@@ -544,16 +542,10 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
b = super().build()
|
b = super().build()
|
||||||
|
|
||||||
if self.ctx.magpie_enabled:
|
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'))
|
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)
|
self.connect_layout.add_widget(button)
|
||||||
|
|
||||||
return b
|
return b
|
||||||
|
|
||||||
self.ui = LADXManager(self)
|
self.ui = LADXManager(self)
|
||||||
|
@@ -214,17 +214,11 @@ class WargrooveContext(CommonContext):
|
|||||||
def run_gui(self):
|
def run_gui(self):
|
||||||
"""Import kivy UI system and start running it as self.ui_task."""
|
"""Import kivy UI system and start running it as self.ui_task."""
|
||||||
from kvui import GameManager, HoverBehavior, ServerToolTip
|
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.lang import Builder
|
||||||
from kivy.uix.button import Button
|
|
||||||
from kivy.uix.togglebutton import ToggleButton
|
from kivy.uix.togglebutton import ToggleButton
|
||||||
from kivy.uix.boxlayout import BoxLayout
|
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.uix.label import Label
|
||||||
from kivy.properties import ColorProperty
|
|
||||||
from kivy.uix.image import Image
|
|
||||||
import pkgutil
|
import pkgutil
|
||||||
|
|
||||||
class TrackerLayout(BoxLayout):
|
class TrackerLayout(BoxLayout):
|
||||||
|
@@ -14,23 +14,50 @@
|
|||||||
salmon: "FA8072" # typically trap item
|
salmon: "FA8072" # typically trap item
|
||||||
white: "FFFFFF" # not used, if you want to change the generic text color change color in Label
|
white: "FFFFFF" # not used, if you want to change the generic text color change color in Label
|
||||||
orange: "FF7700" # Used for command echo
|
orange: "FF7700" # Used for command echo
|
||||||
<Label>:
|
# KivyMD theming parameters
|
||||||
color: "FFFFFF"
|
theme_style: "Dark" # Light/Dark
|
||||||
<TabbedPanel>:
|
primary_palette: "Green" # Many options
|
||||||
tab_width: root.width / app.tab_count
|
dynamic_scheme_name: "TONAL_SPOT"
|
||||||
|
<MDLabel>:
|
||||||
|
color: self.theme_cls.primaryColor
|
||||||
<TooltipLabel>:
|
<TooltipLabel>:
|
||||||
text_size: self.width, None
|
adaptive_height: True
|
||||||
size_hint_y: None
|
|
||||||
height: self.texture_size[1]
|
|
||||||
font_size: dp(20)
|
font_size: dp(20)
|
||||||
markup: True
|
markup: True
|
||||||
|
halign: "left"
|
||||||
<SelectableLabel>:
|
<SelectableLabel>:
|
||||||
|
size_hint: 1, None
|
||||||
canvas.before:
|
canvas.before:
|
||||||
Color:
|
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:
|
Rectangle:
|
||||||
size: self.size
|
size: self.size
|
||||||
pos: self.pos
|
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>:
|
<UILog>:
|
||||||
messages: 1000 # amount of messages stored in client logs.
|
messages: 1000 # amount of messages stored in client logs.
|
||||||
cols: 1
|
cols: 1
|
||||||
@@ -49,7 +76,7 @@
|
|||||||
<HintLabel>:
|
<HintLabel>:
|
||||||
canvas.before:
|
canvas.before:
|
||||||
Color:
|
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:
|
Rectangle:
|
||||||
size: self.size
|
size: self.size
|
||||||
pos: self.pos
|
pos: self.pos
|
||||||
@@ -152,3 +179,15 @@
|
|||||||
height: dp(30)
|
height: dp(30)
|
||||||
multiline: False
|
multiline: False
|
||||||
write_tab: 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
142
data/launcher.kv
Normal 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
|
464
kvui.py
464
kvui.py
@@ -35,8 +35,7 @@ from kivy.config import Config
|
|||||||
Config.set("input", "mouse", "mouse,disable_multitouch")
|
Config.set("input", "mouse", "mouse,disable_multitouch")
|
||||||
Config.set("kivy", "exit_on_escape", "0")
|
Config.set("kivy", "exit_on_escape", "0")
|
||||||
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
|
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
|
||||||
|
from kivymd.uix.divider import MDDivider
|
||||||
from kivy.app import App
|
|
||||||
from kivy.core.window import Window
|
from kivy.core.window import Window
|
||||||
from kivy.core.clipboard import Clipboard
|
from kivy.core.clipboard import Clipboard
|
||||||
from kivy.core.text.markup import MarkupLabel
|
from kivy.core.text.markup import MarkupLabel
|
||||||
@@ -46,30 +45,32 @@ from kivy.clock import Clock
|
|||||||
from kivy.factory import Factory
|
from kivy.factory import Factory
|
||||||
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
|
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
|
||||||
from kivy.metrics import dp
|
from kivy.metrics import dp
|
||||||
from kivy.effects.scroll import ScrollEffect
|
|
||||||
from kivy.uix.widget import Widget
|
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.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.utils import escape_markup
|
||||||
from kivy.lang import Builder
|
from kivy.lang import Builder
|
||||||
from kivy.uix.recycleview.views import RecycleDataViewBehavior
|
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.recycleboxlayout import RecycleBoxLayout
|
||||||
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
|
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
|
||||||
from kivy.animation import Animation
|
from kivy.animation import Animation
|
||||||
from kivy.uix.popup import Popup
|
from kivy.uix.popup import Popup
|
||||||
from kivy.uix.dropdown import DropDown
|
|
||||||
from kivy.uix.image import AsyncImage
|
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)
|
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"\[.*?]")
|
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 :(
|
# I was surprised to find this didn't already exist in kivy :(
|
||||||
class HoverBehavior(object):
|
class HoverBehavior(object):
|
||||||
"""originally from https://stackoverflow.com/a/605348110"""
|
"""originally from https://stackoverflow.com/a/605348110"""
|
||||||
@@ -125,7 +205,7 @@ class HoverBehavior(object):
|
|||||||
Factory.register("HoverBehavior", HoverBehavior)
|
Factory.register("HoverBehavior", HoverBehavior)
|
||||||
|
|
||||||
|
|
||||||
class ToolTip(Label):
|
class ToolTip(MDTooltipPlain):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -133,49 +213,30 @@ class ServerToolTip(ToolTip):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ScrollBox(ScrollView):
|
class HovererableLabel(HoverBehavior, MDLabel):
|
||||||
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):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TooltipLabel(HovererableLabel):
|
class TooltipLabel(HovererableLabel, MDTooltip):
|
||||||
tooltip = None
|
tooltip_display_delay = 0.1
|
||||||
|
|
||||||
def create_tooltip(self, text, x, y):
|
def create_tooltip(self, text, x, y):
|
||||||
text = text.replace("<br>", "\n").replace("&", "&").replace("&bl;", "[").replace("&br;", "]")
|
text = text.replace("<br>", "\n").replace("&", "&").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
|
# position float layout
|
||||||
self.tooltip.x = x - self.tooltip.width / 2
|
center_x, center_y = self.to_window(self.center_x, self.center_y)
|
||||||
self.tooltip.y = y - self.tooltip.height / 2 + 48
|
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:
|
||||||
if self.tooltip:
|
# update
|
||||||
App.get_running_app().root.remove_widget(self.tooltip)
|
self._tooltip.text = text
|
||||||
self.tooltip = None
|
else:
|
||||||
|
self._tooltip = ToolTip(text=text, pos_hint={})
|
||||||
|
self.display_tooltip()
|
||||||
|
|
||||||
def on_mouse_pos(self, window, pos):
|
def on_mouse_pos(self, window, pos):
|
||||||
if not self.get_root_window():
|
if not self.get_root_window():
|
||||||
@@ -202,26 +263,26 @@ class TooltipLabel(HovererableLabel):
|
|||||||
|
|
||||||
def on_leave(self):
|
def on_leave(self):
|
||||||
self.remove_tooltip()
|
self.remove_tooltip()
|
||||||
|
self._tooltip = None
|
||||||
|
|
||||||
|
|
||||||
class ServerLabel(HovererableLabel):
|
class ServerLabel(HovererableLabel, MDTooltip):
|
||||||
|
tooltip_display_delay = 0.1
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(HovererableLabel, self).__init__(*args, **kwargs)
|
super(HovererableLabel, self).__init__(*args, **kwargs)
|
||||||
self.layout = FloatLayout()
|
self._tooltip = ServerToolTip(text="Test")
|
||||||
self.popuplabel = ServerToolTip(text="Test")
|
|
||||||
self.layout.add_widget(self.popuplabel)
|
|
||||||
|
|
||||||
def on_enter(self):
|
def on_enter(self):
|
||||||
self.popuplabel.text = self.get_text()
|
self._tooltip.text = self.get_text()
|
||||||
App.get_running_app().root.add_widget(self.layout)
|
self.display_tooltip()
|
||||||
fade_in_animation.start(self.layout)
|
|
||||||
|
|
||||||
def on_leave(self):
|
def on_leave(self):
|
||||||
App.get_running_app().root.remove_widget(self.layout)
|
self.animation_tooltip_dismiss()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ctx(self) -> context_type:
|
def ctx(self) -> context_type:
|
||||||
return App.get_running_app().ctx
|
return MDApp.get_running_app().ctx
|
||||||
|
|
||||||
def get_text(self):
|
def get_text(self):
|
||||||
if self.ctx.server:
|
if self.ctx.server:
|
||||||
@@ -262,11 +323,11 @@ class ServerLabel(HovererableLabel):
|
|||||||
return "No current server connection. \nPlease connect to an Archipelago server."
|
return "No current server connection. \nPlease connect to an Archipelago server."
|
||||||
|
|
||||||
|
|
||||||
class MainLayout(GridLayout):
|
class MainLayout(MDGridLayout):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ContainerLayout(FloatLayout):
|
class ContainerLayout(MDFloatLayout):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -286,6 +347,11 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
|||||||
return super(SelectableLabel, self).refresh_view_attrs(
|
return super(SelectableLabel, self).refresh_view_attrs(
|
||||||
rv, index, data)
|
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):
|
def on_touch_down(self, touch):
|
||||||
""" Add selection on touch down """
|
""" Add selection on touch down """
|
||||||
if super(SelectableLabel, self).on_touch_down(touch):
|
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.
|
# Not a fan of the following few lines, but they work.
|
||||||
temp = MarkupLabel(text=self.text).markup
|
temp = MarkupLabel(text=self.text).markup
|
||||||
text = "".join(part for part in temp if not part.startswith("["))
|
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:
|
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:
|
if input_text is not None:
|
||||||
cmdinput.text = input_text
|
cmdinput.text = input_text
|
||||||
|
|
||||||
@@ -310,30 +376,115 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
|||||||
""" Respond to the selection of items in the view. """
|
""" Respond to the selection of items in the view. """
|
||||||
self.selected = is_selected
|
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)
|
min_chars = NumericProperty(3)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**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.dropdown.bind(on_select=lambda instance, x: setattr(self, 'text', x))
|
||||||
self.bind(on_text_validate=self.on_message)
|
self.bind(on_text_validate=self.on_message)
|
||||||
|
self.bind(width=lambda instance, x: setattr(self.dropdown, "width", x))
|
||||||
|
|
||||||
def on_message(self, instance):
|
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):
|
def on_text(self, instance, value):
|
||||||
if len(value) >= self.min_chars:
|
if len(value) >= self.min_chars:
|
||||||
self.dropdown.clear_widgets()
|
self.dropdown.items.clear()
|
||||||
ctx: context_type = App.get_running_app().ctx
|
ctx: context_type = MDApp.get_running_app().ctx
|
||||||
if not ctx.game:
|
if not ctx.game:
|
||||||
return
|
return
|
||||||
item_names = ctx.item_names._game_store[ctx.game].values()
|
item_names = ctx.item_names._game_store[ctx.game].values()
|
||||||
|
|
||||||
def on_press(button: Button):
|
def on_press(text):
|
||||||
split_text = MarkupLabel(text=button.text).markup
|
split_text = MarkupLabel(text=text).markup
|
||||||
return self.dropdown.select("".join(text_frag for text_frag in split_text
|
return self.dropdown.select("".join(text_frag for text_frag in split_text
|
||||||
if not text_frag.startswith("[")))
|
if not text_frag.startswith("[")))
|
||||||
lowered = value.lower()
|
lowered = value.lower()
|
||||||
@@ -345,20 +496,29 @@ class AutocompleteHintInput(TextInput):
|
|||||||
else:
|
else:
|
||||||
text = escape_markup(item_name)
|
text = escape_markup(item_name)
|
||||||
text = text[:index] + "[b]" + text[index:index+len(value)]+"[/b]"+text[index+len(value):]
|
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)
|
self.dropdown.items.append({
|
||||||
btn.bind(on_release=on_press)
|
"text": text,
|
||||||
self.dropdown.add_widget(btn)
|
"on_release": lambda: on_press(text),
|
||||||
if not self.dropdown.attach_to:
|
"markup": True
|
||||||
self.dropdown.open(self)
|
})
|
||||||
|
if not self.dropdown.parent:
|
||||||
|
self.dropdown.open()
|
||||||
else:
|
else:
|
||||||
self.dropdown.dismiss()
|
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)
|
selected = BooleanProperty(False)
|
||||||
striped = BooleanProperty(False)
|
striped = BooleanProperty(False)
|
||||||
index = None
|
index = None
|
||||||
dropdown: DropDown
|
dropdown: MDDropdownMenu
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(HintLabel, self).__init__()
|
super(HintLabel, self).__init__()
|
||||||
@@ -369,29 +529,28 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
|||||||
self.entrance_text = ""
|
self.entrance_text = ""
|
||||||
self.status_text = ""
|
self.status_text = ""
|
||||||
self.hint = {}
|
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
|
for status in (HintStatus.HINT_NO_PRIORITY, HintStatus.HINT_PRIORITY, HintStatus.HINT_AVOID):
|
||||||
self.dropdown = DropDown()
|
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 = MDDropdownMenu(caller=self.ids["status"], items=menu_items)
|
||||||
self.dropdown.select(button.status)
|
|
||||||
|
|
||||||
def select(instance, data):
|
def select(instance, data):
|
||||||
ctx.update_hint(self.hint["location"],
|
ctx.update_hint(self.hint["location"],
|
||||||
self.hint["finding_player"],
|
self.hint["finding_player"],
|
||||||
data)
|
data)
|
||||||
|
|
||||||
for status in (HintStatus.HINT_NO_PRIORITY, HintStatus.HINT_PRIORITY, HintStatus.HINT_AVOID):
|
self.dropdown.bind(on_release=self.dropdown.dismiss)
|
||||||
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)
|
|
||||||
|
|
||||||
def set_height(self, instance, value):
|
def set_height(self, instance, value):
|
||||||
self.height = max([child.texture_size[1] for child in self.children])
|
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.entrance_text = data["entrance"]["text"]
|
||||||
self.status_text = data["status"]["text"]
|
self.status_text = data["status"]["text"]
|
||||||
self.hint = data["status"]["hint"]
|
self.hint = data["status"]["hint"]
|
||||||
self.height = self.minimum_height
|
|
||||||
return super(HintLabel, self).refresh_view_attrs(rv, index, data)
|
return super(HintLabel, self).refresh_view_attrs(rv, index, data)
|
||||||
|
|
||||||
def on_touch_down(self, touch):
|
def on_touch_down(self, touch):
|
||||||
@@ -419,10 +577,10 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
|||||||
if status_label.collide_point(*touch.pos):
|
if status_label.collide_point(*touch.pos):
|
||||||
if self.hint["status"] == HintStatus.HINT_FOUND:
|
if self.hint["status"] == HintStatus.HINT_FOUND:
|
||||||
return
|
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
|
if ctx.slot_concerns_self(self.hint["receiving_player"]): # If this player owns this hint
|
||||||
# open a dropdown
|
# open a dropdown
|
||||||
self.dropdown.open(self.ids["status"])
|
self.dropdown.open()
|
||||||
elif self.selected:
|
elif self.selected:
|
||||||
self.parent.clear_selection()
|
self.parent.clear_selection()
|
||||||
else:
|
else:
|
||||||
@@ -455,7 +613,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
|||||||
else:
|
else:
|
||||||
parent.sort_key = key
|
parent.sort_key = key
|
||||||
parent.reversed = False
|
parent.reversed = False
|
||||||
App.get_running_app().update_hints()
|
MDApp.get_running_app().update_hints()
|
||||||
|
|
||||||
def apply_selection(self, rv, index, is_selected):
|
def apply_selection(self, rv, index, is_selected):
|
||||||
""" Respond to the selection of items in the view. """
|
""" Respond to the selection of items in the view. """
|
||||||
@@ -463,7 +621,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
|||||||
self.selected = is_selected
|
self.selected = is_selected
|
||||||
|
|
||||||
|
|
||||||
class ConnectBarTextInput(TextInput):
|
class ConnectBarTextInput(MDTextField):
|
||||||
def insert_text(self, substring, from_undo=False):
|
def insert_text(self, substring, from_undo=False):
|
||||||
s = substring.replace("\n", "").replace("\r", "")
|
s = substring.replace("\n", "").replace("\r", "")
|
||||||
return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo)
|
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 "/!"
|
return len(string) > 0 and string[0] in "/!"
|
||||||
|
|
||||||
|
|
||||||
class CommandPromptTextInput(TextInput):
|
class CommandPromptTextInput(MDTextField):
|
||||||
MAXIMUM_HISTORY_MESSAGES = 50
|
MAXIMUM_HISTORY_MESSAGES = 50
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
@@ -521,7 +679,7 @@ class CommandPromptTextInput(TextInput):
|
|||||||
|
|
||||||
|
|
||||||
class MessageBox(Popup):
|
class MessageBox(Popup):
|
||||||
class MessageBoxLabel(Label):
|
class MessageBoxLabel(MDLabel):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._label.refresh()
|
self._label.refresh()
|
||||||
@@ -539,14 +697,31 @@ class MessageBox(Popup):
|
|||||||
self.height += max(0, label.height - 18)
|
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 = [
|
logging_pairs = [
|
||||||
("Client", "Archipelago"),
|
("Client", "Archipelago"),
|
||||||
]
|
]
|
||||||
base_title: str = "Archipelago Client"
|
base_title: str = "Archipelago Client"
|
||||||
last_autofillable_command: str
|
last_autofillable_command: str
|
||||||
|
|
||||||
main_area_container: GridLayout
|
main_area_container: MDGridLayout
|
||||||
""" subclasses can add more columns beside the tabs """
|
""" subclasses can add more columns beside the tabs """
|
||||||
|
|
||||||
def __init__(self, ctx: context_type):
|
def __init__(self, ctx: context_type):
|
||||||
@@ -581,18 +756,26 @@ class GameManager(App):
|
|||||||
return max(1, len(self.tabs.tab_list))
|
return max(1, len(self.tabs.tab_list))
|
||||||
return 1
|
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:
|
def build(self) -> Layout:
|
||||||
|
self.set_colors()
|
||||||
self.container = ContainerLayout()
|
self.container = ContainerLayout()
|
||||||
|
|
||||||
self.grid = MainLayout()
|
self.grid = MainLayout()
|
||||||
self.grid.cols = 1
|
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
|
# top part
|
||||||
server_label = ServerLabel()
|
server_label = ServerLabel(halign="center")
|
||||||
self.connect_layout.add_widget(server_label)
|
self.connect_layout.add_widget(server_label)
|
||||||
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:",
|
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:",
|
||||||
size_hint_y=None,
|
size_hint_y=None, role="medium",
|
||||||
height=dp(30), multiline=False, write_tab=False)
|
height=dp(70), multiline=False, write_tab=False)
|
||||||
|
|
||||||
def connect_bar_validate(sender):
|
def connect_bar_validate(sender):
|
||||||
if not self.ctx.server:
|
if not self.ctx.server:
|
||||||
@@ -600,26 +783,31 @@ class GameManager(App):
|
|||||||
|
|
||||||
self.server_connect_bar.bind(on_text_validate=connect_bar_validate)
|
self.server_connect_bar.bind(on_text_validate=connect_bar_validate)
|
||||||
self.connect_layout.add_widget(self.server_connect_bar)
|
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.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.connect_layout.add_widget(self.server_connect_button)
|
||||||
self.grid.add_widget(self.connect_layout)
|
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)
|
self.grid.add_widget(self.progressbar)
|
||||||
|
|
||||||
# middle part
|
# middle part
|
||||||
self.tabs = TabbedPanel(size_hint_y=1)
|
self.tabs = ClientTabs()
|
||||||
self.tabs.default_tab_text = "All"
|
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)
|
self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name)
|
||||||
for logger_name, name in
|
for logger_name, name in
|
||||||
self.logging_pairs))
|
self.logging_pairs))
|
||||||
|
self.tabs.carousel.add_widget(self.tabs.default_tab_content)
|
||||||
|
|
||||||
for logger_name, display_name in self.logging_pairs:
|
for logger_name, display_name in self.logging_pairs:
|
||||||
bridge_logger = logging.getLogger(logger_name)
|
bridge_logger = logging.getLogger(logger_name)
|
||||||
panel = TabbedPanelItem(text=display_name)
|
self.log_panels[display_name] = UILog(bridge_logger)
|
||||||
self.log_panels[display_name] = panel.content = UILog(bridge_logger)
|
|
||||||
if len(self.logging_pairs) > 1:
|
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
|
# show Archipelago tab if other logging is present
|
||||||
|
self.tabs.carousel.add_widget(panel.content)
|
||||||
self.tabs.add_widget(panel)
|
self.tabs.add_widget(panel)
|
||||||
|
|
||||||
hint_panel = self.add_client_tab("Hints", HintLayout())
|
hint_panel = self.add_client_tab("Hints", HintLayout())
|
||||||
@@ -627,21 +815,20 @@ class GameManager(App):
|
|||||||
self.log_panels["Hints"] = hint_panel.content
|
self.log_panels["Hints"] = hint_panel.content
|
||||||
hint_panel.content.add_widget(self.hint_log)
|
hint_panel.content.add_widget(self.hint_log)
|
||||||
|
|
||||||
if len(self.logging_pairs) == 1:
|
self.main_area_container = MDGridLayout(size_hint_y=1, rows=1)
|
||||||
self.tabs.default_tab_text = "Archipelago"
|
|
||||||
|
|
||||||
self.main_area_container = GridLayout(size_hint_y=1, rows=1)
|
|
||||||
self.main_area_container.add_widget(self.tabs)
|
self.main_area_container.add_widget(self.tabs)
|
||||||
|
|
||||||
self.grid.add_widget(self.main_area_container)
|
self.grid.add_widget(self.main_area_container)
|
||||||
|
|
||||||
# bottom part
|
# bottom part
|
||||||
bottom_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
|
bottom_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(70), spacing=5, padding=(5, 10))
|
||||||
info_button = Button(size=(dp(100), dp(30)), text="Command:", size_hint_x=None)
|
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)
|
info_button.bind(on_release=self.command_button_action)
|
||||||
bottom_layout.add_widget(info_button)
|
bottom_layout.add_widget(info_button)
|
||||||
self.textinput = CommandPromptTextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
|
self.textinput = CommandPromptTextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
|
||||||
self.textinput.bind(on_text_validate=self.on_message)
|
self.textinput.bind(on_text_validate=self.on_message)
|
||||||
|
info_button.height = self.textinput.height
|
||||||
self.textinput.text_validate_unfocus = False
|
self.textinput.text_validate_unfocus = False
|
||||||
bottom_layout.add_widget(self.textinput)
|
bottom_layout.add_widget(self.textinput)
|
||||||
self.grid.add_widget(bottom_layout)
|
self.grid.add_widget(bottom_layout)
|
||||||
@@ -662,24 +849,26 @@ class GameManager(App):
|
|||||||
def add_client_tab(self, title: str, content: Widget) -> Widget:
|
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.
|
"""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."""
|
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
|
new_tab.content = content
|
||||||
self.tabs.add_widget(new_tab)
|
self.tabs.add_widget(new_tab)
|
||||||
|
self.tabs.carousel.add_widget(new_tab.content)
|
||||||
return new_tab
|
return new_tab
|
||||||
|
|
||||||
def update_texts(self, dt):
|
def update_texts(self, dt):
|
||||||
if hasattr(self.tabs.content.children[0], "fix_heights"):
|
for slide in self.tabs.carousel.slides:
|
||||||
self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream
|
if hasattr(slide, "fix_heights"):
|
||||||
|
slide.fix_heights() # TODO: remove this when Kivy fixes this upstream
|
||||||
if self.ctx.server:
|
if self.ctx.server:
|
||||||
self.title = self.base_title + " " + Utils.__version__ + \
|
self.title = self.base_title + " " + Utils.__version__ + \
|
||||||
f" | Connected to: {self.ctx.server_address} " \
|
f" | Connected to: {self.ctx.server_address} " \
|
||||||
f"{'.'.join(str(e) for e in self.ctx.server_version)}"
|
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.server_connect_bar.readonly = True
|
||||||
self.progressbar.max = len(self.ctx.checked_locations) + len(self.ctx.missing_locations)
|
self.progressbar.max = len(self.ctx.checked_locations) + len(self.ctx.missing_locations)
|
||||||
self.progressbar.value = len(self.ctx.checked_locations)
|
self.progressbar.value = len(self.ctx.checked_locations)
|
||||||
else:
|
else:
|
||||||
self.server_connect_button.text = "Connect"
|
self.server_connect_button._button_text.text = "Connect"
|
||||||
self.server_connect_bar.readonly = False
|
self.server_connect_bar.readonly = False
|
||||||
self.title = self.base_title + " " + Utils.__version__
|
self.title = self.base_title + " " + Utils.__version__
|
||||||
self.progressbar.value = 0
|
self.progressbar.value = 0
|
||||||
@@ -742,8 +931,8 @@ class GameManager(App):
|
|||||||
|
|
||||||
def enable_energy_link(self):
|
def enable_energy_link(self):
|
||||||
if not hasattr(self, "energy_link_label"):
|
if not hasattr(self, "energy_link_label"):
|
||||||
self.energy_link_label = Label(text="Energy Link: Standby",
|
self.energy_link_label = MDLabel(text="Energy Link: Standby",
|
||||||
size_hint_x=None, width=150)
|
size_hint_x=None, width=150, halign="center")
|
||||||
self.connect_layout.add_widget(self.energy_link_label)
|
self.connect_layout.add_widget(self.energy_link_label)
|
||||||
|
|
||||||
def set_new_energy_link_value(self):
|
def set_new_energy_link_value(self):
|
||||||
@@ -779,8 +968,9 @@ class LogtoUI(logging.Handler):
|
|||||||
self.on_log(self.format(record))
|
self.on_log(self.format(record))
|
||||||
|
|
||||||
|
|
||||||
class UILog(RecycleView):
|
class UILog(MDRecycleView):
|
||||||
messages: typing.ClassVar[int] # comes from kv file
|
messages: typing.ClassVar[int] # comes from kv file
|
||||||
|
adaptive_height = True
|
||||||
|
|
||||||
def __init__(self, *loggers_to_handle, **kwargs):
|
def __init__(self, *loggers_to_handle, **kwargs):
|
||||||
super(UILog, self).__init__(**kwargs)
|
super(UILog, self).__init__(**kwargs)
|
||||||
@@ -807,13 +997,13 @@ class UILog(RecycleView):
|
|||||||
element.height = element.texture_size[1]
|
element.height = element.texture_size[1]
|
||||||
|
|
||||||
|
|
||||||
class HintLayout(BoxLayout):
|
class HintLayout(MDBoxLayout):
|
||||||
orientation = "vertical"
|
orientation = "vertical"
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
boxlayout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
|
boxlayout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(55))
|
||||||
boxlayout.add_widget(Label(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(30)))
|
boxlayout.add_widget(MDLabel(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(55)))
|
||||||
boxlayout.add_widget(AutocompleteHintInput())
|
boxlayout.add_widget(AutocompleteHintInput())
|
||||||
self.add_widget(boxlayout)
|
self.add_widget(boxlayout)
|
||||||
|
|
||||||
@@ -846,8 +1036,7 @@ status_sort_weights: dict[HintStatus, int] = {
|
|||||||
HintStatus.HINT_PRIORITY: 4,
|
HintStatus.HINT_PRIORITY: 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class HintLog(MDRecycleView):
|
||||||
class HintLog(RecycleView):
|
|
||||||
header = {
|
header = {
|
||||||
"receiving": {"text": "[u]Receiving Player[/u]"},
|
"receiving": {"text": "[u]Receiving Player[/u]"},
|
||||||
"item": {"text": "[u]Item[/u]"},
|
"item": {"text": "[u]Item[/u]"},
|
||||||
@@ -858,7 +1047,7 @@ class HintLog(RecycleView):
|
|||||||
"hint": {"receiving_player": -1, "location": -1, "finding_player": -1, "status": ""}},
|
"hint": {"receiving_player": -1, "location": -1, "finding_player": -1, "status": ""}},
|
||||||
"striped": True,
|
"striped": True,
|
||||||
}
|
}
|
||||||
|
data: list[typing.Any]
|
||||||
sort_key: str = ""
|
sort_key: str = ""
|
||||||
reversed: bool = True
|
reversed: bool = True
|
||||||
|
|
||||||
@@ -871,7 +1060,7 @@ class HintLog(RecycleView):
|
|||||||
if not hints: # Fix the scrolling looking visually wrong in some edge cases
|
if not hints: # Fix the scrolling looking visually wrong in some edge cases
|
||||||
self.scroll_y = 1.0
|
self.scroll_y = 1.0
|
||||||
data = []
|
data = []
|
||||||
ctx = App.get_running_app().ctx
|
ctx = MDApp.get_running_app().ctx
|
||||||
for hint in hints:
|
for hint in hints:
|
||||||
if not hint.get("status"): # Allows connecting to old servers
|
if not hint.get("status"): # Allows connecting to old servers
|
||||||
hint["status"] = HintStatus.HINT_FOUND if hint["found"] else HintStatus.HINT_UNSPECIFIED
|
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)
|
data = pkgutil.get_data(module, path)
|
||||||
return self._bytes_to_data(data)
|
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())
|
loader = next(loader for loader in ImageLoader.loaders if loader.can_load_memory())
|
||||||
return loader.load(loader, io.BytesIO(data))
|
return loader.load(loader, io.BytesIO(data))
|
||||||
|
|
||||||
|
@@ -12,3 +12,6 @@ cython>=3.0.12
|
|||||||
cymem>=2.0.11
|
cymem>=2.0.11
|
||||||
orjson>=3.10.15
|
orjson>=3.10.15
|
||||||
typing_extensions>=4.12.2
|
typing_extensions>=4.12.2
|
||||||
|
pyshortcuts>=1.9.1
|
||||||
|
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
|
||||||
|
kivymd>=2.0.1.dev0
|
||||||
|
7
setup.py
7
setup.py
@@ -629,12 +629,13 @@ cx_Freeze.setup(
|
|||||||
ext_modules=cythonize("_speedups.pyx"),
|
ext_modules=cythonize("_speedups.pyx"),
|
||||||
options={
|
options={
|
||||||
"build_exe": {
|
"build_exe": {
|
||||||
"packages": ["worlds", "kivy", "cymem", "websockets"],
|
"packages": ["worlds", "kivy", "cymem", "websockets", "kivymd"],
|
||||||
"includes": [],
|
"includes": [],
|
||||||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||||
"pandas", "zstandard"],
|
"pandas"],
|
||||||
|
"zip_includes": [],
|
||||||
"zip_include_packages": ["*"],
|
"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_files": [], # broken in cx 6.14.0, we use more special sauce now
|
||||||
"include_msvcr": False,
|
"include_msvcr": False,
|
||||||
"replace_paths": ["*."],
|
"replace_paths": ["*."],
|
||||||
|
@@ -9,7 +9,8 @@ from worlds.LauncherComponents import Component, SuffixIdentifier, Type, compone
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from SNIClient import SNIContext
|
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)
|
components.append(component)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -27,6 +27,8 @@ class Component:
|
|||||||
"""
|
"""
|
||||||
display_name: str
|
display_name: str
|
||||||
"""Used as the GUI button label and the component name in the CLI args"""
|
"""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
|
type: Type
|
||||||
"""
|
"""
|
||||||
Enum "Type" classification of component intent, for filtering in the Launcher GUI
|
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,
|
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,
|
cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None,
|
||||||
func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = 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.display_name = display_name
|
||||||
|
self.description = description
|
||||||
self.script_name = script_name
|
self.script_name = script_name
|
||||||
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
||||||
self.icon = icon
|
self.icon = icon
|
||||||
|
@@ -5,7 +5,7 @@ from NetUtils import JSONMessagePart
|
|||||||
from kvui import GameManager, HoverBehavior, ServerToolTip, KivyJSONtoTextParser
|
from kvui import GameManager, HoverBehavior, ServerToolTip, KivyJSONtoTextParser
|
||||||
from kivy.app import App
|
from kivy.app import App
|
||||||
from kivy.clock import Clock
|
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.uix.gridlayout import GridLayout
|
||||||
from kivy.lang import Builder
|
from kivy.lang import Builder
|
||||||
from kivy.uix.label import Label
|
from kivy.uix.label import Label
|
||||||
|
Reference in New Issue
Block a user