Kivy: Add a button prompt box (#3470)

* Kivy: Add a button prompt box

* auto format the buttons to display 2 per row to look nicer

* update to kivymd

* have the uri popup use the new API

* have messenger use the new API

* make the buttonprompt import even more lazy

* messenger needs to be lazy too

* make the buttons take up the full dialog width

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
This commit is contained in:
Aaron Wagener
2025-05-18 18:08:39 -05:00
committed by GitHub
parent 90ee9ffe36
commit d3dbdb4491
4 changed files with 160 additions and 93 deletions

View File

@@ -121,46 +121,28 @@ def handle_uri(path: str, launch_args: tuple[str, ...]) -> None:
launch_args = (path, *launch_args)
client_component = []
text_client_component = None
if "game" in queries:
game = queries["game"][0]
else: # TODO around 0.6.0 - this is for pre this change webhost uri's
game = "Archipelago"
game = queries["game"][0]
for component in components:
if component.supports_uri and component.game_name == game:
client_component.append(component)
elif component.display_name == "Text Client":
text_client_component = component
from kvui import MDButton, MDButtonText
from kivymd.uix.dialog import MDDialog, MDDialogHeadlineText, MDDialogContentContainer, MDDialogSupportingText
from kivymd.uix.divider import MDDivider
if not client_component:
run_component(text_client_component, *launch_args)
return
else:
popup_text = MDDialogSupportingText(text="Select client to open and connect with.")
component_buttons = [MDDivider()]
for component in [text_client_component, *client_component]:
component_buttons.append(MDButton(
MDButtonText(text=component.display_name),
on_release=lambda *args, comp=component: run_component(comp, *launch_args),
style="text"
))
component_buttons.append(MDDivider())
MDDialog(
# Headline
MDDialogHeadlineText(text="Connect to Multiworld"),
# Text
popup_text,
# Content
MDDialogContentContainer(
*component_buttons,
orientation="vertical"
),
).open()
from kvui import ButtonsPrompt
component_options = {
text_client_component.display_name: text_client_component,
**{component.display_name: component for component in client_component}
}
popup = ButtonsPrompt("Connect to Multiworld",
"Select client to open and connect with.",
lambda component_name: run_component(component_options[component_name], *launch_args),
*component_options.keys())
popup.open()
def identify(path: None | str) -> tuple[None | str, None | Component]:

View File

@@ -222,3 +222,8 @@
spacing: 10
size_hint_y: None
height: self.minimum_height
<MessageBoxLabel>:
valign: "middle"
halign: "center"
text_size: self.width, None
height: self.texture_size[1]

54
kvui.py
View File

@@ -6,7 +6,6 @@ import re
import io
import pkgutil
from collections import deque
assert "kivy" not in sys.modules, "kvui should be imported before kivy for frozen compatibility"
if sys.platform == "win32":
@@ -57,6 +56,7 @@ from kivy.animation import Animation
from kivy.uix.popup import Popup
from kivy.uix.image import AsyncImage
from kivymd.app import MDApp
from kivymd.uix.dialog import MDDialog, MDDialogHeadlineText, MDDialogSupportingText, MDDialogButtonContainer
from kivymd.uix.gridlayout import MDGridLayout
from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.boxlayout import MDBoxLayout
@@ -710,20 +710,62 @@ class CommandPromptTextInput(ResizableTextField):
self.text = self._command_history[self._command_history_index]
class MessageBoxLabel(MDLabel):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._label.refresh()
class MessageBox(Popup):
class MessageBoxLabel(MDLabel):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._label.refresh()
def __init__(self, title, text, error=False, **kwargs):
label = MessageBox.MessageBoxLabel(text=text)
label = MessageBoxLabel(text=text)
separator_color = [217 / 255, 129 / 255, 122 / 255, 1.] if error else [47 / 255., 167 / 255., 212 / 255, 1.]
super().__init__(title=title, content=label, size_hint=(0.5, None), width=max(100, int(label.width) + 40),
separator_color=separator_color, **kwargs)
self.height += max(0, label.height - 18)
class ButtonsPrompt(MDDialog):
def __init__(self, title: str, text: str, response: typing.Callable[[str], None],
*prompts: str, **kwargs) -> None:
"""
Customizable popup box that lets you create any number of buttons. The text of the pressed button is returned to
the callback.
:param title: The title of the popup.
:param text: The message prompt in the popup.
:param response: A callable that will get called when the user presses a button. The prompt will not close
itself so should be done here if you want to close it when certain buttons are pressed.
:param prompts: Any number of strings to be used for the buttons.
"""
layout = MDBoxLayout(orientation="vertical")
label = MessageBoxLabel(text=text)
layout.add_widget(label)
def on_release(button: MDButton, *args) -> None:
response(button.text)
buttons = [MDDivider()]
for prompt in prompts:
button = MDButton(
MDButtonText(text=prompt, pos_hint={"center_x": 0.5, "center_y": 0.5}),
on_release=on_release,
style="text",
theme_width="Custom",
size_hint_x=1,
)
button.text = prompt
buttons.extend([button, MDDivider()])
super().__init__(
MDDialogHeadlineText(text=title),
MDDialogSupportingText(text=text),
MDDialogButtonContainer(*buttons, orientation="vertical"),
**kwargs,
)
class ClientTabs(MDTabsSecondary):
carousel: MDTabsCarousel
lock_swiping = True

View File

@@ -2,35 +2,28 @@ import argparse
import io
import logging
import os.path
import requests
import subprocess
import urllib.request
from shutil import which
from typing import Any
from typing import Any, Callable, TYPE_CHECKING
from zipfile import ZipFile
from Utils import open_file
from Utils import is_windows, messagebox, open_file, tuplize_version
import requests
from Utils import is_windows, messagebox, tuplize_version
if TYPE_CHECKING:
from kvui import ButtonsPrompt
MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest"
def ask_yes_no_cancel(title: str, text: str) -> bool | None:
"""
Wrapper for tkinter.messagebox.askyesnocancel, that creates a popup dialog box with yes, no, and cancel buttons.
def create_yes_no_popup(title: str, text: str, callback: Callable[[str], None]) -> "ButtonsPrompt":
from kvui import ButtonsPrompt
buttons = ["Yes", "No", "Cancel"]
:param title: Title to be displayed at the top of the message box.
:param text: Text to be displayed inside the message box.
:return: Returns True if yes, False if no, None if cancel.
"""
from tkinter import Tk, messagebox
root = Tk()
root.withdraw()
ret = messagebox.askyesnocancel(title, text)
root.update()
return ret
prompt = ButtonsPrompt(title, text, callback, *buttons)
prompt.open()
return prompt
def launch_game(*args) -> None:
@@ -151,6 +144,76 @@ def launch_game(*args) -> None:
# one of the alpha builds
return "alpha" in latest_version or tuplize_version(latest_version) > tuplize_version(installed_version)
def after_courier_install_popup(answer: str) -> None:
"""Gets called if the user doesn't have courier installed. Handle the button they pressed."""
nonlocal prompt
prompt.dismiss()
if answer in ("No", "Cancel"):
return
logging.info("Installing Courier")
install_courier()
prompt = create_yes_no_popup("Install Mod",
"No randomizer mod detected. Would you like to install now?",
after_mod_install_popup)
def after_mod_install_popup(answer: str) -> None:
"""Gets called if the user has courier but mod isn't installed, or there's an available update."""
nonlocal prompt
prompt.dismiss()
if answer in ("No", "Cancel"):
return
logging.info("Installing Mod")
install_mod()
prompt = create_yes_no_popup("Launch Game",
"Courier and Game mod installed successfully. Launch game now?",
launch)
def after_mod_update_popup(answer: str) -> None:
"""Gets called if there's an available update."""
nonlocal prompt
prompt.dismiss()
if answer == "Cancel":
return
if answer == "Yes":
logging.info("Updating Mod")
install_mod()
prompt = create_yes_no_popup("Launch Game",
"Courier and Game mod installed successfully. Launch game now?",
launch)
else:
prompt = create_yes_no_popup("Launch Game",
"Game Mod not updated. Launch game now?",
launch)
def launch(answer: str | None = None) -> None:
"""Launch the game."""
nonlocal args
if prompt:
prompt.dismiss()
if answer and answer in ("No", "Cancel"):
return
parser = argparse.ArgumentParser(description="Messenger Client Launcher")
parser.add_argument("url", type=str, nargs="?", help="Archipelago Webhost uri to auto connect to.")
args = parser.parse_args(args)
if not is_windows:
if args.url:
open_file(f"steam://rungameid/764790//{args.url}/")
else:
open_file("steam://rungameid/764790")
else:
os.chdir(game_folder)
if args.url:
subprocess.Popen([MessengerWorld.settings.game_path, str(args.url)])
else:
subprocess.Popen(MessengerWorld.settings.game_path)
os.chdir(working_directory)
from . import MessengerWorld
try:
game_folder = os.path.dirname(MessengerWorld.settings.game_path)
@@ -172,49 +235,24 @@ def launch_game(*args) -> None:
except ImportError:
pass
if not courier_installed():
should_install = ask_yes_no_cancel("Install Courier",
"No Courier installation detected. Would you like to install now?")
if not should_install:
return
logging.info("Installing Courier")
install_courier()
prompt = create_yes_no_popup("Install Courier",
"No Courier installation detected. Would you like to install now?",
after_courier_install_popup)
return
if not mod_installed():
should_install = ask_yes_no_cancel("Install Mod",
"No randomizer mod detected. Would you like to install now?")
if not should_install:
return
logging.info("Installing Mod")
install_mod()
prompt = create_yes_no_popup("Install Mod",
"No randomizer mod detected. Would you like to install now?",
after_mod_install_popup)
return
else:
latest = request_data(MOD_URL)["tag_name"]
if available_mod_update(latest):
should_update = ask_yes_no_cancel("Update Mod",
f"New mod version detected. Would you like to update to {latest} now?")
if should_update:
logging.info("Updating mod")
install_mod()
elif should_update is None:
return
if not args:
should_launch = ask_yes_no_cancel("Launch Game",
"Mod installed and up to date. Would you like to launch the game now?")
if not should_launch:
prompt = create_yes_no_popup("Update Mod",
f"New mod version detected. Would you like to update to {latest} now?",
after_mod_update_popup)
return
parser = argparse.ArgumentParser(description="Messenger Client Launcher")
parser.add_argument("url", type=str, nargs="?", help="Archipelago Webhost uri to auto connect to.")
args = parser.parse_args(args)
if not is_windows:
if args.url:
open_file(f"steam://rungameid/764790//{args.url}/")
else:
open_file("steam://rungameid/764790")
else:
os.chdir(game_folder)
if args.url:
subprocess.Popen([MessengerWorld.settings.game_path, str(args.url)])
else:
subprocess.Popen(MessengerWorld.settings.game_path)
os.chdir(working_directory)
if not args:
prompt = create_yes_no_popup("Launch Game",
"Mod installed and up to date. Would you like to launch the game now?",
launch)