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

View File

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

50
kvui.py
View File

@@ -6,7 +6,6 @@ import re
import io import io
import pkgutil import pkgutil
from collections import deque from collections import deque
assert "kivy" not in sys.modules, "kvui should be imported before kivy for frozen compatibility" assert "kivy" not in sys.modules, "kvui should be imported before kivy for frozen compatibility"
if sys.platform == "win32": if sys.platform == "win32":
@@ -57,6 +56,7 @@ from kivy.animation import Animation
from kivy.uix.popup import Popup from kivy.uix.popup import Popup
from kivy.uix.image import AsyncImage from kivy.uix.image import AsyncImage
from kivymd.app import MDApp from kivymd.app import MDApp
from kivymd.uix.dialog import MDDialog, MDDialogHeadlineText, MDDialogSupportingText, MDDialogButtonContainer
from kivymd.uix.gridlayout import MDGridLayout from kivymd.uix.gridlayout import MDGridLayout
from kivymd.uix.floatlayout import MDFloatLayout from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.boxlayout import MDBoxLayout
@@ -710,20 +710,62 @@ class CommandPromptTextInput(ResizableTextField):
self.text = self._command_history[self._command_history_index] self.text = self._command_history[self._command_history_index]
class MessageBox(Popup): class MessageBoxLabel(MDLabel):
class MessageBoxLabel(MDLabel):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self._label.refresh() self._label.refresh()
class MessageBox(Popup):
def __init__(self, title, text, error=False, **kwargs): 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.] 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), super().__init__(title=title, content=label, size_hint=(0.5, None), width=max(100, int(label.width) + 40),
separator_color=separator_color, **kwargs) separator_color=separator_color, **kwargs)
self.height += max(0, label.height - 18) 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): class ClientTabs(MDTabsSecondary):
carousel: MDTabsCarousel carousel: MDTabsCarousel
lock_swiping = True lock_swiping = True

View File

@@ -2,35 +2,28 @@ import argparse
import io import io
import logging import logging
import os.path import os.path
import requests
import subprocess import subprocess
import urllib.request import urllib.request
from shutil import which from shutil import which
from typing import Any from typing import Any, Callable, TYPE_CHECKING
from zipfile import ZipFile from zipfile import ZipFile
from Utils import open_file from Utils import is_windows, messagebox, open_file, tuplize_version
import requests if TYPE_CHECKING:
from kvui import ButtonsPrompt
from Utils import is_windows, messagebox, tuplize_version
MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest" MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest"
def ask_yes_no_cancel(title: str, text: str) -> bool | None: def create_yes_no_popup(title: str, text: str, callback: Callable[[str], None]) -> "ButtonsPrompt":
""" from kvui import ButtonsPrompt
Wrapper for tkinter.messagebox.askyesnocancel, that creates a popup dialog box with yes, no, and cancel buttons. buttons = ["Yes", "No", "Cancel"]
:param title: Title to be displayed at the top of the message box. prompt = ButtonsPrompt(title, text, callback, *buttons)
:param text: Text to be displayed inside the message box. prompt.open()
:return: Returns True if yes, False if no, None if cancel. return prompt
"""
from tkinter import Tk, messagebox
root = Tk()
root.withdraw()
ret = messagebox.askyesnocancel(title, text)
root.update()
return ret
def launch_game(*args) -> None: def launch_game(*args) -> None:
@@ -151,6 +144,76 @@ def launch_game(*args) -> None:
# one of the alpha builds # one of the alpha builds
return "alpha" in latest_version or tuplize_version(latest_version) > tuplize_version(installed_version) 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 from . import MessengerWorld
try: try:
game_folder = os.path.dirname(MessengerWorld.settings.game_path) game_folder = os.path.dirname(MessengerWorld.settings.game_path)
@@ -172,49 +235,24 @@ def launch_game(*args) -> None:
except ImportError: except ImportError:
pass pass
if not courier_installed(): if not courier_installed():
should_install = ask_yes_no_cancel("Install Courier", prompt = create_yes_no_popup("Install Courier",
"No Courier installation detected. Would you like to install now?") "No Courier installation detected. Would you like to install now?",
if not should_install: after_courier_install_popup)
return return
logging.info("Installing Courier")
install_courier()
if not mod_installed(): if not mod_installed():
should_install = ask_yes_no_cancel("Install Mod", prompt = create_yes_no_popup("Install Mod",
"No randomizer mod detected. Would you like to install now?") "No randomizer mod detected. Would you like to install now?",
if not should_install: after_mod_install_popup)
return return
logging.info("Installing Mod")
install_mod()
else: else:
latest = request_data(MOD_URL)["tag_name"] latest = request_data(MOD_URL)["tag_name"]
if available_mod_update(latest): if available_mod_update(latest):
should_update = ask_yes_no_cancel("Update Mod", prompt = create_yes_no_popup("Update Mod",
f"New mod version detected. Would you like to update to {latest} now?") f"New mod version detected. Would you like to update to {latest} now?",
if should_update: after_mod_update_popup)
logging.info("Updating mod")
install_mod()
elif should_update is None:
return return
if not args: if not args:
should_launch = ask_yes_no_cancel("Launch Game", prompt = create_yes_no_popup("Launch Game",
"Mod installed and up to date. Would you like to launch the game now?") "Mod installed and up to date. Would you like to launch the game now?",
if not should_launch: launch)
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)