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

View File

@ -22,4 +22,21 @@
size_hint_y: None size_hint_y: None
height: self.minimum_height height: self.minimum_height
orientation: 'vertical' orientation: 'vertical'
spacing: dp(3) 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_CONSOLELOG"] = "1"
os.environ["KIVY_NO_FILELOG"] = "1" os.environ["KIVY_NO_FILELOG"] = "1"
os.environ["KIVY_NO_ARGS"] = "1" os.environ["KIVY_NO_ARGS"] = "1"
from kivy.app import App from kivy.app import App
from kivy.core.window import Window
from kivy.base import ExceptionHandler, ExceptionManager, Config, Clock 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.button import Button
from kivy.uix.gridlayout import GridLayout from kivy.uix.gridlayout import GridLayout
from kivy.uix.textinput import TextInput from kivy.uix.textinput import TextInput
from kivy.uix.recycleview import RecycleView from kivy.uix.recycleview import RecycleView
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
from kivy.uix.boxlayout import BoxLayout from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label from kivy.uix.label import Label
from kivy.uix.progressbar import ProgressBar from kivy.uix.progressbar import ProgressBar
from kivy.utils import escape_markup from kivy.utils import escape_markup
@ -30,6 +35,102 @@ else:
context_type = object 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): class GameManager(App):
logging_pairs = [ logging_pairs = [
("Client", "Archipelago"), ("Client", "Archipelago"),
@ -46,11 +147,13 @@ class GameManager(App):
super(GameManager, self).__init__() super(GameManager, self).__init__()
def build(self): def build(self):
self.grid = GridLayout() self.container = ContainerLayout()
self.grid = MainLayout()
self.grid.cols = 1 self.grid.cols = 1
connect_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=30) connect_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=30)
# top part # top part
server_label = Label(text="Server:", size_hint_x=None) server_label = ServerLabel()
connect_layout.add_widget(server_label) connect_layout.add_widget(server_label)
self.server_connect_bar = TextInput(text="archipelago.gg", size_hint_y=None, height=30, multiline=False) self.server_connect_bar = TextInput(text="archipelago.gg", size_hint_y=None, height=30, multiline=False)
connect_layout.add_widget(self.server_connect_bar) connect_layout.add_widget(self.server_connect_bar)
@ -94,7 +197,8 @@ class GameManager(App):
self.grid.add_widget(bottom_layout) self.grid.add_widget(bottom_layout)
self.commandprocessor("/help") self.commandprocessor("/help")
Clock.schedule_interval(self.update_texts, 1 / 30) 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): def update_texts(self, dt):
if self.ctx.server: if self.ctx.server:
@ -118,6 +222,11 @@ class GameManager(App):
asyncio.create_task(self.ctx.connect(self.server_connect_bar.text.replace("/connect ", ""))) asyncio.create_task(self.ctx.connect(self.server_connect_bar.text.replace("/connect ", "")))
def on_stop(self): 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() self.ctx.exit_event.set()
def on_message(self, textinput: TextInput): def on_message(self, textinput: TextInput):