# This file is part of Elide, frontend to Lisien, a framework for life simulation games.
# Copyright (c) Zachary Spector, public@zacharyspector.com
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""The big layout that you view all of elide through.
Handles touch, selection, and time control. Contains a graph, a stat
grid, the time control panel, and the menu.
"""
from ast import literal_eval
from functools import partial
from threading import Thread
from kivy.app import App
from kivy.clock import Clock, mainthread, triggered
from kivy.core.text import DEFAULT_FONT
from kivy.factory import Factory
from kivy.logger import Logger
from kivy.properties import (
BooleanProperty,
BoundedNumericProperty,
DictProperty,
ListProperty,
NumericProperty,
ObjectProperty,
ReferenceListProperty,
StringProperty,
VariableListProperty,
)
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.screenmanager import Screen
from kivy.uix.scrollview import ScrollView
from kivy.uix.slider import Slider
from kivy.uix.togglebutton import ToggleButton
from kivy.uix.widget import Widget
from .calendar import Agenda
from .charmenu import CharMenu
from .graph.board import GraphBoardView
from .grid.board import GridBoardView
from .stepper import RuleStepper
from .util import devour, dummynum, load_kv, logwrap
def trigger(func):
return triggered()(func)
Factory.register("CharMenu", cls=CharMenu)
def release_edit_lock(*_):
app = App.get_running_app()
app.edit_locked = app.simulate_button_down
[docs]
class KvLayout(FloatLayout):
pass
[docs]
class StatListPanel(BoxLayout):
"""A panel that displays a simple two-column grid showing the stats of
the selected entity, defaulting to those of the character being
viewed.
Has a button on the bottom to open the StatWindow in which
to add and delete stats, or to change the way they are displayed
in the StatListPanel.
"""
selection_name = StringProperty()
button_text = StringProperty("Configure stats")
cfgstatbut = ObjectProperty()
statlist = ObjectProperty()
proxy = ObjectProperty()
toggle_stat_cfg = ObjectProperty()
@logwrap(section="StatListPanel")
def on_proxy(self, *_):
if hasattr(self.proxy, "name"):
self.selection_name = str(self.proxy.name)
@logwrap(section="StatListPanel")
def set_value(self, k, v):
if v is None:
del self.proxy[k]
else:
try:
vv = literal_eval(v)
except (TypeError, ValueError):
vv = v
self.proxy[k] = vv
[docs]
class TimePanel(BoxLayout):
"""A panel that starts and stop the game, or sets the time.
There's a "simulate" button, which is toggleable. When toggled on, the
simulation will continue to run until it's toggled off
again. Next to this is a "1 turn" button, which will simulate
exactly one turn and stop. And there are two text fields in which
you can manually enter a Branch and Tick to go to. Moving through
time this way doesn't simulate anything--you'll only see what
happened as a result of "simulate," "1 turn," or some other way
the lisien rules engine has been made to run.
"""
screen = ObjectProperty()
buttons_font_size = NumericProperty(18)
disable_one_turn = BooleanProperty()
@logwrap(section="TimePanel")
def set_branch(self, *_):
branch = self.ids.branchfield.text
self.ids.branchfield.text = ""
self.screen.app.branch = branch
self.screen.charmenu.switch_to_menu()
@logwrap(section="TimePanel")
def set_turn(self, *_):
turn = int(self.ids.turnfield.text)
self.ids.turnfield.text = ""
self.screen.app.set_turn(turn)
self.screen.charmenu.switch_to_menu()
@logwrap(section="TimePanel")
def set_tick(self, *_):
tick = int(self.ids.tickfield.text)
self.ids.tickfield.text = ""
self.screen.app.set_tick(tick)
self.screen.charmenu.switch_to_menu()
@mainthread
@logwrap(section="TimePanel")
def _upd_branch_hint(self, app, *_):
self.ids.branchfield.hint_text = app.branch
@mainthread
@logwrap(section="TimePanel")
def _upd_turn_hint(self, app, *_):
self.ids.turnfield.hint_text = str(app.turn)
@mainthread
@logwrap(section="TimePanel")
def _upd_tick_hint(self, app, *_):
self.ids.tickfield.hint_text = str(app.tick)
@logwrap(section="TimePanel")
def on_screen(self, *_):
if not all(
field in self.ids
for field in ("branchfield", "turnfield", "tickfield")
):
Clock.schedule_once(self.on_screen, 0)
return
app = App.get_running_app()
binds = app._bindings
self.ids.branchfield.hint_text = self.screen.app.branch
self.ids.turnfield.hint_text = str(self.screen.app.turn)
self.ids.tickfield.hint_text = str(self.screen.app.tick)
binds["ElideApp", "branch"].add(
app.fbind("branch", self._upd_branch_hint)
)
binds["ElideApp", "turn"].add(app.fbind("turn", self._upd_turn_hint))
binds["ElideApp", "tick"].add(app.fbind("tick", self._upd_tick_hint))
[docs]
class MainScreen(Screen):
"""A master layout that contains one graph and some menus.
This contains three elements: a scrollview (containing the graph),
a menu, and the time control panel. This class has some support methods
for handling interactions with the menu and the character sheet,
but if neither of those happen, the scrollview handles touches on its
own.
"""
name = "mainscreen"
graphboards = DictProperty()
gridboards = DictProperty()
boardview = ObjectProperty()
mainview = ObjectProperty()
charmenu = ObjectProperty()
statlist = ObjectProperty()
statpanel = ObjectProperty()
timepanel = ObjectProperty()
turnscroll = ObjectProperty()
kv = StringProperty()
use_kv = BooleanProperty()
play_speed = NumericProperty(1)
playbut = ObjectProperty()
portaladdbut = ObjectProperty()
portaldirbut = ObjectProperty()
dummyplace = ObjectProperty()
dummything = ObjectProperty()
dummies = ReferenceListProperty(dummyplace, dummything)
dialoglayout = ObjectProperty()
visible = BooleanProperty()
_touch = ObjectProperty(None, allownone=True)
rules_per_frame = BoundedNumericProperty(10, min=1)
tmp_block = BooleanProperty(False)
@property
def app(self):
return App.get_running_app()
def __init__(self, **kw):
for screen_kv in [
"screen",
"dummy",
"charmenu",
"statcfg",
"stepper",
]:
load_kv(screen_kv + ".kv")
super().__init__(**kw)
@logwrap(section="TimePanel")
def _update_adding_portal(self, *_):
self.boardview.adding_portal = (
self.charmenu.portaladdbut.state == "down"
)
@logwrap(section="TimePanel")
def _update_board(self, *_):
self.boardview.board = self.graphboards[self.app.character_name]
self.gridview.board = self.gridboards[self.app.character_name]
@logwrap(section="TimePanel")
def populate(self, *_):
if hasattr(self, "_populated"):
return
if (
None in (self.statpanel, self.charmenu)
or None in (self.app.character_name, self.charmenu.portaladdbut)
or self.app.character_name not in self.graphboards
):
if self.statpanel is None:
Logger.warning("MainScreen: no statpanel")
if self.charmenu is None:
Logger.warning("MainScreen: no charmenu")
elif self.charmenu.portaladdbut is None:
Logger.warning("MainScreen: CharMenu has no portal button")
if self.app.character_name is None:
Logger.warning("MainScreen: no character chosen")
elif self.app.character_name not in self.graphboards:
Logger.warning(
"MainScreen: no graphboard to represent "
+ repr(self.app.character_name)
)
Clock.schedule_once(self.populate, 0)
return
app = App.get_running_app()
binds = app._bindings
self.boardview = GraphBoardView(
scale_min=0.2,
scale_max=4.0,
size=self.mainview.size,
pos=self.mainview.pos,
board=self.graphboards[self.app.character_name],
adding_portal=self.charmenu.portaladdbut.state == "down",
)
binds["MainScreen", "mainview", "size"].add(
self.mainview.fbind("size", self.boardview.setter("size"))
)
binds["MainScreen", "mainview", "pos"].add(
self.mainview.fbind("pos", self.boardview.setter("pos"))
)
binds["CharMenu", "portaladdbut", "state"].add(
self.charmenu.portaladdbut.fbind(
"state", self._update_adding_portal
)
)
binds["ElideApp", "character_name"].add(
app.fbind("character_name", self._update_board)
)
self.calendar = Agenda(update_mode="present")
self.calendar_view = ScrollView(
size=self.mainview.size, pos=self.mainview.pos
)
self.gridview = GridBoardView(
scale_min=0.2,
scale_max=4.0,
size=self.mainview.size,
pos=self.mainview.pos,
board=self.gridboards[self.app.character_name],
)
binds["MainScreen", "mainview", "size"].add(
self.mainview.fbind("size", self.calendar_view.setter("size"))
)
binds["MainScreen", "mainview", "pos"].add(
self.mainview.fbind("pos", self.calendar_view.setter("pos"))
)
binds["MainScreen", "mainview", "size"].add(
self.mainview.fbind("size", self.gridview.setter("size"))
)
binds["MainScreen", "mainview", "pos"].add(
self.mainview.fbind("pos", self.gridview.setter("pos"))
)
self.calendar_view.add_widget(self.calendar)
self.mainview.add_widget(self.boardview)
app._unbinders.append(self.unbind_all)
self._populated = True
def unbind_all(self):
app = App.get_running_app()
binds = app._bindings
for uid in devour(binds["MainScreen", "mainview", "size"]):
self.mainview.unbind_uid("size", uid)
for uid in devour(binds["MainScreen", "mainview", "pos"]):
self.mainview.unbind_uid("pos", uid)
for uid in devour(binds["CharMenu", "portaladdbut", "state"]):
self.charmenu.portaladdbut.unbind_uid("state", uid)
for uid in devour(binds["ElideApp", "character_name"]):
app.unbind_uid("character_name", uid)
for uid in devour(binds["MainScreen", "mainview", "size"]):
self.mainview.unbind_uid("size", uid)
for uid in devour(binds["MainScreen", "mainview", "pos"]):
self.mainview.unbind_uid("pos", uid)
@logwrap(section="TimePanel")
def on_statpanel(self, *_):
if not self.app:
Clock.schedule_once(self.on_statpanel, 0)
return
app = App.get_running_app()
binds = app._bindings
for att in ("selected_proxy", "branch", "turn", "tick"):
binds["ElideApp", att].add(app.fbind(att, self._update_statlist))
@trigger
@logwrap(section="TimePanel")
def _update_statlist(self, *_):
if not self.app or not hasattr(self.app, "engine"):
return
if not self.app.selected_proxy:
self._update_statlist()
return
self.app.update_calendar(
self.statpanel.statlist, past_turns=0, future_turns=0
)
@logwrap(section="TimePanel")
def pull_visibility(self, *_):
if not self.manager:
self.visible = False
return
self.visible = self.manager.current == "main"
@logwrap(section="TimePanel")
def on_manager(self, *_):
if not self.manager:
return
self.pull_visibility()
self.manager.bind(current=self.pull_visibility)
@logwrap(section="TimePanel")
def on_playbut(self, *_):
if hasattr(self, "_play_scheduled"):
return
self._play_scheduled = Clock.schedule_interval(
self.play, 1.0 / self.play_speed
)
[docs]
@logwrap(section="TimePanel")
def on_play_speed(self, *_):
"""Change the interval at which ``self.play`` is called to match my
current ``play_speed``.
"""
if hasattr(self, "_play_scheduled"):
Clock.unschedule(self._play_scheduled)
self._play_scheduled = Clock.schedule_interval(
self.play, 1.0 / self.play_speed
)
[docs]
@logwrap(section="TimePanel")
def on_touch_down(self, touch):
if self.boardview is None:
return
if self.visible:
touch.grab(self)
for interceptor in (
self.timepanel,
self.turnscroll,
self.charmenu,
self.statpanel,
self.dummyplace,
self.dummything,
):
if interceptor.collide_point(*touch.pos):
interceptor.dispatch("on_touch_down", touch)
self.boardview.keep_selection = (
self.gridview.keep_selection
) = True
return True
if self.dialoglayout.dispatch("on_touch_down", touch):
return True
return self.mainview.dispatch("on_touch_down", touch)
[docs]
@logwrap(section="TimePanel")
def on_touch_up(self, touch):
if self.timepanel.collide_point(*touch.pos):
return self.timepanel.dispatch("on_touch_up", touch)
elif self.turnscroll.collide_point(*touch.pos):
return self.turnscroll.dispatch("on_touch_up", touch)
elif self.charmenu.collide_point(*touch.pos):
return self.charmenu.dispatch("on_touch_up", touch)
elif self.statpanel.collide_point(*touch.pos):
return self.statpanel.dispatch("on_touch_up", touch)
return self.mainview.dispatch("on_touch_up", touch)
[docs]
@logwrap(section="TimePanel")
def on_dummies(self, *_):
"""Give the dummies numbers such that, when appended to their names,
they give a unique name for the resulting new
:class:`graph.Pawn` or :class:`graph.Spot`.
"""
app = App.get_running_app()
if not app.character:
Clock.schedule_once(self.on_dummies, 0)
return
binds = app._bindings
def renum_dummy(dummy, *_):
dummy.num = dummynum(self.app.character, dummy.prefix) + 1
for dummy in self.dummies:
if dummy is None or hasattr(dummy, "_numbered"):
continue
if dummy == self.dummything:
binds["ElideApp", "pawncfg", "imgpaths"].add(
app.pawncfg.fbind("imgpaths", dummy.setter("paths"))
)
if dummy == self.dummyplace:
binds["ElideApp", "spotcfg", "imgpaths"].add(
app.spotcfg.fbind("imgpaths", dummy.setter("paths"))
)
dummy.num = dummynum(self.app.character, dummy.prefix) + 1
Logger.debug("MainScreen: dummy #{}".format(dummy.num))
dummy.bind(prefix=partial(renum_dummy, dummy))
dummy._numbered = True
@logwrap(section="TimePanel")
def update_from_time_travel(
self, command, branch, turn, tick, result, **kwargs
):
self._update_from_delta(command, branch, turn, tick, result[-1])
@logwrap(section="TimePanel")
def _update_from_delta(self, cmd, branch, turn, tick, delta, **kwargs):
self.app.branch = branch
self.app.turn = turn
self.app.tick = tick
self.statpanel.statlist.mirror = dict(self.app.selected_proxy)
[docs]
@logwrap(section="TimePanel")
def play(self, *_):
"""If the 'play' button is pressed, advance a turn.
If you want to disable this, set ``engine.universal['block'] = True``
"""
if (
self.playbut is None
or self.playbut.state == "normal"
or not hasattr(self.app, "engine")
or self.app.engine is None
or self.app.engine.closed
or self.app.engine.universal.get("block")
or not hasattr(self.app, "manager")
or self.app.manager.current != "mainscreen"
):
return
self.next_turn(cb=release_edit_lock)
@logwrap(section="TimePanel")
def _update_from_next_turn(
self, command, branch, turn, tick, result, cb=None
):
todo, deltas = result
if isinstance(todo, list):
self.dialoglayout.todo = todo
self.dialoglayout.idx = 0
self._update_from_delta(command, branch, turn, tick, deltas)
self.dialoglayout.advance_dialog()
if cb is not None:
cb(command, branch, turn, tick, result)
self.tmp_block = False
[docs]
@logwrap(section="TimePanel")
def next_turn(self, cb=None, *_):
"""Advance time by one turn, if it's not blocked.
Block time by setting ``engine.universal['block'] = True``"""
if self.tmp_block:
return
eng = self.app.engine
dial = self.dialoglayout
if eng.universal.get("block"):
Logger.info(
"MainScreen: next_turn blocked, delete universal['block'] to unblock"
)
return
if dial.todo:
if not dial.children:
Logger.info(
"MainScreen: DialogLayout has todo but no children, advancing."
)
dial.advance_dialog()
Clock.schedule_once(partial(self.next_turn, cb), self.play_speed)
return
self.tmp_block = True
self._next_turn_thread = Thread(
target=eng.next_turn,
kwargs={"cb": partial(self._update_from_next_turn, cb=cb)},
)
self._next_turn_thread.start()
self.ids.charmenu.switch_to_menu()
@logwrap(section="TimePanel")
def switch_to_calendar(self, *_):
self.app.update_calendar(self.calendar)
self.mainview.clear_widgets()
self.mainview.add_widget(self.calendar_view)
@logwrap(section="TimePanel")
def switch_to_boardview(self, *_):
self.mainview.clear_widgets()
self.app.engine.handle(
"apply_choices", choices=[self.calendar.get_track()]
)
self.mainview.add_widget(self.boardview)
@logwrap(section="TimePanel")
def toggle_gridview(self, *_):
if self.gridview in self.mainview.children:
self.mainview.clear_widgets()
self.mainview.add_widget(self.boardview)
else:
self.mainview.clear_widgets()
self.mainview.add_widget(self.gridview)
@logwrap(section="TimePanel")
def toggle_calendar(self, *_):
# TODO decide how to handle switching between >2 view types
if self.boardview in self.mainview.children:
self.switch_to_calendar()
else:
self.switch_to_boardview()
@trigger
@logwrap(section="TimePanel")
def toggle_timestream(self, *_):
if self.manager.current != "timestream":
self.manager.current = "timestream"
else:
self.manager.current = "mainscreen"
@trigger
@logwrap(section="TimePanel")
def toggle_log(self, *_):
if self.manager.current != "log":
self.manager.current = "log"
else:
self.manager.current = "mainscreen"
[docs]
class Box(Widget):
padding = VariableListProperty(6)
border = VariableListProperty(4)
font_size = StringProperty("15sp")
font_name = StringProperty(DEFAULT_FONT)
background = StringProperty()
background_color = VariableListProperty([1, 1, 1, 1])
foreground_color = VariableListProperty([0, 0, 0, 1])
[docs]
class MessageBox(Box):
"""Looks like a TextInput but doesn't accept any input.
Does support styled text with BBcode.
"""
line_spacing = NumericProperty(0)
text = StringProperty()
[docs]
class Dialog(BoxLayout):
"""MessageBox with a DialogMenu beneath it.
Set the properties ``message_kwargs`` and ``menu_kwargs``,
respectively, to control them -- but you probably want
to do that by returning a pair of dicts from an action
in lisien.
"""
message_kwargs = DictProperty({})
menu_kwargs = DictProperty({})
def __init__(self, **kwargs):
super().__init__(**kwargs)
app = App.get_running_app()
binds = app._bindings
assert not binds["Dialog", id(self), "message_kwargs"]
binds["Dialog", id(self), "message_kwargs"].add(
self.fbind("message_kwargs", self._propagate_msg_kwargs)
)
assert not binds["Dialog", id(self), "menu_kwargs"]
binds["Dialog", id(self), "menu_kwargs"].add(
self.fbind("menu_kwargs", self._propagate_menu_kwargs)
)
self._propagate_msg_kwargs()
self._propagate_menu_kwargs()
app._unbinders.append(self.unbind_all)
def unbind_all(self):
binds = App.get_running_app()._bindings
for uid in devour(binds["Dialog", id(self), "message_kwargs"]):
self.unbind_uid("message_kwargs", uid)
for uid in devour(binds["Dialog", id(self), "menu_kwargs"]):
self.unbind_uid("menu_kwargs", uid)
@partial(logwrap, section="Dialog")
def _propagate_msg_kwargs(self, *_):
if "msg" not in self.ids:
Clock.schedule_once(self._propagate_msg_kwargs, 0)
return
kw = dict(self.message_kwargs)
kw.setdefault(
"background", "atlas://data/images/defaulttheme/textinput"
)
for k, v in kw.items():
setattr(self.ids.msg, k, v)
@partial(logwrap, section="Dialog")
def _propagate_menu_kwargs(self, *_):
if "menu" not in self.ids:
Clock.schedule_once(self._propagate_menu_kwargs, 0)
return
kw = dict(self.menu_kwargs)
kw.setdefault(
"background",
"atlas://data/images/defaulttheme/vkeyboard_background",
)
for k, v in kw.items():
setattr(self.ids.menu, k, v)
[docs]
class DialogLayout(FloatLayout):
"""A layout, normally empty, that can generate dialogs
To make dialogs, set my ``todo`` property to a list. It may contain:
* Strings, which will be displayed with an "OK" button to dismiss them
* Lists of pairs of strings and callables, which generate buttons with the
string on them that, when clicked, call the callable
* Lists of pairs of dictionaries, which are interpreted as keyword
arguments to :class:`MessageBox` and :class:`DialogMenu`
In place of a callable you can use the name of a function in my
``usermod``, a Python module given by name. I'll import it when I need it.
Needs to be instantiated with a lisien ``engine`` -- probably an
:class:`EngineProxy`.
"""
dialog = ObjectProperty()
todo = ListProperty()
usermod = StringProperty("user")
userpkg = StringProperty(None, allownone=True)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.dialog = Dialog()
self._finalize()
@logwrap(section="DialogLayout")
def _finalize(self, *_):
app = App.get_running_app()
if not hasattr(app, "engine"):
Clock.schedule_once(self._finalize, 0)
return
engine = app.engine
todo = engine.universal.get("last_result")
if isinstance(todo, list):
self.todo = todo
else:
self.todo = []
self.idx = engine.universal.get("last_result_idx", 0)
engine.universal.connect(self._pull)
if self.todo:
self.advance_dialog()
@logwrap(section="DialogLayout")
def _pull(self, *_, key, value):
if key == "last_result":
self.todo = value if value and isinstance(value, list) else []
elif key == "last_result_idx":
self.idx = value if value and isinstance(value, int) else 0
@logwrap(section="DialogLayout")
def on_idx(self, *_):
lidx = self.engine.universal.get("last_result_idx")
if lidx is not None and lidx != self.idx:
self.engine.universal["last_result_idx"] = self.idx
Logger.debug(f"DialogLayout.idx = {self.idx}")
[docs]
@mainthread
@logwrap(section="DialogLayout")
def advance_dialog(self, after_ok=None, *args):
"""Try to display the next dialog described in my ``todo``.
:param after_ok: An optional callable. Will be called after the user clicks
a dialog option--as well as after any game-specific code for that option
has run.
"""
self.clear_widgets()
try:
Logger.debug(f"About to update dialog: {self.todo[0]}")
self._update_dialog(self.todo.pop(0), after_ok)
except IndexError:
if after_ok is not None:
after_ok()
@mainthread
@logwrap(section="DialogLayout")
def _update_dialog(self, diargs, after_ok, **kwargs):
if diargs is None:
Logger.debug("DialogLayout: null dialog")
return
if isinstance(diargs, tuple) and diargs[0] == "stop":
if len(diargs) == 1:
Logger.debug("DialogLayout: null dialog")
return
diargs = diargs[1]
dia = self.dialog
# Simple text dialogs just tell the player something and let them click OK
if isinstance(diargs, str):
dia.message_kwargs = {"text": diargs}
dia.menu_kwargs = {
"options": [("OK", partial(self.ok, cb=after_ok))]
}
# List dialogs are for when you need the player to make a choice and don't care much
# about presentation
elif isinstance(diargs, list):
dia.message_kwargs = {"text": "Select from the following:"}
dia.menu_kwargs = {
"options": list(
map(partial(self._munge_menu_option, after_ok), diargs)
)
}
# For real control of the dialog, you need a pair of dicts --
# the 0th describes the message shown to the player, the 1th
# describes the menu below
elif isinstance(diargs, tuple):
if len(diargs) != 2:
raise TypeError(
"Need a tuple of (message, menu) where message is a string, "
"and menu is a dict or list describing options"
)
msgkwargs, mnukwargs = diargs
if isinstance(msgkwargs, dict):
dia.message_kwargs = msgkwargs
elif isinstance(msgkwargs, str):
dia.message_kwargs["text"] = msgkwargs
else:
raise TypeError("Message must be dict or str")
if isinstance(mnukwargs, dict):
mnukwargs["options"] = list(
map(
partial(self._munge_menu_option, after_ok),
mnukwargs["options"],
)
)
dia.menu_kwargs = mnukwargs
elif isinstance(mnukwargs, (list, tuple)):
dia.menu_kwargs["options"] = list(
map(partial(self._munge_menu_option, after_ok), mnukwargs)
)
else:
raise TypeError("Menu must be dict or list")
else:
raise TypeError(
"Don't know how to turn {} into a dialog".format(type(diargs))
)
if dia.parent != self:
Logger.debug("DialogLayout: Adding the dialog to the layout")
self.add_widget(dia)
else:
Logger.debug("DialogLayout: Dialog is already in the layout")
[docs]
@logwrap(section="DialogLayout")
def ok(self, *_, cb=None, cb2=None):
"""Clear dialog widgets, call ``cb`` if provided, and advance the dialog queue
``cb2`` will be called after advancing the dialog."""
self.clear_widgets()
if cb:
cb()
self.advance_dialog(after_ok=cb2)
@logwrap(section="DialogLayout")
def _lookup_func(self, funcname):
from importlib import import_module
if not hasattr(self, "_usermod"):
self._usermod = import_module(self.usermod, self.userpkg)
return getattr(self.usermod, funcname)
@logwrap(section="DialogLayout")
def _munge_menu_option(self, after_ok, option):
if not isinstance(option, tuple):
raise TypeError
name, func = option
if func is None:
return name, partial(self.ok, cb2=after_ok)
if callable(func):
return name, partial(self.ok, cb=func, cb2=after_ok)
if isinstance(func, tuple):
fun = func[0]
if isinstance(fun, str):
fun = self._lookup_func(fun)
args = func[1]
if len(func) == 3:
kwargs = func[2]
func = partial(fun, *args, **kwargs)
else:
func = partial(fun, *args)
if isinstance(func, str):
func = self._lookup_func(func)
return name, partial(self.ok, cb=func, cb2=after_ok)