--- a/xsge_gui/__init__.py +++ b/xsge_gui/__init__.py @@ -1346,199 +1346,282 @@ """ - def show(self): + def show(self, on_close=None): """ - Show this dialog and start its loop. + Show this dialog. - Like :meth:`xsge_gui.Window.show`, this method adds the dialog - to its parent. It then starts this dialog's loop. Call - :meth:`xsge_gui.Dialog.hide` on this dialog to end the loop. + Web port: the dialog no longer runs a loop of its own. It is + queued here and stepped by the main loop of :class:`sge.dsp.Game` + (see ``_ModalRunner``), so this method returns right away and the + code after it runs before the dialog is on the screen. Dialogs + shown one after the other from the same piece of code appear in + that order, each one after the previous one is closed; when this + is called from an event of a dialog that is still open, the new + dialog is stacked on top of it. - The loop suppresses all events, but allows the Quit event to + ``on_close``, when given, is called without arguments after the + dialog has been hidden, the place to use what it returned. + + The dialog suppresses all events, but allows the Quit event to propagate. """ - try: - parent = self.parent() - if parent is not None: - screenshot = sge.gfx.Sprite.from_screenshot() - super().show() - parent.keyboard_focused_window = self - while self in parent.windows: - self.move_to_front() - - # Input events - sge.game.pump_input() - while sge.game.input_events: - event = sge.game.input_events.pop(0) - - if isinstance(event, sge.input.KeyPress): - self.event_key_press(event.key, event.char) - widget = self.keyboard_focused_widget - if widget is not None: - widget.event_key_press(event.key, event.char) - self.event_global_key_press(event.key, event.char) - for widget in self.widgets: - widget.event_global_key_press(event.key, - event.char) - elif isinstance(event, sge.input.KeyRelease): - self.event_key_release(event.key) - widget = self.keyboard_focused_widget - if widget is not None: - widget.event_key_release(event.key) - self.event_global_key_release(event.key) - for widget in self.widgets: - widget.event_global_key_release(event.key) - elif isinstance(event, sge.input.MouseButtonPress): - if parent.get_mouse_focused_window() is self: - if self.get_mouse_on_titlebar(): - self.event_titlebar_mouse_button_press( - event.button) - else: - self.event_mouse_button_press(event.button) - widget = self.get_mouse_focused_widget() - if widget is not None: - widget.event_mouse_button_press( - event.button) - - self.event_global_mouse_button_press(event.button) - for widget in self.widgets: - widget.event_global_mouse_button_press( - event.button) - elif isinstance(event, sge.input.MouseButtonRelease): - if parent.get_mouse_focused_window() is self: - if self.get_mouse_on_titlebar(): - self.event_titlebar_mouse_button_release( - event.button) - else: - self.event_mouse_button_release(event.button) - widget = self.get_mouse_focused_widget() - if widget is not None: - widget.event_mouse_button_release( - event.button) - - self.event_global_mouse_button_release(event.button) - for widget in self.widgets: - widget.event_global_mouse_button_release( - event.button) - elif isinstance(event, sge.input.JoystickAxisMove): - self.event_joystick_axis_move( - event.js_name, event.js_id, event.axis, - event.value) - widget = self.keyboard_focused_widget - if widget is not None: - widget.event_joystick_axis_move( - event.js_name, event.js_id, event.axis, - event.value) - self.event_global_joystick_axis_move( - event.js_name, event.js_id, event.axis, - event.value) - for widget in self.widgets: - widget.event_global_joystick_axis_move( - event.js_name, event.js_id, event.axis, - event.value) - elif isinstance(event, sge.input.JoystickHatMove): - self.event_joystick_hat_move( - event.js_name, event.js_id, event.hat, event.x, - event.y) - widget = self.keyboard_focused_widget - if widget is not None: - widget.event_joystick_hat_move( - event.js_name, event.js_id, event.hat, - event.x, event.y) - self.event_global_joystick_hat_move( - event.js_name, event.js_id, event.hat, event.x, - event.y) - for widget in self.widgets: - widget.event_global_joystick_hat_move( - event.js_name, event.js_id, event.hat, - event.x, event.y) - elif isinstance(event, sge.input.JoystickButtonPress): - self.event_joystick_button_press( - event.js_name, event.js_id, event.button) - widget = self.keyboard_focused_widget - if widget is not None: - widget.event_joystick_button_press( - event.js_name, event.js_id, event.button) - self.event_global_joystick_button_press( - event.js_name, event.js_id, event.button) - for widget in self.widgets: - widget.event_global_joystick_button_press( - event.js_name, event.js_id, event.button) - elif isinstance(event, sge.input.JoystickButtonRelease): - self.event_joystick_button_release( - event.js_name, event.js_id, event.button) - widget = self.keyboard_focused_widget - if widget is not None: - widget.event_joystick_button_release( - event.js_name, event.js_id, event.button) - self.event_global_joystick_button_release( - event.js_name, event.js_id, event.button) - for widget in self.widgets: - widget.event_global_joystick_button_release( - event.js_name, event.js_id, event.button) - elif isinstance(event, sge.input.JoystickEvent): - self.event_joystick(event.js_name, event.js_id, - event.input_type, - event.input_id, event.value) - widget = self.keyboard_focused_widget - if widget is not None: - widget.event_joystick( - event.js_name, event.js_id, - event.input_type, event.input_id, - event.value) - self.event_global_joystick( - event.js_name, event.js_id, event.input_type, - event.input_id, event.value) - for widget in self.widgets: - widget.event_global_joystick( - event.js_name, event.js_id, - event.input_type, event.input_id, - event.value) - elif isinstance(event, sge.input.KeyboardFocusGain): - sge.game.event_paused_gain_keyboard_focus() - elif isinstance(event, sge.input.KeyboardFocusLose): - sge.game.event_paused_lose_keyboard_focus() - elif isinstance(event, sge.input.MouseFocusGain): - sge.game.event_paused_gain_mouse_focus() - elif isinstance(event, sge.input.MouseFocusLose): - sge.game.event_paused_lose_mouse_focus() - elif isinstance(event, sge.input.WindowResize): - sge.game.event_paused_window_resize() - elif isinstance(event, sge.input.QuitRequest): - sge.game.input_events.insert(0, event) - self.hide() - return - - # Regulate speed - time_passed = sge.game.regulate_speed() - - if sge.game.delta: - t = min(time_passed, 1000 / sge.game.delta_min) - delta_mult = t / (1000 / sge.game.fps) + self._modal_on_close = on_close + self._modal_opened = False + self._modal_closed = False + _runner.add(self) + + def hide(self): + """Remove this dialog from its parent handler.""" + self._modal_closed = True + super().hide() + + def _modal_open(self): + # Called by the runner when this dialog reaches the top. + self._modal_opened = True + parent = self.parent() + if parent is not None: + self._modal_screenshot = sge.gfx.Sprite.from_screenshot() + Window.show(self) + parent.keyboard_focused_window = self + + def _modal_frame(self, time_passed): + # One iteration of what used to be the loop of show(). + parent = self.parent() + if parent is None: + return + self.move_to_front() + + # Input events + sge.game.pump_input() + while sge.game.input_events: + event = sge.game.input_events.pop(0) + + if isinstance(event, sge.input.KeyPress): + self.event_key_press(event.key, event.char) + widget = self.keyboard_focused_widget + if widget is not None: + widget.event_key_press(event.key, event.char) + self.event_global_key_press(event.key, event.char) + for widget in self.widgets: + widget.event_global_key_press(event.key, + event.char) + elif isinstance(event, sge.input.KeyRelease): + self.event_key_release(event.key) + widget = self.keyboard_focused_widget + if widget is not None: + widget.event_key_release(event.key) + self.event_global_key_release(event.key) + for widget in self.widgets: + widget.event_global_key_release(event.key) + elif isinstance(event, sge.input.MouseButtonPress): + if parent.get_mouse_focused_window() is self: + if self.get_mouse_on_titlebar(): + self.event_titlebar_mouse_button_press( + event.button) else: - delta_mult = 1 + self.event_mouse_button_press(event.button) + widget = self.get_mouse_focused_widget() + if widget is not None: + widget.event_mouse_button_press( + event.button) + + self.event_global_mouse_button_press(event.button) + for widget in self.widgets: + widget.event_global_mouse_button_press( + event.button) + elif isinstance(event, sge.input.MouseButtonRelease): + if parent.get_mouse_focused_window() is self: + if self.get_mouse_on_titlebar(): + self.event_titlebar_mouse_button_release( + event.button) + else: + self.event_mouse_button_release(event.button) + widget = self.get_mouse_focused_widget() + if widget is not None: + widget.event_mouse_button_release( + event.button) + + self.event_global_mouse_button_release(event.button) + for widget in self.widgets: + widget.event_global_mouse_button_release( + event.button) + elif isinstance(event, sge.input.JoystickAxisMove): + self.event_joystick_axis_move( + event.js_name, event.js_id, event.axis, + event.value) + widget = self.keyboard_focused_widget + if widget is not None: + widget.event_joystick_axis_move( + event.js_name, event.js_id, event.axis, + event.value) + self.event_global_joystick_axis_move( + event.js_name, event.js_id, event.axis, + event.value) + for widget in self.widgets: + widget.event_global_joystick_axis_move( + event.js_name, event.js_id, event.axis, + event.value) + elif isinstance(event, sge.input.JoystickHatMove): + self.event_joystick_hat_move( + event.js_name, event.js_id, event.hat, event.x, + event.y) + widget = self.keyboard_focused_widget + if widget is not None: + widget.event_joystick_hat_move( + event.js_name, event.js_id, event.hat, + event.x, event.y) + self.event_global_joystick_hat_move( + event.js_name, event.js_id, event.hat, event.x, + event.y) + for widget in self.widgets: + widget.event_global_joystick_hat_move( + event.js_name, event.js_id, event.hat, + event.x, event.y) + elif isinstance(event, sge.input.JoystickButtonPress): + self.event_joystick_button_press( + event.js_name, event.js_id, event.button) + widget = self.keyboard_focused_widget + if widget is not None: + widget.event_joystick_button_press( + event.js_name, event.js_id, event.button) + self.event_global_joystick_button_press( + event.js_name, event.js_id, event.button) + for widget in self.widgets: + widget.event_global_joystick_button_press( + event.js_name, event.js_id, event.button) + elif isinstance(event, sge.input.JoystickButtonRelease): + self.event_joystick_button_release( + event.js_name, event.js_id, event.button) + widget = self.keyboard_focused_widget + if widget is not None: + widget.event_joystick_button_release( + event.js_name, event.js_id, event.button) + self.event_global_joystick_button_release( + event.js_name, event.js_id, event.button) + for widget in self.widgets: + widget.event_global_joystick_button_release( + event.js_name, event.js_id, event.button) + elif isinstance(event, sge.input.JoystickEvent): + self.event_joystick(event.js_name, event.js_id, + event.input_type, + event.input_id, event.value) + widget = self.keyboard_focused_widget + if widget is not None: + widget.event_joystick( + event.js_name, event.js_id, + event.input_type, event.input_id, + event.value) + self.event_global_joystick( + event.js_name, event.js_id, event.input_type, + event.input_id, event.value) + for widget in self.widgets: + widget.event_global_joystick( + event.js_name, event.js_id, + event.input_type, event.input_id, + event.value) + elif isinstance(event, sge.input.KeyboardFocusGain): + sge.game.event_paused_gain_keyboard_focus() + elif isinstance(event, sge.input.KeyboardFocusLose): + sge.game.event_paused_lose_keyboard_focus() + elif isinstance(event, sge.input.MouseFocusGain): + sge.game.event_paused_gain_mouse_focus() + elif isinstance(event, sge.input.MouseFocusLose): + sge.game.event_paused_lose_mouse_focus() + elif isinstance(event, sge.input.WindowResize): + sge.game.event_paused_window_resize() + elif isinstance(event, sge.input.QuitRequest): + sge.game.input_events.insert(0, event) + self.hide() + return + + if sge.game.delta: + t = min(time_passed, 1000 / sge.game.delta_min) + delta_mult = t / (1000 / sge.game.fps) + else: + delta_mult = 1 + + # Project screenshot + sge.game.project_sprite(self._modal_screenshot, 0, 0, 0) - # Project screenshot - sge.game.project_sprite(screenshot, 0, 0, 0) + # Project windows + self.event_step(time_passed, delta_mult) + for widget in self.widgets: + widget.event_step(time_passed, delta_mult) + + for window in parent.windows[:]: + window.refresh() + + # Refresh + sge.game.refresh() + + +class _ModalRunner: + + # Steps the open dialogs from the main loop of sge.dsp.Game. ``stack`` + # holds the dialogs, the last one being the one running; dialogs are + # opened when they reach the top. Dialogs shown while the code of an + # event runs go to ``batch`` and are placed when the event is done, + # so that they run in the order they were shown: the finished dialogs + # at the top of the stack leave, then the batch goes on the stack in + # reverse, which makes the first one shown the next to run. + + def __init__(self): + self.stack = [] + self.batch = [] + self.hooked = None + + def add(self, dialog): + game = sge.game + if self.hooked is not game: + game.modal_active = self.active + game.modal_step = self.step + self.hooked = game + self.batch.append(dialog) + + def active(self): + return bool(self.stack or self.batch) + + def _finished(self, dialog): + if dialog._modal_closed: + return True + if not dialog._modal_opened: + return False + parent = dialog.parent() + return parent is None or dialog not in parent.windows + + def _place(self): + # Let the finished dialogs go (their callbacks may show more + # dialogs) and put the new ones on the stack. + while True: + while self.stack and self._finished(self.stack[-1]): + dialog = self.stack.pop() + callback = dialog._modal_on_close + dialog._modal_on_close = None + if callback is not None: + callback() + if self.batch: + self.stack.extend(reversed(self.batch)) + self.batch = [] + continue + break + + def step(self, time_passed): + self._place() + if self.stack and not self.stack[-1]._modal_opened: + self.stack[-1]._modal_open() + if not self.stack: + sge.game.pump_input() + sge.game.input_events = [] + return + + self.stack[-1]._modal_frame(time_passed) + self._place() + if not self.stack: + sge.game.pump_input() + sge.game.input_events = [] - # Project windows - self.event_step(time_passed, delta_mult) - for widget in self.widgets: - widget.event_step(time_passed, delta_mult) - - for window in parent.windows[:]: - window.refresh() - - # Refresh - sge.game.refresh() - - sge.game.pump_input() - sge.game.input_events = [] - except RuntimeError: - # Recursion is possible in some cases due to the way this - # works. This will prevent exceeding recursion depth from - # crashing the entire game. - print("Recursion depth exceeded! Dialog canceled.") + +_runner = _ModalRunner() class MenuWindow(Window): @@ -3566,7 +3649,7 @@ def show_message(parent=None, message="", title="Message", buttons=("Ok",), - default=-1, width=320, height=None): + default=-1, width=320, height=None, on_close=None): """ Show a message and return the button pressed. @@ -3589,9 +3672,11 @@ object created. If set to :const:`None`, set the height automatically based on the space needed for the text. - Value returned is the index of the button pressed, where ``0`` is - the leftmost button, or :const:`None` if no button was pressed (i.e. - the close button on the window frame was pressed instead). + The index of the button pressed, where ``0`` is the leftmost button, + or :const:`None` if no button was pressed (i.e. the close button on + the window frame was pressed instead), is passed to ``on_close`` + once the dialog is closed. Web port: the function returns + :const:`None` right away, since dialogs no longer block. See the documentation for :class:`xsge_gui.MessageDialog` for more information. @@ -3604,17 +3689,19 @@ w = MessageDialog(parent, message=message, title=title, buttons=buttons, default=default, width=width, height=height) - w.show() - w.destroy() - if destroy_parent: - parent.destroy() + def finish(): + w.destroy() + if destroy_parent: + parent.destroy() + if on_close is not None: + on_close(w.choice) - return w.choice + w.show(on_close=finish) def get_text_entry(parent=None, message="", title="Text Entry", text="", - width=320, height=None): + width=320, height=None, on_close=None): """ Return text entered by the user. @@ -3634,8 +3721,10 @@ object created. If set to :const:`None`, set the height automatically based on the space needed for the text. - Value returned is the text entered if the "Ok" button is pressed, or - :const:`None` otherwise. + The text entered if the "Ok" button is pressed, or :const:`None` + otherwise, is passed to ``on_close`` once the dialog is closed. Web + port: the function returns :const:`None` right away, since dialogs + no longer block. See the documentation for :class:`xsge_gui.TextEntryDialog` for more information. @@ -3648,13 +3737,15 @@ w = TextEntryDialog(parent, message=message, title=title, text=text, width=width, height=height) - w.show() - w.destroy() - if destroy_parent: - parent.destroy() + def finish(): + w.destroy() + if destroy_parent: + parent.destroy() + if on_close is not None: + on_close(w.text) - return w.text + w.show(on_close=finish) def get_menu_selection(x, y, items, parent=None, default=0, font_normal=None, @@ -3665,9 +3756,10 @@ outline_normal=None, outline_selected=None, outline_thickness_normal=0, outline_thickness_selected=0, selection_prefix="", - selection_suffix=""): + selection_suffix="", on_close=None): """ - Show a menu and return the index of the menu item selected. + Show a menu; the index of the menu item selected is passed to + ``on_close`` (web port: the function returns right away). Arguments: @@ -3699,15 +3791,17 @@ selection_suffix=selection_suffix) default %= len(w.widgets) w.keyboard_selected_widget = w.widgets[default] - w.show() - w.destroy() - if destroy_parent: - parent.destroy() + def finish(): + w.destroy() + if destroy_parent: + parent.destroy() + if on_close is not None: + on_close(w.choice) - return w.choice - else: - return None + w.show(on_close=finish) + elif on_close is not None: + on_close(None) def _refresh_screen(time_passed, delta_mult):