2021-07-30 20:18:03 +02:00
|
|
|
import os
|
|
|
|
import logging
|
2023-09-17 22:56:59 +02:00
|
|
|
import sys
|
2021-10-19 05:38:17 +02:00
|
|
|
import typing
|
2024-03-12 14:52:57 +01:00
|
|
|
import re
|
2024-11-29 21:58:52 -05:00
|
|
|
import io
|
|
|
|
import pkgutil
|
2024-06-08 19:08:47 -07:00
|
|
|
from collections import deque
|
2021-07-30 20:18:03 +02:00
|
|
|
|
2024-08-23 19:12:01 -05:00
|
|
|
assert "kivy" not in sys.modules, "kvui should be imported before kivy for frozen compatibility"
|
|
|
|
|
2023-09-17 22:56:59 +02:00
|
|
|
if sys.platform == "win32":
|
|
|
|
import ctypes
|
2023-11-07 14:51:35 -06:00
|
|
|
|
2023-09-17 22:56:59 +02:00
|
|
|
# kivy 2.2.0 introduced DPI awareness on Windows, but it makes the UI enter an infinitely recursive re-layout
|
|
|
|
# by setting the application to not DPI Aware, Windows handles scaling the entire window on its own, ignoring kivy's
|
2024-11-27 03:28:00 +01:00
|
|
|
ctypes.windll.shcore.SetProcessDpiAwareness(0)
|
2023-09-17 22:56:59 +02:00
|
|
|
|
2021-07-30 20:18:03 +02:00
|
|
|
os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
|
|
|
os.environ["KIVY_NO_FILELOG"] = "1"
|
|
|
|
os.environ["KIVY_NO_ARGS"] = "1"
|
2021-11-08 18:57:03 +01:00
|
|
|
os.environ["KIVY_LOG_ENABLE"] = "0"
|
2021-10-29 10:03:15 +02:00
|
|
|
|
2022-06-14 08:55:57 +02:00
|
|
|
import Utils
|
2023-11-07 14:51:35 -06:00
|
|
|
|
2022-06-14 08:55:57 +02:00
|
|
|
if Utils.is_frozen():
|
|
|
|
os.environ["KIVY_DATA_DIR"] = Utils.local_path("data")
|
|
|
|
|
2025-02-02 02:53:16 +01:00
|
|
|
import platformdirs
|
|
|
|
os.environ["KIVY_HOME"] = os.path.join(platformdirs.user_config_dir("Archipelago", False), "kivy")
|
|
|
|
os.makedirs(os.environ["KIVY_HOME"], exist_ok=True)
|
|
|
|
|
2022-06-03 13:38:55 +02:00
|
|
|
from kivy.config import Config
|
2022-02-24 04:47:01 +01:00
|
|
|
|
2021-12-10 09:29:59 +01:00
|
|
|
Config.set("input", "mouse", "mouse,disable_multitouch")
|
2023-11-07 14:51:35 -06:00
|
|
|
Config.set("kivy", "exit_on_escape", "0")
|
|
|
|
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
|
2025-04-05 11:46:24 -05:00
|
|
|
from kivymd.uix.divider import MDDivider
|
2021-10-29 10:03:15 +02:00
|
|
|
from kivy.core.window import Window
|
2021-11-19 21:25:01 +01:00
|
|
|
from kivy.core.clipboard import Clipboard
|
2021-11-21 23:45:15 +01:00
|
|
|
from kivy.core.text.markup import MarkupLabel
|
2024-11-29 21:58:52 -05:00
|
|
|
from kivy.core.image import ImageLoader, ImageLoaderBase, ImageData
|
2022-06-03 13:38:55 +02:00
|
|
|
from kivy.base import ExceptionHandler, ExceptionManager
|
|
|
|
from kivy.clock import Clock
|
2021-10-29 10:03:15 +02:00
|
|
|
from kivy.factory import Factory
|
2025-04-15 17:09:27 -05:00
|
|
|
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty, StringProperty
|
|
|
|
from kivy.metrics import dp, sp
|
2023-02-13 01:55:43 +01:00
|
|
|
from kivy.uix.widget import Widget
|
2022-10-27 02:30:22 -07:00
|
|
|
from kivy.uix.layout import Layout
|
2021-07-30 20:18:03 +02:00
|
|
|
from kivy.utils import escape_markup
|
|
|
|
from kivy.lang import Builder
|
2021-11-19 21:25:01 +01:00
|
|
|
from kivy.uix.recycleview.views import RecycleDataViewBehavior
|
2025-04-05 11:46:24 -05:00
|
|
|
from kivy.uix.behaviors import FocusBehavior, ToggleButtonBehavior
|
2021-11-19 21:25:01 +01:00
|
|
|
from kivy.uix.recycleboxlayout import RecycleBoxLayout
|
|
|
|
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
|
2022-05-24 00:20:02 +02:00
|
|
|
from kivy.animation import Animation
|
2022-06-04 17:02:02 +02:00
|
|
|
from kivy.uix.popup import Popup
|
2024-11-29 21:58:52 -05:00
|
|
|
from kivy.uix.image import AsyncImage
|
2025-04-05 11:46:24 -05:00
|
|
|
from kivymd.app import MDApp
|
|
|
|
from kivymd.uix.gridlayout import MDGridLayout
|
|
|
|
from kivymd.uix.floatlayout import MDFloatLayout
|
|
|
|
from kivymd.uix.boxlayout import MDBoxLayout
|
2025-04-15 17:09:27 -05:00
|
|
|
from kivymd.uix.tab.tab import MDTabsSecondary, MDTabsItem, MDTabsItemText, MDTabsCarousel
|
2025-04-05 11:46:24 -05:00
|
|
|
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
|
2022-05-24 00:20:02 +02:00
|
|
|
|
|
|
|
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
|
2021-07-30 20:18:03 +02:00
|
|
|
|
2024-11-28 20:10:31 -05:00
|
|
|
from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType, HintStatus
|
2024-06-01 14:32:41 +02:00
|
|
|
from Utils import async_start, get_input_text_from_response
|
2021-07-30 20:18:03 +02:00
|
|
|
|
2021-10-19 05:38:17 +02:00
|
|
|
if typing.TYPE_CHECKING:
|
|
|
|
import CommonClient
|
|
|
|
|
|
|
|
context_type = CommonClient.CommonContext
|
|
|
|
else:
|
|
|
|
context_type = object
|
|
|
|
|
2024-03-12 14:52:57 +01:00
|
|
|
remove_between_brackets = re.compile(r"\[.*?]")
|
|
|
|
|
2021-09-30 09:09:21 +02:00
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
class ThemedApp(MDApp):
|
|
|
|
def set_colors(self):
|
|
|
|
text_colors = KivyJSONtoTextParser.TextColors()
|
2025-04-15 17:09:27 -05:00
|
|
|
self.theme_cls.theme_style = text_colors.theme_style
|
|
|
|
self.theme_cls.primary_palette = text_colors.primary_palette
|
|
|
|
self.theme_cls.dynamic_scheme_name = text_colors.dynamic_scheme_name
|
|
|
|
self.theme_cls.dynamic_scheme_contrast = text_colors.dynamic_scheme_contrast
|
2025-04-05 11:46:24 -05:00
|
|
|
|
|
|
|
|
|
|
|
class ImageIcon(MDButtonIcon, AsyncImage):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(args, kwargs)
|
2025-04-06 13:11:16 -05:00
|
|
|
self.image = ApAsyncImage(**kwargs)
|
2025-04-05 11:46:24 -05:00
|
|
|
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__()
|
2025-04-06 13:11:16 -05:00
|
|
|
self.image = ApAsyncImage(**image_args)
|
2025-04-05 11:46:24 -05:00
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2025-04-15 17:09:27 -05:00
|
|
|
# thanks kivymd
|
|
|
|
class ResizableTextField(MDTextField):
|
|
|
|
"""
|
|
|
|
Resizable MDTextField that manually overrides the builtin sizing.
|
|
|
|
|
|
|
|
Note that in order to use this, the sizing must be specified from within a .kv rule.
|
|
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
# cursed rules override
|
|
|
|
rules = Builder.match(self)
|
|
|
|
textfield = next((rule for rule in rules if rule.name == f"<MDTextField>"), None)
|
|
|
|
if textfield:
|
|
|
|
subclasses = rules[rules.index(textfield) + 1:]
|
|
|
|
for subclass in subclasses:
|
|
|
|
height_rule = subclass.properties.get("height", None)
|
|
|
|
if height_rule:
|
|
|
|
height_rule.ignore_prev = True
|
|
|
|
super().__init__(args, kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
def on_release(self: MDButton, *args):
|
|
|
|
super(MDButton, self).on_release(args)
|
|
|
|
self.on_leave()
|
|
|
|
|
|
|
|
MDButton.on_release = on_release
|
|
|
|
|
2021-10-29 10:03:15 +02:00
|
|
|
# I was surprised to find this didn't already exist in kivy :(
|
|
|
|
class HoverBehavior(object):
|
2022-05-24 00:20:02 +02:00
|
|
|
"""originally from https://stackoverflow.com/a/605348110"""
|
2021-10-29 10:03:15 +02:00
|
|
|
hovered = BooleanProperty(False)
|
|
|
|
border_point = ObjectProperty(None)
|
|
|
|
|
|
|
|
def __init__(self, **kwargs):
|
2023-11-07 14:51:35 -06:00
|
|
|
self.register_event_type("on_enter")
|
|
|
|
self.register_event_type("on_leave")
|
2021-10-29 10:03:15 +02:00
|
|
|
Window.bind(mouse_pos=self.on_mouse_pos)
|
2021-10-31 08:07:37 -07:00
|
|
|
Window.bind(on_cursor_leave=self.on_cursor_leave)
|
2021-10-29 10:03:15 +02:00
|
|
|
super(HoverBehavior, self).__init__(**kwargs)
|
|
|
|
|
2022-05-24 00:20:02 +02:00
|
|
|
def on_mouse_pos(self, window, pos):
|
2021-10-29 10:03:15 +02:00
|
|
|
if not self.get_root_window():
|
2022-05-24 00:20:02 +02:00
|
|
|
return # Abort if not displayed
|
|
|
|
|
|
|
|
# to_widget translates window pos to within widget pos
|
2021-10-29 10:03:15 +02:00
|
|
|
inside = self.collide_point(*self.to_widget(*pos))
|
|
|
|
if self.hovered == inside:
|
|
|
|
return # We have already done what was needed
|
|
|
|
self.border_point = pos
|
|
|
|
self.hovered = inside
|
|
|
|
|
|
|
|
if inside:
|
|
|
|
self.dispatch("on_enter")
|
|
|
|
else:
|
|
|
|
self.dispatch("on_leave")
|
|
|
|
|
2021-10-31 08:07:37 -07:00
|
|
|
def on_cursor_leave(self, *args):
|
|
|
|
# if the mouse left the window, it is obviously no longer inside the hover label.
|
|
|
|
self.hovered = BooleanProperty(False)
|
|
|
|
self.border_point = ObjectProperty(None)
|
|
|
|
self.dispatch("on_leave")
|
|
|
|
|
2021-10-29 10:03:15 +02:00
|
|
|
|
2023-11-07 14:51:35 -06:00
|
|
|
Factory.register("HoverBehavior", HoverBehavior)
|
2021-10-29 10:03:15 +02:00
|
|
|
|
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
class ToolTip(MDTooltipPlain):
|
2022-05-24 00:20:02 +02:00
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class ServerToolTip(ToolTip):
|
2021-10-29 10:03:15 +02:00
|
|
|
pass
|
|
|
|
|
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
class HovererableLabel(HoverBehavior, MDLabel):
|
2022-05-24 00:20:02 +02:00
|
|
|
pass
|
|
|
|
|
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
class TooltipLabel(HovererableLabel, MDTooltip):
|
|
|
|
tooltip_display_delay = 0.1
|
2023-11-07 14:51:35 -06:00
|
|
|
|
|
|
|
def create_tooltip(self, text, x, y):
|
|
|
|
text = text.replace("<br>", "\n").replace("&", "&").replace("&bl;", "[").replace("&br;", "]")
|
|
|
|
# position float layout
|
2025-04-05 11:46:24 -05:00
|
|
|
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
|
2023-11-07 14:51:35 -06:00
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
if self._tooltip:
|
|
|
|
# update
|
|
|
|
self._tooltip.text = text
|
|
|
|
else:
|
|
|
|
self._tooltip = ToolTip(text=text, pos_hint={})
|
|
|
|
self.display_tooltip()
|
2023-11-07 14:51:35 -06:00
|
|
|
|
|
|
|
def on_mouse_pos(self, window, pos):
|
|
|
|
if not self.get_root_window():
|
|
|
|
return # Abort if not displayed
|
|
|
|
super().on_mouse_pos(window, pos)
|
|
|
|
if self.refs and self.hovered:
|
|
|
|
|
|
|
|
tx, ty = self.to_widget(*pos, relative=True)
|
|
|
|
# Why TF is Y flipped *within* the texture?
|
|
|
|
ty = self.texture_size[1] - ty
|
|
|
|
hit = False
|
|
|
|
for uid, zones in self.refs.items():
|
|
|
|
for zone in zones:
|
|
|
|
x, y, w, h = zone
|
|
|
|
if x <= tx <= w and y <= ty <= h:
|
|
|
|
self.create_tooltip(uid.split("|", 1)[1], *pos)
|
|
|
|
hit = True
|
|
|
|
break
|
|
|
|
if not hit:
|
|
|
|
self.remove_tooltip()
|
|
|
|
|
|
|
|
def on_enter(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def on_leave(self):
|
|
|
|
self.remove_tooltip()
|
2025-04-05 11:46:24 -05:00
|
|
|
self._tooltip = None
|
2023-11-07 14:51:35 -06:00
|
|
|
|
|
|
|
|
2025-04-15 17:09:27 -05:00
|
|
|
class ServerLabel(HoverBehavior, MDTooltip, MDBoxLayout):
|
2025-04-05 11:46:24 -05:00
|
|
|
tooltip_display_delay = 0.1
|
2025-04-15 17:09:27 -05:00
|
|
|
text: str = StringProperty("Server:")
|
2025-04-05 11:46:24 -05:00
|
|
|
|
2021-10-29 10:03:15 +02:00
|
|
|
def __init__(self, *args, **kwargs):
|
2025-04-15 17:09:27 -05:00
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.add_widget(MDIcon(icon="information", font_size=sp(15)))
|
|
|
|
self.add_widget(TooltipLabel(text=self.text, pos_hint={"center_x": 0.5, "center_y": 0.5},
|
|
|
|
font_size=sp(15)))
|
2025-04-05 11:46:24 -05:00
|
|
|
self._tooltip = ServerToolTip(text="Test")
|
2021-10-29 10:03:15 +02:00
|
|
|
|
|
|
|
def on_enter(self):
|
2025-04-05 11:46:24 -05:00
|
|
|
self._tooltip.text = self.get_text()
|
|
|
|
self.display_tooltip()
|
2021-10-29 10:03:15 +02:00
|
|
|
|
|
|
|
def on_leave(self):
|
2025-04-05 11:46:24 -05:00
|
|
|
self.animation_tooltip_dismiss()
|
2021-10-29 10:03:15 +02:00
|
|
|
|
2022-02-24 06:17:39 +01:00
|
|
|
@property
|
|
|
|
def ctx(self) -> context_type:
|
2025-04-05 11:46:24 -05:00
|
|
|
return MDApp.get_running_app().ctx
|
2022-02-24 06:17:39 +01:00
|
|
|
|
2021-10-29 10:03:15 +02:00
|
|
|
def get_text(self):
|
|
|
|
if self.ctx.server:
|
|
|
|
ctx = self.ctx
|
|
|
|
text = f"Connected to: {ctx.server_address}."
|
|
|
|
if ctx.slot is not None:
|
2021-10-30 07:52:03 +02:00
|
|
|
text += f"\nYou are Slot Number {ctx.slot} in Team Number {ctx.team}, " \
|
|
|
|
f"named {ctx.player_names[ctx.slot]}."
|
2021-10-29 10:03:15 +02:00
|
|
|
if ctx.items_received:
|
|
|
|
text += f"\nYou have received {len(ctx.items_received)} items. " \
|
|
|
|
f"You can list them in order with /received."
|
|
|
|
if ctx.total_locations:
|
|
|
|
text += f"\nYou have checked {len(ctx.checked_locations)} " \
|
|
|
|
f"out of {ctx.total_locations} locations. " \
|
|
|
|
f"You can get more info on missing checks with /missing."
|
|
|
|
if ctx.permissions:
|
|
|
|
text += "\nPermissions:"
|
|
|
|
for permission_name, permission_data in ctx.permissions.items():
|
|
|
|
text += f"\n {permission_name}: {permission_data}"
|
2021-11-07 14:42:05 +01:00
|
|
|
if ctx.hint_cost is not None and ctx.total_locations:
|
2023-03-24 23:14:34 +01:00
|
|
|
min_cost = int(ctx.server_version >= (0, 3, 9))
|
2021-10-29 10:03:15 +02:00
|
|
|
text += f"\nA new !hint <itemname> costs {ctx.hint_cost}% of checks made. " \
|
2023-03-24 23:14:34 +01:00
|
|
|
f"For you this means every " \
|
2023-04-10 14:44:20 -05:00
|
|
|
f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))} " \
|
|
|
|
"location checks." \
|
|
|
|
f"\nYou currently have {ctx.hint_points} points."
|
2021-10-29 10:03:15 +02:00
|
|
|
elif ctx.hint_cost == 0:
|
|
|
|
text += "\n!hint is free to use."
|
2024-10-01 16:55:34 -05:00
|
|
|
if ctx.stored_data and "_read_race_mode" in ctx.stored_data:
|
|
|
|
text += "\nRace mode is enabled." \
|
|
|
|
if ctx.stored_data["_read_race_mode"] else "\nRace mode is disabled."
|
2021-10-29 10:03:15 +02:00
|
|
|
else:
|
|
|
|
text += f"\nYou are not authenticated yet."
|
|
|
|
|
|
|
|
return text
|
|
|
|
|
|
|
|
else:
|
|
|
|
return "No current server connection. \nPlease connect to an Archipelago server."
|
|
|
|
|
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
class MainLayout(MDGridLayout):
|
2021-10-29 10:03:15 +02:00
|
|
|
pass
|
|
|
|
|
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
class ContainerLayout(MDFloatLayout):
|
2021-10-29 10:03:15 +02:00
|
|
|
pass
|
|
|
|
|
|
|
|
|
2021-11-19 21:25:01 +01:00
|
|
|
class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
|
|
|
|
RecycleBoxLayout):
|
|
|
|
""" Adds selection and focus behaviour to the view. """
|
|
|
|
|
|
|
|
|
2023-11-07 14:51:35 -06:00
|
|
|
class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
2021-11-19 21:25:01 +01:00
|
|
|
""" Add selection support to the Label """
|
|
|
|
index = None
|
|
|
|
selected = BooleanProperty(False)
|
|
|
|
|
|
|
|
def refresh_view_attrs(self, rv, index, data):
|
|
|
|
""" Catch and handle the view changes """
|
|
|
|
self.index = index
|
|
|
|
return super(SelectableLabel, self).refresh_view_attrs(
|
|
|
|
rv, index, data)
|
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
def on_size(self, instance_label, size: list) -> None:
|
|
|
|
super().on_size(instance_label, size)
|
|
|
|
if self.parent:
|
|
|
|
self.width = self.parent.width
|
|
|
|
|
2021-11-19 21:25:01 +01:00
|
|
|
def on_touch_down(self, touch):
|
|
|
|
""" Add selection on touch down """
|
|
|
|
if super(SelectableLabel, self).on_touch_down(touch):
|
|
|
|
return True
|
|
|
|
if self.collide_point(*touch.pos):
|
2021-11-22 17:44:14 +01:00
|
|
|
if self.selected:
|
|
|
|
self.parent.clear_selection()
|
|
|
|
else:
|
|
|
|
# Not a fan of the following few lines, but they work.
|
|
|
|
temp = MarkupLabel(text=self.text).markup
|
2025-04-03 09:22:02 +02:00
|
|
|
text = "".join(part for part in temp if not part.startswith("["))
|
2025-04-05 11:46:24 -05:00
|
|
|
cmdinput = MDApp.get_running_app().textinput
|
2024-06-01 14:32:41 +02:00
|
|
|
if not cmdinput.text:
|
2025-04-05 11:46:24 -05:00
|
|
|
input_text = get_input_text_from_response(text, MDApp.get_running_app().last_autofillable_command)
|
2024-06-01 14:32:41 +02:00
|
|
|
if input_text is not None:
|
|
|
|
cmdinput.text = input_text
|
2021-11-28 01:51:13 +01:00
|
|
|
|
2023-11-07 14:51:35 -06:00
|
|
|
Clipboard.copy(text.replace("&", "&").replace("&bl;", "[").replace("&br;", "]"))
|
2021-11-22 17:44:14 +01:00
|
|
|
return self.parent.select_with_touch(self.index, touch)
|
2021-11-19 21:25:01 +01:00
|
|
|
|
|
|
|
def apply_selection(self, rv, index, is_selected):
|
|
|
|
""" Respond to the selection of items in the view. """
|
|
|
|
self.selected = is_selected
|
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
|
|
|
|
class MarkupDropdownTextItem(MDDropdownTextItem):
|
|
|
|
def __init__(self):
|
|
|
|
super().__init__()
|
|
|
|
for child in self.children:
|
|
|
|
if child.__class__ == MDLabel:
|
|
|
|
child.markup = True
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
2025-04-15 17:09:27 -05:00
|
|
|
class AutocompleteHintInput(ResizableTextField):
|
2025-01-10 20:21:02 +01:00
|
|
|
min_chars = NumericProperty(3)
|
|
|
|
|
|
|
|
def __init__(self, **kwargs):
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
|
2025-04-15 17:09:27 -05:00
|
|
|
self.dropdown = MarkupDropdown(caller=self, position="bottom", border_margin=dp(2), width=self.width)
|
2025-01-10 20:21:02 +01:00
|
|
|
self.bind(on_text_validate=self.on_message)
|
2025-04-05 11:46:24 -05:00
|
|
|
self.bind(width=lambda instance, x: setattr(self.dropdown, "width", x))
|
2025-01-10 20:21:02 +01:00
|
|
|
|
|
|
|
def on_message(self, instance):
|
2025-04-05 11:46:24 -05:00
|
|
|
MDApp.get_running_app().commandprocessor("!hint "+instance.text)
|
2025-01-10 20:21:02 +01:00
|
|
|
|
|
|
|
def on_text(self, instance, value):
|
|
|
|
if len(value) >= self.min_chars:
|
2025-04-05 11:46:24 -05:00
|
|
|
self.dropdown.items.clear()
|
|
|
|
ctx: context_type = MDApp.get_running_app().ctx
|
2025-01-10 20:21:02 +01:00
|
|
|
if not ctx.game:
|
|
|
|
return
|
|
|
|
item_names = ctx.item_names._game_store[ctx.game].values()
|
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
def on_press(text):
|
|
|
|
split_text = MarkupLabel(text=text).markup
|
2025-04-15 17:09:27 -05:00
|
|
|
self.set_text(self, "".join(text_frag for text_frag in split_text
|
|
|
|
if not text_frag.startswith("[")))
|
|
|
|
self.dropdown.dismiss()
|
|
|
|
self.focus = True
|
|
|
|
|
2025-01-10 20:21:02 +01:00
|
|
|
lowered = value.lower()
|
|
|
|
for item_name in item_names:
|
|
|
|
try:
|
|
|
|
index = item_name.lower().index(lowered)
|
|
|
|
except ValueError:
|
|
|
|
pass # substring not found
|
|
|
|
else:
|
|
|
|
text = escape_markup(item_name)
|
|
|
|
text = text[:index] + "[b]" + text[index:index+len(value)]+"[/b]"+text[index+len(value):]
|
2025-04-05 11:46:24 -05:00
|
|
|
self.dropdown.items.append({
|
|
|
|
"text": text,
|
2025-04-15 17:09:27 -05:00
|
|
|
"on_release": lambda txt=text: on_press(txt),
|
2025-04-05 11:46:24 -05:00
|
|
|
"markup": True
|
|
|
|
})
|
|
|
|
if not self.dropdown.parent:
|
|
|
|
self.dropdown.open()
|
2025-01-10 20:21:02 +01:00
|
|
|
else:
|
|
|
|
self.dropdown.dismiss()
|
|
|
|
|
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
status_icons = {
|
|
|
|
HintStatus.HINT_NO_PRIORITY: "information",
|
|
|
|
HintStatus.HINT_PRIORITY: "exclamation-thick",
|
|
|
|
HintStatus.HINT_AVOID: "alert"
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class HintLabel(RecycleDataViewBehavior, MDBoxLayout):
|
2023-11-07 14:51:35 -06:00
|
|
|
selected = BooleanProperty(False)
|
|
|
|
striped = BooleanProperty(False)
|
|
|
|
index = None
|
2025-04-05 11:46:24 -05:00
|
|
|
dropdown: MDDropdownMenu
|
2023-11-07 14:51:35 -06:00
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
super(HintLabel, self).__init__()
|
|
|
|
self.receiving_text = ""
|
|
|
|
self.item_text = ""
|
|
|
|
self.finding_text = ""
|
|
|
|
self.location_text = ""
|
|
|
|
self.entrance_text = ""
|
2024-11-28 20:10:31 -05:00
|
|
|
self.status_text = ""
|
|
|
|
self.hint = {}
|
2023-11-07 14:51:35 -06:00
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
ctx = MDApp.get_running_app().ctx
|
|
|
|
menu_items = []
|
2024-11-28 20:10:31 -05:00
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
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)
|
|
|
|
})
|
2024-11-28 20:10:31 -05:00
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
self.dropdown = MDDropdownMenu(caller=self.ids["status"], items=menu_items)
|
2024-11-28 20:10:31 -05:00
|
|
|
|
|
|
|
def select(instance, data):
|
|
|
|
ctx.update_hint(self.hint["location"],
|
|
|
|
self.hint["finding_player"],
|
|
|
|
data)
|
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
self.dropdown.bind(on_release=self.dropdown.dismiss)
|
2024-11-28 20:10:31 -05:00
|
|
|
|
2023-11-07 14:51:35 -06:00
|
|
|
def set_height(self, instance, value):
|
|
|
|
self.height = max([child.texture_size[1] for child in self.children])
|
|
|
|
|
|
|
|
def refresh_view_attrs(self, rv, index, data):
|
|
|
|
self.index = index
|
2024-03-12 14:52:57 +01:00
|
|
|
self.striped = data.get("striped", False)
|
2023-11-07 14:51:35 -06:00
|
|
|
self.receiving_text = data["receiving"]["text"]
|
|
|
|
self.item_text = data["item"]["text"]
|
|
|
|
self.finding_text = data["finding"]["text"]
|
|
|
|
self.location_text = data["location"]["text"]
|
|
|
|
self.entrance_text = data["entrance"]["text"]
|
2024-11-28 20:10:31 -05:00
|
|
|
self.status_text = data["status"]["text"]
|
|
|
|
self.hint = data["status"]["hint"]
|
2023-11-07 14:51:35 -06:00
|
|
|
return super(HintLabel, self).refresh_view_attrs(rv, index, data)
|
|
|
|
|
|
|
|
def on_touch_down(self, touch):
|
|
|
|
""" Add selection on touch down """
|
|
|
|
if super(HintLabel, self).on_touch_down(touch):
|
|
|
|
return True
|
2024-03-12 14:52:57 +01:00
|
|
|
if self.index: # skip header
|
2023-11-07 14:51:35 -06:00
|
|
|
if self.collide_point(*touch.pos):
|
2024-11-28 20:10:31 -05:00
|
|
|
status_label = self.ids["status"]
|
|
|
|
if status_label.collide_point(*touch.pos):
|
|
|
|
if self.hint["status"] == HintStatus.HINT_FOUND:
|
|
|
|
return
|
2025-04-05 11:46:24 -05:00
|
|
|
ctx = MDApp.get_running_app().ctx
|
2024-12-10 20:35:36 +01:00
|
|
|
if ctx.slot_concerns_self(self.hint["receiving_player"]): # If this player owns this hint
|
2024-11-28 20:10:31 -05:00
|
|
|
# open a dropdown
|
2025-04-05 11:46:24 -05:00
|
|
|
self.dropdown.open()
|
2024-11-28 20:10:31 -05:00
|
|
|
elif self.selected:
|
2023-11-07 14:51:35 -06:00
|
|
|
self.parent.clear_selection()
|
|
|
|
else:
|
2024-03-12 14:52:57 +01:00
|
|
|
text = "".join((self.receiving_text, "\'s ", self.item_text, " is at ", self.location_text, " in ",
|
2023-11-07 14:51:35 -06:00
|
|
|
self.finding_text, "\'s World", (" at " + self.entrance_text)
|
|
|
|
if self.entrance_text != "Vanilla"
|
2024-11-28 20:10:31 -05:00
|
|
|
else "", ". (", self.status_text.lower(), ")"))
|
2023-11-07 14:51:35 -06:00
|
|
|
temp = MarkupLabel(text).markup
|
2025-04-08 12:06:19 -04:00
|
|
|
text = "".join(part for part in temp if not part.startswith("["))
|
2023-11-07 14:51:35 -06:00
|
|
|
Clipboard.copy(escape_markup(text).replace("&", "&").replace("&bl;", "[").replace("&br;", "]"))
|
|
|
|
return self.parent.select_with_touch(self.index, touch)
|
2024-03-12 14:52:57 +01:00
|
|
|
else:
|
|
|
|
parent = self.parent
|
|
|
|
parent.clear_selection()
|
|
|
|
parent: HintLog = parent.parent
|
|
|
|
# find correct column
|
|
|
|
for child in self.children:
|
|
|
|
if child.collide_point(*touch.pos):
|
|
|
|
key = child.sort_key
|
2024-11-28 20:10:31 -05:00
|
|
|
if key == "status":
|
2025-02-10 19:19:00 +01:00
|
|
|
parent.hint_sorter = lambda element: status_sort_weights[element["status"]["hint"]["status"]]
|
|
|
|
else:
|
|
|
|
parent.hint_sorter = lambda element: (
|
|
|
|
remove_between_brackets.sub("", element[key]["text"]).lower()
|
|
|
|
)
|
2024-03-12 14:52:57 +01:00
|
|
|
if key == parent.sort_key:
|
|
|
|
# second click reverses order
|
|
|
|
parent.reversed = not parent.reversed
|
|
|
|
else:
|
|
|
|
parent.sort_key = key
|
|
|
|
parent.reversed = False
|
2025-04-05 11:46:24 -05:00
|
|
|
MDApp.get_running_app().update_hints()
|
2023-11-07 14:51:35 -06:00
|
|
|
|
|
|
|
def apply_selection(self, rv, index, is_selected):
|
|
|
|
""" Respond to the selection of items in the view. """
|
2024-03-12 14:52:57 +01:00
|
|
|
if self.index:
|
2023-11-07 14:51:35 -06:00
|
|
|
self.selected = is_selected
|
|
|
|
|
|
|
|
|
2025-04-15 17:09:27 -05:00
|
|
|
class ConnectBarTextInput(ResizableTextField):
|
2022-02-28 18:25:07 -08:00
|
|
|
def insert_text(self, substring, from_undo=False):
|
2023-11-07 14:51:35 -06:00
|
|
|
s = substring.replace("\n", "").replace("\r", "")
|
2022-02-28 18:25:07 -08:00
|
|
|
return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo)
|
|
|
|
|
|
|
|
|
2024-06-08 19:08:47 -07:00
|
|
|
def is_command_input(string: str) -> bool:
|
|
|
|
return len(string) > 0 and string[0] in "/!"
|
|
|
|
|
|
|
|
|
2025-04-15 17:09:27 -05:00
|
|
|
class CommandPromptTextInput(ResizableTextField):
|
2024-06-08 19:08:47 -07:00
|
|
|
MAXIMUM_HISTORY_MESSAGES = 50
|
|
|
|
|
|
|
|
def __init__(self, **kwargs) -> None:
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
self._command_history_index = -1
|
|
|
|
self._command_history: typing.Deque[str] = deque(maxlen=CommandPromptTextInput.MAXIMUM_HISTORY_MESSAGES)
|
2025-04-15 17:09:27 -05:00
|
|
|
|
2024-06-08 19:08:47 -07:00
|
|
|
def update_history(self, new_entry: str) -> None:
|
|
|
|
self._command_history_index = -1
|
|
|
|
if is_command_input(new_entry):
|
|
|
|
self._command_history.appendleft(new_entry)
|
|
|
|
|
|
|
|
def keyboard_on_key_down(
|
|
|
|
self,
|
|
|
|
window,
|
|
|
|
keycode: typing.Tuple[int, str],
|
|
|
|
text: typing.Optional[str],
|
|
|
|
modifiers: typing.List[str]
|
|
|
|
) -> bool:
|
|
|
|
"""
|
|
|
|
:param window: The kivy window object
|
|
|
|
:param keycode: A tuple of (keycode, keyname). Keynames are always lowercase
|
|
|
|
:param text: The text printed by this key, not accounting for modifiers, or `None` if no text.
|
|
|
|
Seems to pretty naively interpret the keycode as unicode, so numlock can return odd characters.
|
|
|
|
:param modifiers: A list of string modifiers, like `ctrl` or `numlock`
|
|
|
|
"""
|
|
|
|
if keycode[1] == 'up':
|
|
|
|
self._change_to_history_text_if_available(self._command_history_index + 1)
|
|
|
|
return True
|
|
|
|
if keycode[1] == 'down':
|
|
|
|
self._change_to_history_text_if_available(self._command_history_index - 1)
|
|
|
|
return True
|
|
|
|
return super().keyboard_on_key_down(window, keycode, text, modifiers)
|
2025-04-15 17:09:27 -05:00
|
|
|
|
2024-06-08 19:08:47 -07:00
|
|
|
def _change_to_history_text_if_available(self, new_index: int) -> None:
|
|
|
|
if new_index < -1:
|
|
|
|
return
|
|
|
|
if new_index >= len(self._command_history):
|
|
|
|
return
|
|
|
|
self._command_history_index = new_index
|
|
|
|
if new_index == -1:
|
|
|
|
self.text = ""
|
|
|
|
return
|
|
|
|
self.text = self._command_history[self._command_history_index]
|
|
|
|
|
|
|
|
|
2022-06-04 17:02:02 +02:00
|
|
|
class MessageBox(Popup):
|
2025-04-05 11:46:24 -05:00
|
|
|
class MessageBoxLabel(MDLabel):
|
2022-06-04 17:02:02 +02:00
|
|
|
def __init__(self, **kwargs):
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
self._label.refresh()
|
|
|
|
|
|
|
|
def __init__(self, title, text, error=False, **kwargs):
|
|
|
|
label = MessageBox.MessageBoxLabel(text=text)
|
|
|
|
separator_color = [217 / 255, 129 / 255, 122 / 255, 1.] if error else [47 / 255., 167 / 255., 212 / 255, 1.]
|
2025-04-15 17:09:27 -05:00
|
|
|
super().__init__(title=title, content=label, size_hint=(0.5, None), width=max(100, int(label.width) + 40),
|
2022-06-04 17:02:02 +02:00
|
|
|
separator_color=separator_color, **kwargs)
|
|
|
|
self.height += max(0, label.height - 18)
|
|
|
|
|
|
|
|
|
2025-04-15 17:09:27 -05:00
|
|
|
class ClientTabs(MDTabsSecondary):
|
2025-04-05 11:46:24 -05:00
|
|
|
carousel: MDTabsCarousel
|
|
|
|
lock_swiping = True
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
2025-04-15 17:09:27 -05:00
|
|
|
self.carousel = MDTabsCarousel(lock_swiping=True, anim_move_duration=0.2)
|
|
|
|
super().__init__(*args, MDDivider(size_hint_y=None, height=dp(1)), self.carousel, **kwargs)
|
2025-04-05 11:46:24 -05:00
|
|
|
self.size_hint_y = 1
|
|
|
|
|
2025-04-15 17:09:27 -05:00
|
|
|
def _check_panel_height(self, *args):
|
|
|
|
self.ids.tab_scroll.height = dp(38)
|
|
|
|
|
|
|
|
def update_indicator(
|
|
|
|
self, x: float = 0.0, w: float = 0.0, instance: MDTabsItem = None
|
|
|
|
) -> None:
|
|
|
|
def update_indicator(*args):
|
|
|
|
indicator_pos = (0, 0)
|
|
|
|
indicator_size = (0, 0)
|
|
|
|
|
|
|
|
item_text_object = self._get_tab_item_text_icon_object()
|
|
|
|
|
|
|
|
if item_text_object:
|
|
|
|
indicator_pos = (
|
|
|
|
instance.x + dp(12),
|
|
|
|
self.indicator.pos[1]
|
|
|
|
if not self._tabs_carousel
|
|
|
|
else self._tabs_carousel.height,
|
|
|
|
)
|
|
|
|
indicator_size = (
|
|
|
|
instance.width - dp(24),
|
|
|
|
self.indicator_height,
|
|
|
|
)
|
|
|
|
|
|
|
|
Animation(
|
|
|
|
pos=indicator_pos,
|
|
|
|
size=indicator_size,
|
|
|
|
d=0 if not self.indicator_anim else self.indicator_duration,
|
|
|
|
t=self.indicator_transition,
|
|
|
|
).start(self.indicator)
|
|
|
|
|
|
|
|
if not instance:
|
|
|
|
self.indicator.pos = (x, self.indicator.pos[1])
|
|
|
|
self.indicator.size = (w, self.indicator_height)
|
|
|
|
else:
|
|
|
|
Clock.schedule_once(update_indicator)
|
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2025-04-15 17:09:27 -05:00
|
|
|
class CommandButton(MDButton, MDTooltip):
|
|
|
|
def __init__(self, *args, manager: "GameManager", **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.manager = manager
|
|
|
|
self._tooltip = ToolTip(text="Test")
|
|
|
|
|
|
|
|
def on_enter(self):
|
|
|
|
self._tooltip.text = self.manager.commandprocessor.get_help_text()
|
|
|
|
self._tooltip.font_size = dp(20 - (len(self._tooltip.text) // 400)) # mostly guessing on the numbers here
|
|
|
|
self.display_tooltip()
|
|
|
|
|
|
|
|
def on_leave(self):
|
|
|
|
self.animation_tooltip_dismiss()
|
|
|
|
|
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
class GameManager(ThemedApp):
|
2021-07-30 20:18:03 +02:00
|
|
|
logging_pairs = [
|
|
|
|
("Client", "Archipelago"),
|
|
|
|
]
|
2021-11-29 21:35:06 +01:00
|
|
|
base_title: str = "Archipelago Client"
|
|
|
|
last_autofillable_command: str
|
2021-07-30 20:18:03 +02:00
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
main_area_container: MDGridLayout
|
2022-10-27 02:30:22 -07:00
|
|
|
""" subclasses can add more columns beside the tabs """
|
|
|
|
|
2021-10-19 05:38:17 +02:00
|
|
|
def __init__(self, ctx: context_type):
|
|
|
|
self.title = self.base_title
|
2021-07-30 20:18:03 +02:00
|
|
|
self.ctx = ctx
|
|
|
|
self.commandprocessor = ctx.command_processor(ctx)
|
|
|
|
self.icon = r"data/icon.png"
|
|
|
|
self.json_to_kivy_parser = KivyJSONtoTextParser(ctx)
|
2024-06-08 19:08:47 -07:00
|
|
|
self.log_panels: typing.Dict[str, Widget] = {}
|
2021-11-29 21:35:06 +01:00
|
|
|
|
|
|
|
# keep track of last used command to autofill on click
|
|
|
|
self.last_autofillable_command = "hint"
|
2021-12-02 03:14:26 +01:00
|
|
|
autofillable_commands = ("hint_location", "hint", "getitem")
|
2021-11-29 21:35:06 +01:00
|
|
|
original_say = ctx.on_user_say
|
|
|
|
|
|
|
|
def intercept_say(text):
|
|
|
|
text = original_say(text)
|
|
|
|
if text:
|
|
|
|
for command in autofillable_commands:
|
2022-02-24 04:47:01 +01:00
|
|
|
if text.startswith("!" + command):
|
2021-11-29 21:35:06 +01:00
|
|
|
self.last_autofillable_command = command
|
|
|
|
break
|
|
|
|
return text
|
2022-02-24 04:47:01 +01:00
|
|
|
|
2021-11-29 21:35:06 +01:00
|
|
|
ctx.on_user_say = intercept_say
|
|
|
|
|
2021-07-30 20:18:03 +02:00
|
|
|
super(GameManager, self).__init__()
|
|
|
|
|
2023-01-24 03:42:34 +01:00
|
|
|
@property
|
|
|
|
def tab_count(self):
|
|
|
|
if hasattr(self, "tabs"):
|
|
|
|
return max(1, len(self.tabs.tab_list))
|
|
|
|
return 1
|
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
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)
|
|
|
|
|
2022-10-27 02:30:22 -07:00
|
|
|
def build(self) -> Layout:
|
2025-04-05 11:46:24 -05:00
|
|
|
self.set_colors()
|
2021-10-29 10:03:15 +02:00
|
|
|
self.container = ContainerLayout()
|
|
|
|
|
|
|
|
self.grid = MainLayout()
|
2021-07-30 20:18:03 +02:00
|
|
|
self.grid.cols = 1
|
2025-04-15 17:09:27 -05:00
|
|
|
self.connect_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(40),
|
2025-04-05 11:46:24 -05:00
|
|
|
spacing=5, padding=(5, 10))
|
2021-10-22 05:25:09 +02:00
|
|
|
# top part
|
2025-04-15 17:09:27 -05:00
|
|
|
server_label = ServerLabel(width=dp(75))
|
2022-02-24 04:47:01 +01:00
|
|
|
self.connect_layout.add_widget(server_label)
|
2023-11-07 14:51:35 -06:00
|
|
|
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:",
|
2025-04-15 17:09:27 -05:00
|
|
|
pos_hint={"center_x": 0.5, "center_y": 0.5})
|
2023-11-07 14:51:35 -06:00
|
|
|
|
2022-11-01 06:54:40 +01:00
|
|
|
def connect_bar_validate(sender):
|
|
|
|
if not self.ctx.server:
|
|
|
|
self.connect_button_action(sender)
|
2023-11-07 14:51:35 -06:00
|
|
|
|
2025-04-15 17:09:27 -05:00
|
|
|
self.server_connect_bar.height = dp(30)
|
2022-11-01 06:54:40 +01:00
|
|
|
self.server_connect_bar.bind(on_text_validate=connect_bar_validate)
|
2022-02-24 04:47:01 +01:00
|
|
|
self.connect_layout.add_widget(self.server_connect_bar)
|
2025-04-05 11:46:24 -05:00
|
|
|
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})
|
2021-10-19 05:38:17 +02:00
|
|
|
self.server_connect_button.bind(on_press=self.connect_button_action)
|
2025-04-05 11:46:24 -05:00
|
|
|
self.server_connect_button.height = self.server_connect_bar.height
|
2022-02-24 04:47:01 +01:00
|
|
|
self.connect_layout.add_widget(self.server_connect_button)
|
|
|
|
self.grid.add_widget(self.connect_layout)
|
2025-04-05 11:46:24 -05:00
|
|
|
self.progressbar = MDLinearProgressIndicator(size_hint_y=None, height=3)
|
2021-10-22 05:25:09 +02:00
|
|
|
self.grid.add_widget(self.progressbar)
|
|
|
|
|
|
|
|
# middle part
|
2025-04-15 17:09:27 -05:00
|
|
|
self.tabs = ClientTabs(pos_hint={"center_x": 0.5, "center_y": 0.5})
|
2025-04-05 11:46:24 -05:00
|
|
|
self.tabs.add_widget(MDTabsItem(MDTabsItemText(text="All" if len(self.logging_pairs) > 1 else "Archipelago")))
|
2021-07-30 20:18:03 +02:00
|
|
|
self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name)
|
2025-04-05 11:46:24 -05:00
|
|
|
for logger_name, name in
|
|
|
|
self.logging_pairs))
|
|
|
|
self.tabs.carousel.add_widget(self.tabs.default_tab_content)
|
2021-07-30 20:18:03 +02:00
|
|
|
|
|
|
|
for logger_name, display_name in self.logging_pairs:
|
|
|
|
bridge_logger = logging.getLogger(logger_name)
|
2025-04-05 11:46:24 -05:00
|
|
|
self.log_panels[display_name] = UILog(bridge_logger)
|
2023-11-07 14:51:35 -06:00
|
|
|
if len(self.logging_pairs) > 1:
|
2025-04-05 11:46:24 -05:00
|
|
|
panel = MDTabsItem(MDTabsItemText(text=display_name))
|
|
|
|
panel.content = self.log_panels[display_name]
|
2023-11-07 14:51:35 -06:00
|
|
|
# show Archipelago tab if other logging is present
|
2025-04-05 11:46:24 -05:00
|
|
|
self.tabs.carousel.add_widget(panel.content)
|
2023-11-07 14:51:35 -06:00
|
|
|
self.tabs.add_widget(panel)
|
|
|
|
|
2025-01-10 20:21:02 +01:00
|
|
|
hint_panel = self.add_client_tab("Hints", HintLayout())
|
|
|
|
self.hint_log = HintLog(self.json_to_kivy_parser)
|
2024-09-22 09:24:14 -05:00
|
|
|
self.log_panels["Hints"] = hint_panel.content
|
2025-01-10 20:21:02 +01:00
|
|
|
hint_panel.content.add_widget(self.hint_log)
|
2023-11-07 14:51:35 -06:00
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
self.main_area_container = MDGridLayout(size_hint_y=1, rows=1)
|
2022-10-27 02:30:22 -07:00
|
|
|
self.main_area_container.add_widget(self.tabs)
|
|
|
|
|
|
|
|
self.grid.add_widget(self.main_area_container)
|
2021-10-22 00:37:20 +02:00
|
|
|
|
2021-10-22 05:25:09 +02:00
|
|
|
# bottom part
|
2025-04-15 17:09:27 -05:00
|
|
|
bottom_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(40), spacing=5, padding=(5, 10))
|
|
|
|
info_button = CommandButton(MDButtonText(text="Command:", halign="left"), manager=self, radius=5,
|
|
|
|
style="filled", size=(dp(100), dp(70)), size_hint_x=None, size_hint_y=None,
|
|
|
|
pos_hint={"center_y": 0.575})
|
2021-10-24 23:22:06 +02:00
|
|
|
info_button.bind(on_release=self.command_button_action)
|
2021-10-22 05:25:09 +02:00
|
|
|
bottom_layout.add_widget(info_button)
|
2024-06-08 19:08:47 -07:00
|
|
|
self.textinput = CommandPromptTextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
|
2021-11-22 17:44:14 +01:00
|
|
|
self.textinput.bind(on_text_validate=self.on_message)
|
2025-04-05 11:46:24 -05:00
|
|
|
info_button.height = self.textinput.height
|
2022-11-01 06:54:40 +01:00
|
|
|
self.textinput.text_validate_unfocus = False
|
2021-11-22 17:44:14 +01:00
|
|
|
bottom_layout.add_widget(self.textinput)
|
2021-10-22 05:25:09 +02:00
|
|
|
self.grid.add_widget(bottom_layout)
|
2021-07-30 20:18:03 +02:00
|
|
|
self.commandprocessor("/help")
|
2021-10-19 05:38:17 +02:00
|
|
|
Clock.schedule_interval(self.update_texts, 1 / 30)
|
2021-10-29 10:03:15 +02:00
|
|
|
self.container.add_widget(self.grid)
|
2022-11-01 06:54:40 +01:00
|
|
|
|
2023-01-23 04:59:51 +01:00
|
|
|
# If the address contains a port, select it; otherwise, select the host.
|
|
|
|
s = self.server_connect_bar.text
|
|
|
|
host_start = s.find("@") + 1
|
|
|
|
ipv6_end = s.find("]", host_start) + 1
|
|
|
|
port_start = s.find(":", ipv6_end if ipv6_end > 0 else host_start) + 1
|
2022-11-01 06:54:40 +01:00
|
|
|
self.server_connect_bar.focus = True
|
2023-01-23 04:59:51 +01:00
|
|
|
self.server_connect_bar.select_text(port_start if port_start > 0 else host_start, len(s))
|
2022-11-01 06:54:40 +01:00
|
|
|
|
2025-04-15 17:09:27 -05:00
|
|
|
# Uncomment to enable the kivy live editor console
|
|
|
|
# Press Ctrl-E (with numlock/capslock) disabled to open
|
|
|
|
# from kivy.core.window import Window
|
|
|
|
# from kivy.modules import console
|
|
|
|
# console.create_console(Window, self.container)
|
|
|
|
|
2021-10-29 10:03:15 +02:00
|
|
|
return self.container
|
2021-07-30 20:18:03 +02:00
|
|
|
|
2025-04-15 17:09:27 -05:00
|
|
|
def add_client_tab(self, title: str, content: Widget, index: int = -1) -> Widget:
|
2024-09-22 09:24:14 -05:00
|
|
|
"""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."""
|
2025-04-05 11:46:24 -05:00
|
|
|
new_tab = MDTabsItem(MDTabsItemText(text=title))
|
2024-09-22 09:24:14 -05:00
|
|
|
new_tab.content = content
|
2025-04-15 17:09:27 -05:00
|
|
|
if -1 < index <= len(self.tabs.carousel.slides):
|
|
|
|
new_tab.bind(on_release=self.tabs.set_active_item)
|
|
|
|
new_tab._tabs = self.tabs
|
|
|
|
self.tabs.ids.container.add_widget(new_tab, index=index)
|
|
|
|
self.tabs.carousel.add_widget(new_tab.content, index=len(self.tabs.carousel.slides) - index)
|
|
|
|
else:
|
|
|
|
self.tabs.add_widget(new_tab)
|
|
|
|
self.tabs.carousel.add_widget(new_tab.content)
|
2024-09-22 09:24:14 -05:00
|
|
|
return new_tab
|
|
|
|
|
2021-10-19 05:38:17 +02:00
|
|
|
def update_texts(self, dt):
|
2025-04-05 11:46:24 -05:00
|
|
|
for slide in self.tabs.carousel.slides:
|
|
|
|
if hasattr(slide, "fix_heights"):
|
|
|
|
slide.fix_heights() # TODO: remove this when Kivy fixes this upstream
|
2021-10-19 05:38:17 +02:00
|
|
|
if self.ctx.server:
|
2021-10-29 15:18:58 +02:00
|
|
|
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)}"
|
2025-04-05 11:46:24 -05:00
|
|
|
self.server_connect_button._button_text.text = "Disconnect"
|
2022-11-01 06:54:40 +01:00
|
|
|
self.server_connect_bar.readonly = True
|
2021-10-22 05:25:09 +02:00
|
|
|
self.progressbar.max = len(self.ctx.checked_locations) + len(self.ctx.missing_locations)
|
|
|
|
self.progressbar.value = len(self.ctx.checked_locations)
|
2021-10-19 05:38:17 +02:00
|
|
|
else:
|
2025-04-05 11:46:24 -05:00
|
|
|
self.server_connect_button._button_text.text = "Connect"
|
2022-11-01 06:54:40 +01:00
|
|
|
self.server_connect_bar.readonly = False
|
2021-10-29 15:18:58 +02:00
|
|
|
self.title = self.base_title + " " + Utils.__version__
|
2021-10-22 05:25:09 +02:00
|
|
|
self.progressbar.value = 0
|
2021-10-19 05:38:17 +02:00
|
|
|
|
2021-10-24 23:22:06 +02:00
|
|
|
def command_button_action(self, button):
|
2021-11-19 21:25:01 +01:00
|
|
|
if self.ctx.server:
|
|
|
|
logging.getLogger("Client").info("/help for client commands and !help for server commands.")
|
|
|
|
else:
|
|
|
|
logging.getLogger("Client").info("/help for client commands and once you are connected, "
|
|
|
|
"!help for server commands.")
|
2021-10-24 23:22:06 +02:00
|
|
|
|
2021-10-19 05:38:17 +02:00
|
|
|
def connect_button_action(self, button):
|
2024-07-21 18:12:11 -05:00
|
|
|
self.ctx.username = None
|
2024-07-25 01:21:51 -05:00
|
|
|
self.ctx.password = None
|
2021-10-19 05:38:17 +02:00
|
|
|
if self.ctx.server:
|
2022-11-02 07:51:35 -07:00
|
|
|
async_start(self.ctx.disconnect())
|
2021-10-19 05:38:17 +02:00
|
|
|
else:
|
2022-11-02 07:51:35 -07:00
|
|
|
async_start(self.ctx.connect(self.server_connect_bar.text.replace("/connect ", "")))
|
2021-10-19 05:38:17 +02:00
|
|
|
|
2021-07-30 20:18:03 +02:00
|
|
|
def on_stop(self):
|
2021-10-29 10:03:15 +02:00
|
|
|
# "kill" input tasks
|
|
|
|
for x in range(self.ctx.input_requests):
|
|
|
|
self.ctx.input_queue.put_nowait("")
|
|
|
|
self.ctx.input_requests = 0
|
|
|
|
|
2021-07-30 20:18:03 +02:00
|
|
|
self.ctx.exit_event.set()
|
|
|
|
|
2024-06-08 19:08:47 -07:00
|
|
|
def on_message(self, textinput: CommandPromptTextInput):
|
2021-07-30 20:18:03 +02:00
|
|
|
try:
|
|
|
|
input_text = textinput.text.strip()
|
|
|
|
textinput.text = ""
|
2024-06-08 19:08:47 -07:00
|
|
|
textinput.update_history(input_text)
|
2021-07-30 20:18:03 +02:00
|
|
|
|
|
|
|
if self.ctx.input_requests > 0:
|
|
|
|
self.ctx.input_requests -= 1
|
|
|
|
self.ctx.input_queue.put_nowait(input_text)
|
2024-06-08 19:08:47 -07:00
|
|
|
elif is_command_input(input_text):
|
|
|
|
self.ctx.on_ui_command(input_text)
|
|
|
|
self.commandprocessor(input_text)
|
2021-07-30 20:18:03 +02:00
|
|
|
elif input_text:
|
|
|
|
self.commandprocessor(input_text)
|
2021-11-21 05:47:19 +01:00
|
|
|
|
2021-07-30 20:18:03 +02:00
|
|
|
except Exception as e:
|
|
|
|
logging.getLogger("Client").exception(e)
|
|
|
|
|
2022-01-18 05:52:29 +01:00
|
|
|
def print_json(self, data: typing.List[JSONMessagePart]):
|
2021-07-30 20:18:03 +02:00
|
|
|
text = self.json_to_kivy_parser(data)
|
|
|
|
self.log_panels["Archipelago"].on_message_markup(text)
|
|
|
|
self.log_panels["All"].on_message_markup(text)
|
|
|
|
|
2022-11-01 06:54:40 +01:00
|
|
|
def focus_textinput(self):
|
|
|
|
if hasattr(self, "textinput"):
|
|
|
|
self.textinput.focus = True
|
|
|
|
|
2022-06-27 03:10:41 -07:00
|
|
|
def update_address_bar(self, text: str):
|
|
|
|
if hasattr(self, "server_connect_bar"):
|
|
|
|
self.server_connect_bar.text = text
|
|
|
|
else:
|
|
|
|
logging.getLogger("Client").info("Could not update address bar as the GUI is not yet initialized.")
|
|
|
|
|
2022-02-24 04:47:01 +01:00
|
|
|
def enable_energy_link(self):
|
|
|
|
if not hasattr(self, "energy_link_label"):
|
2025-04-05 11:46:24 -05:00
|
|
|
self.energy_link_label = MDLabel(text="Energy Link: Standby",
|
|
|
|
size_hint_x=None, width=150, halign="center")
|
2022-02-24 04:47:01 +01:00
|
|
|
self.connect_layout.add_widget(self.energy_link_label)
|
|
|
|
|
|
|
|
def set_new_energy_link_value(self):
|
|
|
|
if hasattr(self, "energy_link_label"):
|
|
|
|
self.energy_link_label.text = f"EL: {Utils.format_SI_prefix(self.ctx.current_energy_link_value)}J"
|
|
|
|
|
2023-11-07 14:51:35 -06:00
|
|
|
def update_hints(self):
|
2024-11-28 20:10:31 -05:00
|
|
|
hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}", [])
|
2025-01-10 20:21:02 +01:00
|
|
|
self.hint_log.refresh_hints(hints)
|
2023-11-07 14:51:35 -06:00
|
|
|
|
2023-03-29 05:25:29 +02:00
|
|
|
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
|
|
|
|
def open_settings(self, *largs):
|
|
|
|
pass
|
|
|
|
|
2021-07-30 20:18:03 +02:00
|
|
|
|
|
|
|
class LogtoUI(logging.Handler):
|
|
|
|
def __init__(self, on_log):
|
2021-11-17 22:46:32 +01:00
|
|
|
super(LogtoUI, self).__init__(logging.INFO)
|
2021-07-30 20:18:03 +02:00
|
|
|
self.on_log = on_log
|
|
|
|
|
2022-06-07 00:15:08 +02:00
|
|
|
@staticmethod
|
|
|
|
def format_compact(record: logging.LogRecord) -> str:
|
|
|
|
if isinstance(record.msg, Exception):
|
|
|
|
return str(record.msg)
|
2023-11-07 14:51:35 -06:00
|
|
|
return (f"{record.exc_info[1]}\n" if record.exc_info else "") + str(record.msg).split("\n")[0]
|
2022-06-07 00:15:08 +02:00
|
|
|
|
2021-07-30 20:18:03 +02:00
|
|
|
def handle(self, record: logging.LogRecord) -> None:
|
2023-11-07 14:51:35 -06:00
|
|
|
if getattr(record, "skip_gui", False):
|
2022-06-07 00:15:08 +02:00
|
|
|
pass # skip output
|
2023-11-07 14:51:35 -06:00
|
|
|
elif getattr(record, "compact_gui", False):
|
2022-06-07 00:15:08 +02:00
|
|
|
self.on_log(self.format_compact(record))
|
|
|
|
else:
|
|
|
|
self.on_log(self.format(record))
|
2021-07-30 20:18:03 +02:00
|
|
|
|
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
class UILog(MDRecycleView):
|
2023-02-13 03:50:05 +01:00
|
|
|
messages: typing.ClassVar[int] # comes from kv file
|
2025-04-05 11:46:24 -05:00
|
|
|
adaptive_height = True
|
2021-07-30 20:18:03 +02:00
|
|
|
|
|
|
|
def __init__(self, *loggers_to_handle, **kwargs):
|
|
|
|
super(UILog, self).__init__(**kwargs)
|
|
|
|
self.data = []
|
|
|
|
for logger in loggers_to_handle:
|
|
|
|
logger.addHandler(LogtoUI(self.on_log))
|
|
|
|
|
2021-11-01 06:40:29 +01:00
|
|
|
def on_log(self, record: str) -> None:
|
|
|
|
self.data.append({"text": escape_markup(record)})
|
2023-02-13 03:50:05 +01:00
|
|
|
self.clean_old()
|
2021-07-30 20:18:03 +02:00
|
|
|
|
|
|
|
def on_message_markup(self, text):
|
|
|
|
self.data.append({"text": text})
|
2023-02-13 03:50:05 +01:00
|
|
|
self.clean_old()
|
|
|
|
|
|
|
|
def clean_old(self):
|
|
|
|
if len(self.data) > self.messages:
|
|
|
|
self.data.pop(0)
|
2021-07-30 20:18:03 +02:00
|
|
|
|
2022-01-23 23:31:49 +01:00
|
|
|
def fix_heights(self):
|
|
|
|
"""Workaround fix for divergent texture and layout heights"""
|
|
|
|
for element in self.children[0].children:
|
|
|
|
if element.height != element.texture_size[1]:
|
|
|
|
element.height = element.texture_size[1]
|
|
|
|
|
2021-07-30 20:18:03 +02:00
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
class HintLayout(MDBoxLayout):
|
2025-01-10 20:21:02 +01:00
|
|
|
orientation = "vertical"
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
2025-04-15 17:09:27 -05:00
|
|
|
boxlayout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(40))
|
|
|
|
boxlayout.add_widget(MDLabel(text="New Hint:", size_hint_x=None, size_hint_y=None,
|
|
|
|
height=dp(40), width=dp(75), halign="center", valign="center"))
|
2025-01-10 20:21:02 +01:00
|
|
|
boxlayout.add_widget(AutocompleteHintInput())
|
|
|
|
self.add_widget(boxlayout)
|
|
|
|
|
2025-04-01 11:06:49 -05:00
|
|
|
def fix_heights(self):
|
|
|
|
for child in self.children:
|
|
|
|
fix_func = getattr(child, "fix_heights", None)
|
|
|
|
if fix_func:
|
|
|
|
fix_func()
|
|
|
|
|
2025-04-15 17:09:27 -05:00
|
|
|
|
2024-11-28 20:10:31 -05:00
|
|
|
status_names: typing.Dict[HintStatus, str] = {
|
|
|
|
HintStatus.HINT_FOUND: "Found",
|
|
|
|
HintStatus.HINT_UNSPECIFIED: "Unspecified",
|
|
|
|
HintStatus.HINT_NO_PRIORITY: "No Priority",
|
|
|
|
HintStatus.HINT_AVOID: "Avoid",
|
|
|
|
HintStatus.HINT_PRIORITY: "Priority",
|
|
|
|
}
|
|
|
|
status_colors: typing.Dict[HintStatus, str] = {
|
|
|
|
HintStatus.HINT_FOUND: "green",
|
|
|
|
HintStatus.HINT_UNSPECIFIED: "white",
|
|
|
|
HintStatus.HINT_NO_PRIORITY: "cyan",
|
|
|
|
HintStatus.HINT_AVOID: "salmon",
|
|
|
|
HintStatus.HINT_PRIORITY: "plum",
|
|
|
|
}
|
2025-02-10 19:19:00 +01:00
|
|
|
status_sort_weights: dict[HintStatus, int] = {
|
|
|
|
HintStatus.HINT_FOUND: 0,
|
|
|
|
HintStatus.HINT_UNSPECIFIED: 1,
|
|
|
|
HintStatus.HINT_NO_PRIORITY: 2,
|
|
|
|
HintStatus.HINT_AVOID: 3,
|
|
|
|
HintStatus.HINT_PRIORITY: 4,
|
|
|
|
}
|
2024-11-28 20:10:31 -05:00
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
class HintLog(MDRecycleView):
|
2023-11-07 14:51:35 -06:00
|
|
|
header = {
|
|
|
|
"receiving": {"text": "[u]Receiving Player[/u]"},
|
|
|
|
"item": {"text": "[u]Item[/u]"},
|
|
|
|
"finding": {"text": "[u]Finding Player[/u]"},
|
|
|
|
"location": {"text": "[u]Location[/u]"},
|
|
|
|
"entrance": {"text": "[u]Entrance[/u]"},
|
2024-11-28 20:10:31 -05:00
|
|
|
"status": {"text": "[u]Status[/u]",
|
|
|
|
"hint": {"receiving_player": -1, "location": -1, "finding_player": -1, "status": ""}},
|
2023-11-07 14:51:35 -06:00
|
|
|
"striped": True,
|
|
|
|
}
|
2025-04-05 11:46:24 -05:00
|
|
|
data: list[typing.Any]
|
2024-03-12 14:52:57 +01:00
|
|
|
sort_key: str = ""
|
2024-11-28 20:10:31 -05:00
|
|
|
reversed: bool = True
|
2024-03-12 14:52:57 +01:00
|
|
|
|
2023-11-07 14:51:35 -06:00
|
|
|
def __init__(self, parser):
|
|
|
|
super(HintLog, self).__init__()
|
|
|
|
self.data = [self.header]
|
|
|
|
self.parser = parser
|
|
|
|
|
|
|
|
def refresh_hints(self, hints):
|
2024-11-28 20:10:31 -05:00
|
|
|
if not hints: # Fix the scrolling looking visually wrong in some edge cases
|
|
|
|
self.scroll_y = 1.0
|
2024-03-12 14:52:57 +01:00
|
|
|
data = []
|
2025-04-05 11:46:24 -05:00
|
|
|
ctx = MDApp.get_running_app().ctx
|
2023-11-07 14:51:35 -06:00
|
|
|
for hint in hints:
|
2024-11-28 20:10:31 -05:00
|
|
|
if not hint.get("status"): # Allows connecting to old servers
|
|
|
|
hint["status"] = HintStatus.HINT_FOUND if hint["found"] else HintStatus.HINT_UNSPECIFIED
|
|
|
|
hint_status_node = self.parser.handle_node({"type": "color",
|
|
|
|
"color": status_colors.get(hint["status"], "red"),
|
|
|
|
"text": status_names.get(hint["status"], "Unknown")})
|
2024-12-10 20:35:36 +01:00
|
|
|
if hint["status"] != HintStatus.HINT_FOUND and ctx.slot_concerns_self(hint["receiving_player"]):
|
2024-11-28 20:10:31 -05:00
|
|
|
hint_status_node = f"[u]{hint_status_node}[/u]"
|
2024-03-12 14:52:57 +01:00
|
|
|
data.append({
|
2023-11-07 14:51:35 -06:00
|
|
|
"receiving": {"text": self.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})},
|
2024-06-01 06:07:13 -05:00
|
|
|
"item": {"text": self.parser.handle_node({
|
|
|
|
"type": "item_id",
|
|
|
|
"text": hint["item"],
|
|
|
|
"flags": hint["item_flags"],
|
|
|
|
"player": hint["receiving_player"],
|
|
|
|
})},
|
2023-11-07 14:51:35 -06:00
|
|
|
"finding": {"text": self.parser.handle_node({"type": "player_id", "text": hint["finding_player"]})},
|
2024-06-01 06:07:13 -05:00
|
|
|
"location": {"text": self.parser.handle_node({
|
|
|
|
"type": "location_id",
|
|
|
|
"text": hint["location"],
|
|
|
|
"player": hint["finding_player"],
|
|
|
|
})},
|
2023-11-07 14:51:35 -06:00
|
|
|
"entrance": {"text": self.parser.handle_node({"type": "color" if hint["entrance"] else "text",
|
|
|
|
"color": "blue", "text": hint["entrance"]
|
|
|
|
if hint["entrance"] else "Vanilla"})},
|
2024-11-28 20:10:31 -05:00
|
|
|
"status": {
|
|
|
|
"text": hint_status_node,
|
|
|
|
"hint": hint,
|
|
|
|
},
|
2023-11-07 14:51:35 -06:00
|
|
|
})
|
2024-03-12 14:52:57 +01:00
|
|
|
|
|
|
|
data.sort(key=self.hint_sorter, reverse=self.reversed)
|
|
|
|
for i in range(0, len(data), 2):
|
|
|
|
data[i]["striped"] = True
|
|
|
|
data.insert(0, self.header)
|
|
|
|
self.data = data
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def hint_sorter(element: dict) -> str:
|
2024-11-28 20:10:31 -05:00
|
|
|
return element["status"]["hint"]["status"] # By status by default
|
2023-11-07 14:51:35 -06:00
|
|
|
|
2024-03-22 15:29:24 -05:00
|
|
|
def fix_heights(self):
|
|
|
|
"""Workaround fix for divergent texture and layout heights"""
|
|
|
|
for element in self.children[0].children:
|
|
|
|
max_height = max(child.texture_size[1] for child in element.children)
|
|
|
|
element.height = max_height
|
|
|
|
|
2023-11-07 14:51:35 -06:00
|
|
|
|
2024-11-29 21:58:52 -05:00
|
|
|
class ApAsyncImage(AsyncImage):
|
2025-04-15 17:09:27 -05:00
|
|
|
|
2024-11-29 21:58:52 -05:00
|
|
|
def is_uri(self, filename: str) -> bool:
|
|
|
|
if filename.startswith("ap:"):
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
return super().is_uri(filename)
|
|
|
|
|
|
|
|
|
|
|
|
class ImageLoaderPkgutil(ImageLoaderBase):
|
|
|
|
def load(self, filename: str) -> typing.List[ImageData]:
|
|
|
|
# take off the "ap:" prefix
|
|
|
|
module, path = filename[3:].split("/", 1)
|
|
|
|
data = pkgutil.get_data(module, path)
|
|
|
|
return self._bytes_to_data(data)
|
|
|
|
|
2025-04-05 11:46:24 -05:00
|
|
|
@staticmethod
|
|
|
|
def _bytes_to_data(data: typing.Union[bytes, bytearray]) -> typing.List[ImageData]:
|
2024-11-29 21:58:52 -05:00
|
|
|
loader = next(loader for loader in ImageLoader.loaders if loader.can_load_memory())
|
|
|
|
return loader.load(loader, io.BytesIO(data))
|
|
|
|
|
|
|
|
|
|
|
|
# grab the default loader method so we can override it but use it as a fallback
|
|
|
|
_original_image_loader_load = ImageLoader.load
|
|
|
|
|
|
|
|
|
|
|
|
def load_override(filename: str, default_load=_original_image_loader_load, **kwargs):
|
|
|
|
if filename.startswith("ap:"):
|
|
|
|
return ImageLoaderPkgutil(filename)
|
|
|
|
else:
|
|
|
|
return default_load(filename, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
ImageLoader.load = load_override
|
|
|
|
|
|
|
|
|
2021-07-30 20:18:03 +02:00
|
|
|
class E(ExceptionHandler):
|
|
|
|
logger = logging.getLogger("Client")
|
|
|
|
|
|
|
|
def handle_exception(self, inst):
|
2021-11-01 06:40:29 +01:00
|
|
|
self.logger.exception("Uncaught Exception:", exc_info=inst)
|
|
|
|
return ExceptionManager.PASS
|
2021-07-30 20:18:03 +02:00
|
|
|
|
|
|
|
|
|
|
|
class KivyJSONtoTextParser(JSONtoTextParser):
|
2023-02-13 01:55:43 +01:00
|
|
|
# dummy class to absorb kvlang definitions
|
|
|
|
class TextColors(Widget):
|
2025-04-15 17:09:27 -05:00
|
|
|
white: str = StringProperty("FFFFFF")
|
|
|
|
black: str = StringProperty("000000")
|
|
|
|
red: str = StringProperty("EE0000")
|
|
|
|
green: str = StringProperty("00FF7F")
|
|
|
|
yellow: str = StringProperty("FAFAD2")
|
|
|
|
blue: str = StringProperty("6495ED")
|
|
|
|
magenta: str = StringProperty("EE00EE")
|
|
|
|
cyan: str = StringProperty("00EEEE")
|
|
|
|
slateblue: str = StringProperty("6D8BE8")
|
|
|
|
plum: str = StringProperty("AF99EF")
|
|
|
|
salmon: str = StringProperty("FA8072")
|
|
|
|
orange: str = StringProperty("FF7700")
|
|
|
|
# KivyMD parameters
|
|
|
|
theme_style: str = StringProperty("Dark")
|
|
|
|
primary_palette: str = StringProperty("Lightsteelblue")
|
|
|
|
dynamic_scheme_name: str = StringProperty("VIBRANT")
|
|
|
|
dynamic_scheme_contrast: int = NumericProperty(0)
|
2023-02-13 01:55:43 +01:00
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
# we grab the color definitions from the .kv file, then overwrite the JSONtoTextParser default entries
|
|
|
|
colors = self.TextColors()
|
|
|
|
color_codes = self.color_codes.copy()
|
|
|
|
for name, code in color_codes.items():
|
|
|
|
color_codes[name] = getattr(colors, name, code)
|
|
|
|
self.color_codes = color_codes
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
2022-05-24 00:20:02 +02:00
|
|
|
def __call__(self, *args, **kwargs):
|
|
|
|
self.ref_count = 0
|
|
|
|
return super(KivyJSONtoTextParser, self).__call__(*args, **kwargs)
|
|
|
|
|
|
|
|
def _handle_item_name(self, node: JSONMessagePart):
|
|
|
|
flags = node.get("flags", 0)
|
2024-04-14 20:36:55 +02:00
|
|
|
item_types = []
|
2022-05-24 00:20:02 +02:00
|
|
|
if flags & 0b001: # advancement
|
2024-04-14 20:36:55 +02:00
|
|
|
item_types.append("progression")
|
|
|
|
if flags & 0b010: # useful
|
|
|
|
item_types.append("useful")
|
|
|
|
if flags & 0b100: # trap
|
|
|
|
item_types.append("trap")
|
|
|
|
if not item_types:
|
|
|
|
item_types.append("normal")
|
|
|
|
|
|
|
|
node.setdefault("refs", []).append("Item Class: " + ", ".join(item_types))
|
2022-05-24 00:20:02 +02:00
|
|
|
return super(KivyJSONtoTextParser, self)._handle_item_name(node)
|
|
|
|
|
|
|
|
def _handle_player_id(self, node: JSONMessagePart):
|
|
|
|
player = int(node["text"])
|
|
|
|
slot_info = self.ctx.slot_info.get(player, None)
|
|
|
|
if slot_info:
|
|
|
|
text = f"Game: {slot_info.game}<br>" \
|
|
|
|
f"Type: {SlotType(slot_info.type).name}"
|
|
|
|
if slot_info.group_members:
|
2024-03-12 13:13:52 -07:00
|
|
|
text += f"<br>Members:<br> " + "<br> ".join(
|
|
|
|
escape_markup(self.ctx.player_names[player])
|
|
|
|
for player in slot_info.group_members
|
|
|
|
)
|
2022-05-24 00:20:02 +02:00
|
|
|
node.setdefault("refs", []).append(text)
|
|
|
|
return super(KivyJSONtoTextParser, self)._handle_player_id(node)
|
2021-07-30 20:18:03 +02:00
|
|
|
|
|
|
|
def _handle_color(self, node: JSONMessagePart):
|
|
|
|
colors = node["color"].split(";")
|
|
|
|
node["text"] = escape_markup(node["text"])
|
|
|
|
for color in colors:
|
|
|
|
color_code = self.color_codes.get(color, None)
|
|
|
|
if color_code:
|
|
|
|
node["text"] = f"[color={color_code}]{node['text']}[/color]"
|
|
|
|
return self._handle_text(node)
|
|
|
|
return self._handle_text(node)
|
|
|
|
|
2022-05-24 00:20:02 +02:00
|
|
|
def _handle_text(self, node: JSONMessagePart):
|
2024-07-19 01:37:59 -05:00
|
|
|
# All other text goes through _handle_color, and we don't want to escape markup twice,
|
|
|
|
# or mess up text that already has intentional markup applied to it
|
|
|
|
if node.get("type", "text") == "text":
|
|
|
|
node["text"] = escape_markup(node["text"])
|
2022-05-24 00:20:02 +02:00
|
|
|
for ref in node.get("refs", []):
|
|
|
|
node["text"] = f"[ref={self.ref_count}|{ref}]{node['text']}[/ref]"
|
|
|
|
self.ref_count += 1
|
|
|
|
return super(KivyJSONtoTextParser, self)._handle_text(node)
|
|
|
|
|
2021-07-30 20:18:03 +02:00
|
|
|
|
|
|
|
ExceptionManager.add_handler(E())
|
|
|
|
|
|
|
|
Builder.load_file(Utils.local_path("data", "client.kv"))
|
2023-03-29 20:14:45 +02:00
|
|
|
user_file = Utils.user_path("data", "user.kv")
|
2023-02-13 01:55:43 +01:00
|
|
|
if os.path.exists(user_file):
|
|
|
|
logging.info("Loading user.kv into builder.")
|
|
|
|
Builder.load_file(user_file)
|