CommonClient.py UI: Add info on "Server:" label hover

CommonClient.py UI: prevent freeze if UI is closed while waiting on text user input
This commit is contained in:
Fabian Dill 2021-10-29 10:03:15 +02:00
parent 2217a9304d
commit 7f020857d1
3 changed files with 140 additions and 8 deletions

View File

@ -1,6 +1,5 @@
from __future__ import annotations
import logging
import typing
import asyncio
import urllib.parse
import sys
@ -92,7 +91,7 @@ class ClientCommandProcessor(CommandProcessor):
class CommonContext():
tags:typing.Set[str] = {"AP"}
tags: typing.Set[str] = {"AP"}
starting_reconnect_delay: int = 5
current_reconnect_delay: int = starting_reconnect_delay
command_processor: int = ClientCommandProcessor
@ -107,6 +106,7 @@ class CommonContext():
self.server_task = None
self.server: typing.Optional[Endpoint] = None
self.server_version = Version(0, 0, 0)
self.hint_cost: typing.Optional[int] = None
self.permissions = {
"forfeit": "disabled",
"collect": "disabled",
@ -121,11 +121,11 @@ class CommonContext():
self.auth = None
self.seed_name = None
self.locations_checked: typing.Set[int] = set()
self.locations_checked: typing.Set[int] = set() # local state
self.locations_scouted: typing.Set[int] = set()
self.items_received = []
self.missing_locations: typing.Set[int] = set()
self.checked_locations: typing.Set[int] = set()
self.checked_locations: typing.Set[int] = set() # server state
self.locations_info = {}
self.input_queue = asyncio.Queue()
@ -143,6 +143,12 @@ class CommonContext():
# execution
self.keep_alive_task = asyncio.create_task(keep_alive(self))
@property
def total_locations(self) -> typing.Optional[int]:
"""Will return None until connected."""
if self.checked_locations or self.missing_locations:
return len(self.checked_locations | self.missing_locations)
async def connection_closed(self):
self.auth = None
self.items_received = []

View File

@ -23,3 +23,20 @@
height: self.minimum_height
orientation: 'vertical'
spacing: dp(3)
<ServerLabel>:
text: "Server:"
size_hint_x: None
<ContainerLayout>:
size_hint_x: 1
size_hint_y: 1
pos: (0, 0)
<ServerToolTip>:
size: self.texture_size
pos_hint: {'center_y': 0.5, 'center_x': 0.5}
halign: "left"
canvas.before:
Color:
rgba: 0.2, 0.2, 0.2, 1
Rectangle:
size: self.size
pos: self.pos

115
kvui.py
View File

@ -6,14 +6,19 @@ import asyncio
os.environ["KIVY_NO_CONSOLELOG"] = "1"
os.environ["KIVY_NO_FILELOG"] = "1"
os.environ["KIVY_NO_ARGS"] = "1"
from kivy.app import App
from kivy.core.window import Window
from kivy.base import ExceptionHandler, ExceptionManager, Config, Clock
from kivy.factory import Factory
from kivy.properties import BooleanProperty, ObjectProperty
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.textinput import TextInput
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.utils import escape_markup
@ -30,6 +35,102 @@ else:
context_type = object
# I was surprised to find this didn't already exist in kivy :(
class HoverBehavior(object):
"""from https://stackoverflow.com/a/605348110"""
hovered = BooleanProperty(False)
border_point = ObjectProperty(None)
def __init__(self, **kwargs):
self.register_event_type('on_enter')
self.register_event_type('on_leave')
Window.bind(mouse_pos=self.on_mouse_pos)
super(HoverBehavior, self).__init__(**kwargs)
def on_mouse_pos(self, *args):
if not self.get_root_window():
return # do proceed if I'm not displayed <=> If have no parent
pos = args[1]
# Next line to_widget allow to compensate for relative layout
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")
Factory.register('HoverBehavior', HoverBehavior)
class ServerToolTip(Label):
pass
class ServerLabel(HoverBehavior, Label):
hover_text = """"""
def __init__(self, *args, **kwargs):
super(ServerLabel, self).__init__(*args, **kwargs)
self.layout = FloatLayout()
self.popuplabel = ServerToolTip(text="Test")
self.layout.add_widget(self.popuplabel)
def on_enter(self):
self.popuplabel.text = self.get_text()
App.get_running_app().root.add_widget(self.layout)
def on_leave(self):
App.get_running_app().root.remove_widget(self.layout)
def get_text(self):
if self.ctx.server:
ctx = self.ctx
text = f"Connected to: {ctx.server_address}."
if ctx.slot is not None:
text += f"\nYou are Slot Number {ctx.slot} in Team Number {ctx.team}, named {ctx.player_names[ctx.slot]}."
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}"
if ctx.hint_cost is not None:
text += f"\nA new !hint <itemname> costs {ctx.hint_cost}% of checks made. " \
f"For you this means every {max(0, int(ctx.hint_cost * 0.01 * ctx.total_locations))} location checks."
elif ctx.hint_cost == 0:
text += "\n!hint is free to use."
else:
text += f"\nYou are not authenticated yet."
return text
else:
return "No current server connection. \nPlease connect to an Archipelago server."
@property
def ctx(self) -> context_type:
return App.get_running_app().ctx
class MainLayout(GridLayout):
pass
class ContainerLayout(FloatLayout):
pass
class GameManager(App):
logging_pairs = [
("Client", "Archipelago"),
@ -46,11 +147,13 @@ class GameManager(App):
super(GameManager, self).__init__()
def build(self):
self.grid = GridLayout()
self.container = ContainerLayout()
self.grid = MainLayout()
self.grid.cols = 1
connect_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=30)
# top part
server_label = Label(text="Server:", size_hint_x=None)
server_label = ServerLabel()
connect_layout.add_widget(server_label)
self.server_connect_bar = TextInput(text="archipelago.gg", size_hint_y=None, height=30, multiline=False)
connect_layout.add_widget(self.server_connect_bar)
@ -94,7 +197,8 @@ class GameManager(App):
self.grid.add_widget(bottom_layout)
self.commandprocessor("/help")
Clock.schedule_interval(self.update_texts, 1 / 30)
return self.grid
self.container.add_widget(self.grid)
return self.container
def update_texts(self, dt):
if self.ctx.server:
@ -118,6 +222,11 @@ class GameManager(App):
asyncio.create_task(self.ctx.connect(self.server_connect_bar.text.replace("/connect ", "")))
def on_stop(self):
# "kill" input tasks
for x in range(self.ctx.input_requests):
self.ctx.input_queue.put_nowait("")
self.ctx.input_requests = 0
self.ctx.exit_event.set()
def on_message(self, textinput: TextInput):