# Copyright 2004-2019 Tom Rothamel # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # This contains various Displayables that handle events. from __future__ import print_function import renpy.display import renpy.audio from renpy.display.render import render, Render import pygame_sdl2 as pygame import math def compile_event(key, keydown): """ Compiles a keymap entry into a python expression. keydown determines if we are dealing with keys going down (press), or keys going up (release). """ # Lists or tuples get turned into or expressions. if isinstance(key, (list, tuple)): if not key: return "(False)" return "(" + " or ".join([compile_event(i, keydown) for i in key]) + ")" # If it's in config.keymap, compile what's in config.keymap. if key in renpy.config.keymap: return compile_event(renpy.config.keymap[key], keydown) if key in renpy.config.default_keymap: return compile_event(renpy.config.default_keymap[key], keydown) if key is None: return "(False)" part = key.split("_") # Deal with the mouse. if part[0] == "mousedown": if keydown: return "(ev.type == %d and ev.button == %d)" % (pygame.MOUSEBUTTONDOWN, int(part[1])) else: return "(False)" if part[0] == "mouseup": if keydown: return "(ev.type == %d and ev.button == %d)" % (pygame.MOUSEBUTTONUP, int(part[1])) else: return "(False)" # Deal with the Joystick / Gamepad. if part[0] == "joy" or part[0] == "pad": return "(False)" MODIFIERS = { "keydown", "keyup", "repeat", "alt", "meta", "shift", "noshift", "ctrl" } modifiers = set() while part[0] in MODIFIERS: modifiers.add(part.pop(0)) key = "_".join(part) if "keydown" in modifiers: keydown = True elif "keyup" in modifiers: keydown = False # Otherwise, deal with it as a key. if keydown: rv = "(ev.type == %d" % pygame.KEYDOWN else: rv = "(ev.type == %d" % pygame.KEYUP if "repeat" in modifiers: rv += " and (ev.repeat)" else: rv += " and (not ev.repeat)" if key not in [ "K_LALT", "K_RALT" ]: if "alt" in modifiers: rv += " and (ev.mod & %d)" % pygame.KMOD_ALT else: rv += " and not (ev.mod & %d)" % pygame.KMOD_ALT if key not in [ "K_LGUI", "K_RGUI" ]: if "meta" in modifiers: rv += " and (ev.mod & %d)" % pygame.KMOD_META else: rv += " and not (ev.mod & %d)" % pygame.KMOD_META if key not in [ "K_LCTRL", "K_RCTRL" ]: if "ctrl" in modifiers: rv += " and (ev.mod & %d)" % pygame.KMOD_CTRL else: rv += " and not (ev.mod & %d)" % pygame.KMOD_CTRL if key not in [ "K_LSHIFT", "K_RSHIFT" ]: if "shift" in modifiers: rv += " and (ev.mod & %d)" % pygame.KMOD_SHIFT if "noshift" in modifiers: rv += " and not (ev.mod & %d)" % pygame.KMOD_SHIFT if len(part) == 1: if len(part[0]) != 1: if renpy.config.developer: raise Exception("Invalid key specifier %s" % key) else: return "(False)" rv += " and ev.unicode == %r)" % part[0] else: if part[0] != "K": if renpy.config.developer: raise Exception("Invalid key specifier %s" % key) else: return "(False)" rv += " and ev.key == %d)" % (getattr(pygame.constants, key)) return rv # These store a lambda for each compiled key in the system. event_cache = { } keyup_cache = { } def clear_keymap_cache(): """ :doc: other Clears the keymap cache. This allows changes to :var:`config.keymap` to take effect without restarting Ren'Py. """ event_cache.clear() keyup_cache.clear() def queue_event(name, up=False, **kwargs): """ :doc: other Queues an event with the given name. `Name` should be one of the event names in :var:`config.keymap`, or a list of such names. `up` This should be false when the event begins (for example, when a keyboard button is pressed.) It should be true when the event ends (when the button is released.) The event is queued at the time this function is called. This function will not work to replace an event with another - doing so will change event order. (Use :var:`config.keymap` instead.) This method is threadsafe. """ # Avoid queueing events before we're ready. if not renpy.display.interface: return if not isinstance(name, (list, tuple)): name = [ name ] data = { "eventnames" : name, "up" : up } data.update(kwargs) ev = pygame.event.Event(renpy.display.core.EVENTNAME, data) pygame.event.post(ev) def map_event(ev, keysym): """ :doc: udd_utility Returns true if the pygame event `ev` matches `keysym` `keysym` One of: * The name of a keybinding in :var:`config.keymap`. * A keysym, as documented in the :ref:`keymap` section. * A list containing one or more keysyms. """ if ev.type == renpy.display.core.EVENTNAME: if (keysym in ev.eventnames) and not ev.up: return True return False check_code = event_cache.get(keysym, None) if check_code is None: check_code = eval("lambda ev : " + compile_event(keysym, True), globals()) event_cache[keysym] = check_code return check_code(ev) def map_keyup(ev, name): """Returns true if the event matches the named keycode being released.""" if ev.type == renpy.display.core.EVENTNAME: if (name in ev.eventnames) and ev.up: return True check_code = keyup_cache.get(name, None) if check_code is None: check_code = eval("lambda ev : " + compile_event(name, False), globals()) keyup_cache[name] = check_code return check_code(ev) def skipping(ev): """ This handles setting skipping in response to the press of one of the CONTROL keys. The library handles skipping in response to TAB. """ if not renpy.config.allow_skipping: return if not renpy.store._skipping: return if map_event(ev, "skip"): renpy.config.skipping = "slow" renpy.exports.restart_interaction() if map_keyup(ev, "skip") or map_event(ev, "stop_skipping"): renpy.config.skipping = None renpy.exports.restart_interaction() return def inspector(ev): return map_event(ev, "inspector") ############################################################################## # Utility functions for dealing with actions. def predict_action(var): """ Predicts some of the actions that may be caused by a variable. """ if var is None: return if isinstance(var, renpy.ui.Action): var.predict() if isinstance(var, (list, tuple)): for i in var: predict_action(i) def run(action, *args, **kwargs): """ :doc: run :name: renpy.run :args: (action) Run an action or list of actions. A single action is called with no arguments, a list of actions is run in order using this function, and None is ignored. Returns the result of the first action to return a value. """ if action is None: return None if isinstance(action, (list, tuple)): rv = None for i in action: new_rv = run(i, *args, **kwargs) if new_rv is not None: rv = new_rv return rv return action(*args, **kwargs) def run_unhovered(var): """ Calls the unhovered method on the variable, if it exists. """ if var is None: return None if isinstance(var, (list, tuple)): for i in var: run_unhovered(i) return f = getattr(var, "unhovered", None) if f is not None: f() def run_periodic(var, st): if isinstance(var, (list, tuple)): rv = None for i in var: v = run_periodic(i, st) if rv is None or v < rv: rv = v return rv if isinstance(var, renpy.ui.Action): return var.periodic(st) def get_tooltip(action): if isinstance(action, (list, tuple)): for i in action: rv = get_tooltip(i) if rv is not None: return rv return None func = getattr(action, "get_tooltip", None) if func is None: return None return func() def is_selected(action): """ :name: renpy.is_selected :doc: run Returns true if `action` indicates it is selected, or false otherwise. """ if isinstance(action, (list, tuple)): for i in action: if isinstance(i, renpy.store.SelectedIf): # @UndefinedVariable return i.get_selected() return any(is_selected(i) for i in action) elif isinstance(action, renpy.ui.Action): return action.get_selected() else: return False def is_sensitive(action): """ :name: renpy.is_sensitive :doc: run Returns true if `action` indicates it is sensitive, or False otherwise. """ if isinstance(action, (list, tuple)): for i in action: if isinstance(i, renpy.store.SensitiveIf): # @UndefinedVariable return i.get_sensitive() return all(is_sensitive(i) for i in action) elif isinstance(action, renpy.ui.Action): return action.get_sensitive() else: return True def alt(clicked): if isinstance(clicked, (list, tuple)): rv = [ ] for i in clicked: t = alt(i) if t is not None: rv.append(t) if rv: return " ".join(rv) else: return None if isinstance(clicked, renpy.ui.Action): return clicked.alt else: return None ############################################################################## # Special-Purpose Displayables class Keymap(renpy.display.layout.Null): """ This is a behavior that maps keys to actions that are called when the key is pressed. The keys are specified by giving the appropriate k_constant from pygame.constants, or the unicode for the key. """ def __init__(self, replaces=None, activate_sound=None, **keymap): if activate_sound is not None: super(Keymap, self).__init__(style='default', activate_sound=activate_sound) else: super(Keymap, self).__init__(style='default') self.keymap = keymap def event(self, ev, x, y, st): for name, action in self.keymap.iteritems(): if map_event(ev, name): renpy.exports.play(self.style.activate_sound) rv = run(action) if rv is not None: return rv raise renpy.display.core.IgnoreEvent() def predict_one_action(self): for i in self.keymap.itervalues(): predict_action(i) class RollForward(renpy.display.layout.Null): """ This behavior implements rollforward. """ def __init__(self, value, **properties): super(RollForward, self).__init__(**properties) self.value = value def event(self, ev, x, y, st): if map_event(ev, "rollforward"): return renpy.exports.roll_forward_core(self.value) class PauseBehavior(renpy.display.layout.Null): """ This is a class implementing the Pause behavior, which is to return a value after a certain amount of time has elapsed. """ voice = False def __init__(self, delay, result=False, voice=False, **properties): super(PauseBehavior, self).__init__(**properties) self.delay = delay self.result = result self.voice = voice def event(self, ev, x, y, st): if st >= self.delay: if self.voice and renpy.config.nw_voice: if (not renpy.config.afm_callback()) or renpy.display.tts.is_active(): renpy.game.interface.timeout(0.05) return # If we have been drawn since the timeout, simply return # true. Otherwise, force a redraw, and return true when # it comes back. if renpy.game.interface.drawn_since(st - self.delay): return self.result else: renpy.game.interface.force_redraw = True renpy.game.interface.timeout(max(self.delay - st, 0)) class SoundStopBehavior(renpy.display.layout.Null): """ This is a class implementing the sound stop behavior, which is to return False when a sound is no longer playing on the named channel. """ def __init__(self, channel, result=False, **properties): super(SoundStopBehavior, self).__init__(**properties) self.channel = channel self.result = result def event(self, ev, x, y, st): if not renpy.audio.music.get_playing(self.channel): return self.result renpy.game.interface.timeout(.025) class SayBehavior(renpy.display.layout.Null): """ This is a class that implements the say behavior, which is to return True (ending the interaction) if the user presses space or enter, or clicks the left mouse button. """ focusable = True text = None dismiss_unfocused = [ 'dismiss_unfocused' ] def __init__(self, default=True, afm=None, dismiss=[ 'dismiss' ], allow_dismiss=None, dismiss_unfocused=[ 'dismiss_unfocused' ], **properties): super(SayBehavior, self).__init__(default=default, **properties) if not isinstance(dismiss, (list, tuple)): dismiss = [ dismiss ] if afm is not None: self.afm_length = len(afm) else: self.afm_length = None # What keybindings lead to dismissal? self.dismiss = dismiss self.allow_dismiss = allow_dismiss def _tts_all(self): raise renpy.display.tts.TTSRoot() def set_text(self, text): self.text = text try: afm_text = text.text[0][text.start:text.end] afm_text = renpy.text.extras.filter_text_tags(afm_text, allow=[]) self.afm_length = max(len(afm_text), 1) except: self.afm_length = max(text.end - text.start, 1) def event(self, ev, x, y, st): if self.afm_length and renpy.game.preferences.afm_time and renpy.game.preferences.afm_enable: afm_delay = ( 1.0 * ( renpy.config.afm_bonus + self.afm_length ) / renpy.config.afm_characters ) * renpy.game.preferences.afm_time if self.text is not None: afm_delay += self.text.get_time() if st > afm_delay: if renpy.config.afm_callback: if renpy.config.afm_callback() and not renpy.display.tts.is_active(): return True else: renpy.game.interface.timeout(0.1) else: return True else: renpy.game.interface.timeout(afm_delay - st) dismiss = [ (i, True) for i in self.dismiss ] + [ (i, False) for i in self.dismiss_unfocused ] for dismiss_event, check_focus in dismiss: if map_event(ev, dismiss_event): if check_focus and not self.is_focused(): continue if renpy.config.skipping: renpy.config.skipping = None renpy.exports.restart_interaction() raise renpy.display.core.IgnoreEvent() if not renpy.config.enable_rollback_side: rollback_side = "disable" if renpy.exports.mobile: rollback_side = renpy.game.preferences.mobile_rollback_side else: rollback_side = renpy.game.preferences.desktop_rollback_side if ev.type == pygame.MOUSEBUTTONUP: percent = 1.0 * x / renpy.config.screen_width if rollback_side == "left": if percent < renpy.config.rollback_side_size: renpy.exports.rollback() raise renpy.display.core.IgnoreEvent() elif rollback_side == "right": if (1.0 - percent) < renpy.config.rollback_side_size: renpy.exports.rollback() raise renpy.display.core.IgnoreEvent() if renpy.game.preferences.using_afm_enable and \ renpy.game.preferences.afm_enable and \ not renpy.game.preferences.afm_after_click: renpy.game.preferences.afm_enable = False renpy.exports.restart_interaction() raise renpy.display.core.IgnoreEvent() if self.allow_dismiss: if not self.allow_dismiss(): raise renpy.display.core.IgnoreEvent() return True skip_delay = renpy.config.skip_delay / 1000.0 if renpy.config.skipping and renpy.config.allow_skipping and renpy.store._skipping: if ev.type == renpy.display.core.TIMEEVENT and st >= skip_delay: if renpy.game.preferences.skip_unseen: return True elif renpy.config.skipping == "fast": return True elif renpy.game.context().seen_current(True): return True else: renpy.config.skipping = None renpy.exports.restart_interaction() else: renpy.game.interface.timeout(skip_delay - st) return None ############################################################################## # Button KEY_EVENTS = ( pygame.KEYDOWN, pygame.KEYUP, pygame.TEXTEDITING, pygame.TEXTINPUT ) class Button(renpy.display.layout.Window): keymap = { } action = None alternate = None longpress_start = None longpress_x = None longpress_y = None role_parameter = None keysym = None alternate_keysym = None # This locks the displayable against further change. locked = False def __init__(self, child=None, style='button', clicked=None, hovered=None, unhovered=None, action=None, role=None, time_policy=None, keymap={}, alternate=None, selected=None, sensitive=None, keysym=None, alternate_keysym=None, **properties): if isinstance(clicked, renpy.ui.Action): action = clicked super(Button, self).__init__(child, style=style, **properties) self.action = action self.selected = selected self.sensitive = sensitive self.clicked = clicked self.hovered = hovered self.unhovered = unhovered self.alternate = alternate self.focusable = True # (clicked is not None) or (action is not None) self.role_parameter = role self.keymap = keymap self.keysym = keysym self.alternate_keysym = alternate_keysym self.time_policy_data = None self._duplicatable = False def _duplicate(self, args): if args and args.args: args.extraneous() return self def _get_tooltip(self): if self._tooltip is not None: return self._tooltip return get_tooltip(self.action) def _in_current_store(self): rv = self._copy() rv.style = self.style.copy() rv.set_style_prefix(self.style.prefix, True) rv.focusable = False rv.locked = True return rv def predict_one_action(self): predict_action(self.clicked) predict_action(self.hovered) predict_action(self.unhovered) predict_action(self.alternate) if self.keymap: for v in self.keymap.itervalues(): predict_action(v) def render(self, width, height, st, at): if self.style.time_policy: st, self.time_policy_data = self.style.time_policy(st, self.time_policy_data, self.style) rv = super(Button, self).render(width, height, st, at) if self.clicked: rect = self.style.focus_rect if rect is not None: fx, fy, fw, fh = rect else: fx = self.style.left_margin fy = self.style.top_margin fw = rv.width - self.style.right_margin fh = rv.height - self.style.bottom_margin mask = self.style.focus_mask if mask is True: mask = rv elif mask is not None: try: mask = renpy.display.render.render(mask, rv.width, rv.height, st, at) except: if callable(mask): mask = mask else: raise Exception("Focus_mask must be None, True, a displayable, or a callable.") if mask is not None: fmx = 0 fmy = 0 else: fmx = None fmy = None rv.add_focus(self, None, fx, fy, fw, fh, fmx, fmy, mask) return rv def focus(self, default=False): super(Button, self).focus(default) rv = None if not default: rv = run(self.hovered) self.set_transform_event(self.role + "hover") if self.child is not None: self.child.set_transform_event(self.role + "hover") return rv def unfocus(self, default=False): super(Button, self).unfocus(default) self.longpress_start = None if not default: run_unhovered(self.hovered) run(self.unhovered) self.set_transform_event(self.role + "idle") if self.child is not None: self.child.set_transform_event(self.role + "idle") def is_selected(self): if self.selected is not None: return self.selected return is_selected(self.action) def is_sensitive(self): if self.sensitive is not None: return self.sensitive return is_sensitive(self.action) def per_interact(self): if not self.locked: if self.action is not None: if self.is_selected(): role = 'selected_' else: role = '' if self.is_sensitive(): clicked = self.action else: clicked = None role = '' else: role = '' clicked = self.clicked if self.role_parameter is not None: role = self.role_parameter if (role != self.role) or (clicked is not self.clicked): renpy.display.render.invalidate(self) self.role = role self.clicked = clicked if self.clicked is not None: self.set_style_prefix(self.role + "idle_", True) self.focusable = True else: self.set_style_prefix(self.role + "insensitive_", True) self.focusable = False super(Button, self).per_interact() def event(self, ev, x, y, st): if self.locked: return None def handle_click(action): renpy.exports.play(self.style.activate_sound) rv = run(action) if rv is not None: return rv else: raise renpy.display.core.IgnoreEvent() # Call self.action.periodic() timeout = run_periodic(self.action, st) if timeout is not None: renpy.game.interface.timeout(timeout) # If we have a child, try passing the event to it. (For keyboard # events, this only happens if we're focused.) if (not (ev.type in KEY_EVENTS)) or self.style.key_events: rv = super(Button, self).event(ev, x, y, st) if rv is not None: return rv if (self.keysym is not None) and (self.clicked is not None): if map_event(ev, self.keysym): return handle_click(self.clicked) if (self.alternate_keysym is not None) and (self.alternate is not None): if map_event(ev, self.alternate_keysym): return handle_click(self.alternate) # If not focused, ignore all events. if not self.is_focused(): return None # Check the keymap. for name, action in self.keymap.iteritems(): if map_event(ev, name): return run(action) # Handle the longpress event, if necessary. if (self.alternate is not None) and renpy.display.touch: if ev.type == pygame.MOUSEBUTTONDOWN and ev.button == 1: self.longpress_start = st self.longpress_x = x self.longpress_y = y renpy.game.interface.timeout(renpy.config.longpress_duration) if self.longpress_start is not None: if math.hypot(x - self.longpress_x, y - self.longpress_y) > renpy.config.longpress_radius: self.longpress_start = None elif st >= (self.longpress_start + renpy.config.longpress_duration): renpy.exports.vibrate(renpy.config.longpress_vibrate) renpy.display.interface.after_longpress() return handle_click(self.alternate) # Ignore as appropriate: if (self.clicked is not None) and map_event(ev, "button_ignore"): raise renpy.display.core.IgnoreEvent() if (self.alternate is not None) and map_event(ev, "button_alternate_ignore"): raise renpy.display.core.IgnoreEvent() # If clicked, if (self.clicked is not None) and map_event(ev, "button_select"): return handle_click(self.clicked) if (self.alternate is not None) and map_event(ev, "button_alternate"): return handle_click(self.alternate) return None def set_style_prefix(self, prefix, root): if root: super(Button, self).set_style_prefix(prefix, root) def _tts(self): return "" def _tts_all(self): rv = self._tts_common(alt(self.action)) if self.is_selected(): rv += " " + renpy.minstore.__("selected") return rv # Reimplementation of the TextButton widget as a Button and a Text # widget. def TextButton(text, style='button', text_style='button_text', clicked=None, **properties): text_properties, button_properties = renpy.easy.split_properties(properties, "text_", "") text = renpy.text.text.Text(text, style=text_style, **text_properties) # @UndefinedVariable return Button(text, style=style, clicked=clicked, **button_properties) class ImageButton(Button): """ Used to implement the guts of an image button. """ def __init__(self, idle_image, hover_image=None, insensitive_image=None, activate_image=None, selected_idle_image=None, selected_hover_image=None, selected_insensitive_image=None, selected_activate_image=None, style='image_button', clicked=None, hovered=None, **properties): hover_image = hover_image or idle_image insensitive_image = insensitive_image or idle_image activate_image = activate_image or hover_image selected_idle_image = selected_idle_image or idle_image selected_hover_image = selected_hover_image or hover_image selected_insensitive_image = selected_insensitive_image or insensitive_image selected_activate_image = selected_activate_image or activate_image self.state_children = dict( idle_=renpy.easy.displayable(idle_image), hover_=renpy.easy.displayable(hover_image), insensitive_=renpy.easy.displayable(insensitive_image), activate_=renpy.easy.displayable(activate_image), selected_idle_=renpy.easy.displayable(selected_idle_image), selected_hover_=renpy.easy.displayable(selected_hover_image), selected_insensitive_=renpy.easy.displayable(selected_insensitive_image), selected_activate_=renpy.easy.displayable(selected_activate_image), ) super(ImageButton, self).__init__(None, style=style, clicked=clicked, hovered=hovered, **properties) def visit(self): return self.state_children.values() def get_child(self): return self.style.child or self.state_children[self.style.prefix] # This is used for an input that takes its focus from a button. class HoveredProxy(object): def __init__(self, a, b): self.a = a self.b = b def __call__(self): self.a() if self.b: return self.b() # The currently editable input value. current_input_value = None # Is the current input value active? input_value_active = False # The default input value to use if the currently editable value doesn't # exist. default_input_value = None # A list of input values that exist. input_values = [ ] # A list of inputs that exist in the current interaction. inputs = [ ] def input_pre_per_interact(): global input_values global inputs global default_input_value input_values = [ ] inputs = [ ] default_input_value = None def input_post_per_interact(): global current_input_value global input_value_active for i in input_values: if i is current_input_value: break else: current_input_value = default_input_value input_value_active = True for i in inputs: editable = (i.value is current_input_value) and input_value_active and i.value.editable content = i.value.get_text() if (i.editable != editable) or (content != i.content): i.update_text(content, editable) i.caret_pos = len(content) class Input(renpy.text.text.Text): # @UndefinedVariable """ This is a Displayable that takes text as input. """ changed = None prefix = "" suffix = "" caret_pos = 0 old_caret_pos = 0 pixel_width = None default = u"" edit_text = u"" value = None shown = False def __init__(self, default="", length=None, style='input', allow=None, exclude=None, prefix="", suffix="", changed=None, button=None, replaces=None, editable=True, pixel_width=None, value=None, copypaste=False, **properties): super(Input, self).__init__("", style=style, replaces=replaces, substitute=False, **properties) if value: self.value = value changed = value.set_text default = value.get_text() self.default = unicode(default) self.content = self.default self.length = length self.allow = allow self.exclude = exclude self.prefix = prefix self.suffix = suffix self.copypaste = copypaste self.changed = changed self.editable = editable self.pixel_width = pixel_width caretprops = { 'color' : None } for i in properties: if i.endswith("color"): caretprops[i] = properties[i] self.caret = renpy.display.image.Solid(xmaximum=1, style=style, **caretprops) self.caret_pos = len(self.content) self.old_caret_pos = self.caret_pos if button: self.editable = False button.hovered = HoveredProxy(self.enable, button.hovered) button.unhovered = HoveredProxy(self.disable, button.unhovered) if isinstance(replaces, Input): self.content = replaces.content self.editable = replaces.editable self.caret_pos = replaces.caret_pos self.shown = replaces.shown self.update_text(self.content, self.editable) def update_text(self, new_content, editable, check_size=False): edit = renpy.display.interface.text_editing old_content = self.content if new_content != self.content or editable != self.editable or edit: renpy.display.render.redraw(self, 0) self.editable = editable # Choose the caret. caret = self.style.caret if caret is None: caret = self.caret # Format text being edited by the IME. if edit: self.edit_text = edit.text edit_text_0 = edit.text[:edit.start] edit_text_1 = edit.text[edit.start:edit.start + edit.length] edit_text_2 = edit.text[edit.start + edit.length:] edit_text = "" if edit_text_0: edit_text += "{u=1}" + edit_text_0.replace("{", "{{") + "{/u}" if edit_text_1: edit_text += "{u=2}" + edit_text_1.replace("{", "{{") + "{/u}" if edit_text_2: edit_text += "{u=1}" + edit_text_2.replace("{", "{{") + "{/u}" else: self.edit_text = "" edit_text = "" def set_content(content): if content == "": content = u"\u200b" if editable: l = len(content) self.set_text([self.prefix, content[0:self.caret_pos].replace("{", "{{"), edit_text, caret, content[self.caret_pos:l].replace("{", "{{"), self.suffix]) else: self.set_text([self.prefix, content.replace("{", "{{"), self.suffix ]) set_content(new_content) if check_size and self.pixel_width: w, _h = self.size() if w > self.pixel_width: self.caret_pos = self.old_caret_pos set_content(old_content) return if new_content != old_content: self.content = new_content if self.changed: self.changed(new_content) # This is needed to ensure the caret updates properly. def set_style_prefix(self, prefix, root): if prefix != self.style.prefix: self.update_text(self.content, self.editable) super(Input, self).set_style_prefix(prefix, root) def enable(self): self.update_text(self.content, True) def disable(self): self.update_text(self.content, False) def per_interact(self): global default_input_value if self.value is not None: inputs.append(self) input_values.append(self.value) if self.value.default and (default_input_value is None): default_input_value = self.value if not self.shown: if self.value is not None: default = self.value.get_text() self.default = unicode(default) self.content = self.default self.caret_pos = len(self.content) self.update_text(self.content, self.editable) self.shown = True def event(self, ev, x, y, st): self.old_caret_pos = self.caret_pos if not self.editable: return None if (ev.type == pygame.KEYDOWN) and (pygame.key.get_mods() & pygame.KMOD_LALT) and (not ev.unicode): return None l = len(self.content) raw_text = None if map_event(ev, "input_backspace"): if self.content and self.caret_pos > 0: content = self.content[0:self.caret_pos-1] + self.content[self.caret_pos:l] self.caret_pos -= 1 self.update_text(content, self.editable) renpy.display.render.redraw(self, 0) raise renpy.display.core.IgnoreEvent() elif map_event(ev, "input_enter"): content = self.content if self.edit_text: content = content[0:self.caret_pos] + self.edit_text + self.content[self.caret_pos:] if self.value: return self.value.enter() if not self.changed: return content elif map_event(ev, "input_left"): if self.caret_pos > 0: self.caret_pos -= 1 self.update_text(self.content, self.editable) renpy.display.render.redraw(self, 0) raise renpy.display.core.IgnoreEvent() elif map_event(ev, "input_right"): if self.caret_pos < l: self.caret_pos += 1 self.update_text(self.content, self.editable) renpy.display.render.redraw(self, 0) raise renpy.display.core.IgnoreEvent() elif map_event(ev, "input_delete"): if self.caret_pos < l: content = self.content[0:self.caret_pos] + self.content[self.caret_pos+1:l] self.update_text(content, self.editable) renpy.display.render.redraw(self, 0) raise renpy.display.core.IgnoreEvent() elif map_event(ev, "input_home"): self.caret_pos = 0 self.update_text(self.content, self.editable) renpy.display.render.redraw(self, 0) raise renpy.display.core.IgnoreEvent() elif map_event(ev, "input_end"): self.caret_pos = l self.update_text(self.content, self.editable) renpy.display.render.redraw(self, 0) raise renpy.display.core.IgnoreEvent() elif self.copypaste and map_event(ev, "input_copy"): pygame.scrap.put(pygame.scrap.SCRAP_TEXT, self.content) raise renpy.display.core.IgnoreEvent() elif self.copypaste and map_event(ev, "input_paste"): text = pygame.scrap.get(pygame.scrap.SCRAP_TEXT) raw_text = "" for c in text: if ord(c) >= 32: raw_text += c elif ev.type == pygame.TEXTEDITING: self.update_text(self.content, self.editable, check_size=True) raise renpy.display.core.IgnoreEvent() elif ev.type == pygame.TEXTINPUT: self.edit_text = "" raw_text = ev.text elif ev.type == pygame.KEYDOWN: if ev.unicode and ord(ev.unicode[0]) >= 32: raw_text = ev.unicode elif renpy.display.interface.text_event_in_queue(): raise renpy.display.core.IgnoreEvent() elif (32 <= ev.key < 127) and not (ev.mod & (pygame.KMOD_ALT | pygame.KMOD_META)): # Ignore printable keycodes without unicode. raise renpy.display.core.IgnoreEvent() if raw_text is not None: text = "" for c in raw_text: if self.allow and c not in self.allow: continue if self.exclude and c in self.exclude: continue text += c if self.length: remaining = self.length - len(self.content) text = text[:remaining] if text: content = self.content[0:self.caret_pos] + text + self.content[self.caret_pos:l] self.caret_pos += len(text) self.update_text(content, self.editable, check_size=True) raise renpy.display.core.IgnoreEvent() def render(self, width, height, st, at): rv = super(Input, self).render(width, height, st, at) if self.editable: rv.text_input = True return rv # A map from adjustment to lists of displayables that want to be redrawn # if the adjustment changes. adj_registered = { } # This class contains information about an adjustment that can change the # position of content. class Adjustment(renpy.object.Object): """ :doc: ui :name: ui.adjustment class Adjustment objects represent a value that can be adjusted by a bar or viewport. They contain information about the value, the range of the value, and how to adjust the value in small steps and large pages. """ force_step = False def __init__(self, range=1, value=0, step=None, page=None, changed=None, adjustable=None, ranged=None, force_step=False): # @ReservedAssignment """ The following parameters correspond to fields or properties on the adjustment object: `range` The range of the adjustment, a number. `value` The value of the adjustment, a number. `step` The step size of the adjustment, a number. If None, then defaults to 1/10th of a page, if set. Otherwise, defaults to the 1/20th of the range. This is used when scrolling a viewport with the mouse wheel. `page` The page size of the adjustment. If None, this is set automatically by a viewport. If never set, defaults to 1/10th of the range. It's can be used when clicking on a scrollbar. The following parameters control the behavior of the adjustment. `adjustable` If True, this adjustment can be changed by a bar. If False, it can't. It defaults to being adjustable if a `changed` function is given or if the adjustment is associated with a viewport, and not adjustable otherwise. `changed` This function is called with the new value when the value of the adjustment changes. `ranged` This function is called with the adjustment object when the range of the adjustment is set by a viewport. `force_step` If True and this adjustment changes by dragging associated viewport or a bar, value will be changed only if the drag reached next step. If "release" and this adjustment changes by dragging associated viewport or a bar, after the release, value will be rounded to the nearest step. If False, this adjustment will changes by dragging, ignoring the step value. .. method:: change(value) Changes the value of the adjustment to `value`, updating any bars and viewports that use the adjustment. """ super(Adjustment, self).__init__() if adjustable is None: if changed: adjustable = True self._value = value self._range = range self._page = page self._step = step self.changed = changed self.adjustable = adjustable self.ranged = ranged self.force_step = force_step def round_value(self, value, release): # Prevent deadlock border points if value <= 0: return 0 elif value >= self._range: return self._range if self.force_step is False: return value if (not release) and self.force_step == "release": return value return type(self.value)(self.step * round(float(value) / self.step)) def get_value(self): if self._value <= 0: return 0 if self._value >= self._range: return self._range return self._value def set_value(self, v): self._value = v value = property(get_value, set_value) def get_range(self): return self._range def set_range(self, v): self._range = v if self.ranged: self.ranged(self) range = property(get_range, set_range) # @ReservedAssignment def get_page(self): if self._page is not None: return self._page return self._range / 10 def set_page(self, v): self._page = v page = property(get_page, set_page) def get_step(self): if self._step is not None: return self._step if self._page is not None and self.page > 0: return self._page / 10 if isinstance(self._range, float): return self._range / 10 else: return 1 def set_step(self, v): self._step = v step = property(get_step, set_step) # Register a displayable to be redrawn when this adjustment changes. def register(self, d): adj_registered.setdefault(self, [ ]).append(d) def change(self, value): if value < 0: value = 0 if value > self._range: value = self._range if value != self._value: self._value = value for d in adj_registered.setdefault(self, [ ]): renpy.display.render.redraw(d, 0) if self.changed: return self.changed(value) return None def update(self): """ Updates things that depend on this adjustment without firing the changed handler. """ for d in adj_registered.setdefault(self, [ ]): renpy.display.render.invalidate(d) class Bar(renpy.display.core.Displayable): """ Implements a bar that can display an integer value, and respond to clicks on that value. """ __version__ = 2 def after_upgrade(self, version): if version < 1: self.adjustment = Adjustment(self.range, self.value, changed=self.changed) # E1101 self.adjustment.register(self) del self.range # E1101 del self.value # E1101 del self.changed # E1101 if version < 2: self.value = None def __init__(self, range=None, # @ReservedAssignment value=None, width=None, height=None, changed=None, adjustment=None, step=None, page=None, bar=None, style=None, vertical=False, replaces=None, hovered=None, unhovered=None, **properties): self.value = None if adjustment is None: if isinstance(value, renpy.ui.BarValue): if isinstance(replaces, Bar): value.replaces(replaces.value) self.value = value adjustment = value.get_adjustment() renpy.game.interface.timeout(0) tooltip = value.get_tooltip() if tooltip is not None: properties.setdefault("tooltip", tooltip) else: adjustment = Adjustment(range, value, step=step, page=page, changed=changed) if style is None: if self.value is not None: if vertical: style = self.value.get_style()[1] else: style = self.value.get_style()[0] else: if vertical: style = 'vbar' else: style = 'bar' if width is not None: properties['xmaximum'] = width if height is not None: properties['ymaximum'] = height super(Bar, self).__init__(style=style, **properties) self.adjustment = adjustment self.focusable = True # These are set when we are first rendered. self.thumb_dim = 0 self.height = 0 self.width = 0 self.hidden = False self.hovered = hovered self.unhovered = unhovered def per_interact(self): if self.value is not None: adjustment = self.value.get_adjustment() if adjustment.value != self.value: renpy.display.render.invalidate(self) self.adjustment = adjustment self.focusable = self.adjustment.adjustable self.adjustment.register(self) def visit(self): rv = [ ] self.style._visit_bar(rv.append) return rv def render(self, width, height, st, at): # Handle redrawing. if self.value is not None: redraw = self.value.periodic(st) if redraw is not None: renpy.display.render.redraw(self, redraw) xminimum = self.style.xminimum yminimum = self.style.yminimum if xminimum is not None: width = max(width, xminimum) height = max(height, yminimum) # Store the width and height for the event function to use. self.width = width self.height = height range = self.adjustment.range # @ReservedAssignment value = self.adjustment.value page = self.adjustment.page if range <= 0: if self.style.unscrollable == "hide": self.hidden = True return renpy.display.render.Render(width, height) elif self.style.unscrollable == "insensitive": self.set_style_prefix("insensitive_", True) self.hidden = False if self.style.bar_invert ^ self.style.bar_vertical: value = range - value bar_vertical = self.style.bar_vertical if bar_vertical: dimension = height else: dimension = width fore_gutter = self.style.fore_gutter aft_gutter = self.style.aft_gutter active = dimension - fore_gutter - aft_gutter if range: thumb_dim = active * page / (range + page) else: thumb_dim = active thumb_offset = abs(self.style.thumb_offset) if bar_vertical: thumb = render(self.style.thumb, width, thumb_dim, st, at) thumb_shadow = render(self.style.thumb_shadow, width, thumb_dim, st, at) thumb_dim = thumb.height else: thumb = render(self.style.thumb, thumb_dim, height, st, at) thumb_shadow = render(self.style.thumb_shadow, thumb_dim, height, st, at) thumb_dim = thumb.width # Remove the offset from the thumb. thumb_dim -= thumb_offset * 2 self.thumb_dim = thumb_dim active -= thumb_dim if range: fore_size = active * value / range else: fore_size = active fore_size = int(fore_size) aft_size = active - fore_size fore_size += fore_gutter aft_size += aft_gutter rv = renpy.display.render.Render(width, height) if bar_vertical: if self.style.bar_resizing: foresurf = render(self.style.fore_bar, width, fore_size, st, at) aftsurf = render(self.style.aft_bar, width, aft_size, st, at) rv.blit(thumb_shadow, (0, fore_size - thumb_offset)) rv.blit(foresurf, (0, 0), main=False) rv.blit(aftsurf, (0, height-aft_size), main=False) rv.blit(thumb, (0, fore_size - thumb_offset)) else: foresurf = render(self.style.fore_bar, width, height, st, at) aftsurf = render(self.style.aft_bar, width, height, st, at) rv.blit(thumb_shadow, (0, fore_size - thumb_offset)) rv.blit(foresurf.subsurface((0, 0, width, fore_size)), (0, 0), main=False) rv.blit(aftsurf.subsurface((0, height - aft_size, width, aft_size)), (0, height - aft_size), main=False) rv.blit(thumb, (0, fore_size - thumb_offset)) else: if self.style.bar_resizing: foresurf = render(self.style.fore_bar, fore_size, height, st, at) aftsurf = render(self.style.aft_bar, aft_size, height, st, at) rv.blit(thumb_shadow, (fore_size - thumb_offset, 0)) rv.blit(foresurf, (0, 0), main=False) rv.blit(aftsurf, (width-aft_size, 0), main=False) rv.blit(thumb, (fore_size - thumb_offset, 0)) else: foresurf = render(self.style.fore_bar, width, height, st, at) aftsurf = render(self.style.aft_bar, width, height, st, at) rv.blit(thumb_shadow, (fore_size - thumb_offset, 0)) rv.blit(foresurf.subsurface((0, 0, fore_size, height)), (0, 0), main=False) rv.blit(aftsurf.subsurface((width - aft_size, 0, aft_size, height)), (width-aft_size, 0), main=False) rv.blit(thumb, (fore_size - thumb_offset, 0)) if self.focusable: rv.add_focus(self, None, 0, 0, width, height) return rv def focus(self, default=False): super(Bar, self).focus(default) self.set_transform_event("hover") if not default: run(self.hovered) def unfocus(self, default=False): super(Bar, self).unfocus() self.set_transform_event("idle") if not default: run_unhovered(self.hovered) run(self.unhovered) def event(self, ev, x, y, st): if not self.focusable: return None if not self.is_focused(): return None if self.hidden: return None range = self.adjustment.range # @ReservedAssignment old_value = self.adjustment.value value = old_value vertical = self.style.bar_vertical invert = self.style.bar_invert ^ vertical if invert: value = range - value grabbed = (renpy.display.focus.get_grab() is self) just_grabbed = False ignore_event = False if not grabbed and map_event(ev, "bar_activate"): renpy.display.tts.speak(renpy.minstore.__("activate")) renpy.display.focus.set_grab(self) self.set_style_prefix("selected_hover_", True) just_grabbed = True grabbed = True ignore_event = True if grabbed: if vertical: increase = "bar_down" decrease = "bar_up" else: increase = "bar_right" decrease = "bar_left" if map_event(ev, decrease): renpy.display.tts.speak(renpy.minstore.__("decrease")) value -= self.adjustment.step ignore_event = True if map_event(ev, increase): renpy.display.tts.speak(renpy.minstore.__("increase")) value += self.adjustment.step ignore_event = True if ev.type in (pygame.MOUSEMOTION, pygame.MOUSEBUTTONUP, pygame.MOUSEBUTTONDOWN): if vertical: tgutter = self.style.fore_gutter bgutter = self.style.aft_gutter zone_height = self.height - tgutter - bgutter - self.thumb_dim if zone_height: value = (y - tgutter - self.thumb_dim / 2) * range / zone_height else: value = 0 else: lgutter = self.style.fore_gutter rgutter = self.style.aft_gutter zone_width = self.width - lgutter - rgutter - self.thumb_dim if zone_width: value = (x - lgutter - self.thumb_dim / 2) * range / zone_width else: value = 0 ignore_event = True if isinstance(range, int): value = int(value) if value < 0: renpy.display.tts.speak("") value = 0 if value > range: renpy.display.tts.speak("") value = range if invert: value = range - value if grabbed and not just_grabbed and map_event(ev, "bar_deactivate"): renpy.display.tts.speak(renpy.minstore.__("deactivate")) self.set_style_prefix("hover_", True) renpy.display.focus.set_grab(None) # Invoke rounding adjustment on bar release value = self.adjustment.round_value(value, release=True) if value != old_value: rv = self.adjustment.change(value) if rv is not None: return rv raise renpy.display.core.IgnoreEvent() if value != old_value: value = self.adjustment.round_value(value, release=False) rv = self.adjustment.change(value) if rv is not None: return rv if ignore_event: raise renpy.display.core.IgnoreEvent() else: return None def set_style_prefix(self, prefix, root): if root: super(Bar, self).set_style_prefix(prefix, root) def _tts(self): return "" def _tts_all(self): if self.value is not None: alt = self.value.alt else: alt = "" return self._tts_common(alt) + renpy.minstore.__("bar") class Conditional(renpy.display.layout.Container): """ This class renders its child if and only if the condition is true. Otherwise, it renders nothing. (Well, a Null). Warning: the condition MUST NOT update the game state in any way, as that would break rollback. """ def __init__(self, condition, *args, **properties): super(Conditional, self).__init__(*args, **properties) self.condition = condition self.null = renpy.display.layout.Null() self.state = eval(self.condition, vars(renpy.store)) def render(self, width, height, st, at): if self.state: return render(self.child, width, height, st, at) else: return render(self.null, width, height, st, at) def event(self, ev, x, y, st): state = eval(self.condition, vars(renpy.store)) if state != self.state: renpy.display.render.redraw(self, 0) self.state = state if state: return self.child.event(ev, x, y, st) class TimerState(renpy.python.RevertableObject): """ Stores the state of the timer, which may need to be rolled back. """ # Prevents us from having to worry about our initialization being # rolled back. started = False next_event = None class Timer(renpy.display.layout.Null): __version__ = 1 started = False def after_upgrade(self, version): if version < 1: self.state = TimerState() self.state.started = self.started self.state.next_event = self.next_event def __init__(self, delay, action=None, repeat=False, args=(), kwargs={}, replaces=None, **properties): super(Timer, self).__init__(**properties) if action is None: raise Exception("A timer must have an action supplied.") if delay <= 0: raise Exception("A timer's delay must be > 0.") # The delay. self.delay = delay # Should we repeat the event? self.repeat = repeat # The time the next event should occur. self.next_event = None # The function and its arguments. self.function = action self.args = args self.kwargs = kwargs # Did we start the timer? self.started = False if replaces is not None: self.state = replaces.state else: self.state = TimerState() def event(self, ev, x, y, st): state = self.state if not state.started: state.started = True state.next_event = st + self.delay if state.next_event is None: return if st < state.next_event: renpy.game.interface.timeout(state.next_event - st) return if not self.repeat: state.next_event = None else: state.next_event = state.next_event + self.delay if state.next_event < st: state.next_event = st + self.delay renpy.game.interface.timeout(state.next_event - st) return run(self.function, *self.args, **self.kwargs) class MouseArea(renpy.display.core.Displayable): # The offset between st and at. at_st_offset = 0 def __init__(self, hovered=None, unhovered=None, replaces=None, **properties): super(MouseArea, self).__init__(**properties) self.hovered = hovered self.unhovered = unhovered # Are we hovered right now? self.is_hovered = False if replaces is not None: self.is_hovered = replaces.is_hovered # Taken from the render. self.width = 0 self.height = 0 def render(self, width, height, st, at): self.width = width self.height = height self.at_st_offset = at - st return Render(width, height) def event(self, ev, x, y, st): # Mouseareas should not handle events when something else is grabbing. if renpy.display.focus.get_grab(): return if self.style.focus_mask is not None: crend = renpy.display.render.render(self.style.focus_mask, self.width, self.height, st, self.at_st_offset + st) is_hovered = crend.is_pixel_opaque(x, y) elif 0 <= x < self.width and 0 <= y < self.height: is_hovered = True else: is_hovered = False if is_hovered and not self.is_hovered: self.is_hovered = True return run(self.hovered) elif not is_hovered and self.is_hovered: self.is_hovered = False run_unhovered(self.hovered) run(self.unhovered) class OnEvent(renpy.display.core.Displayable): """ This is a displayable that runs an action in response to a transform event. It's used to implement the screen language on statement. """ def __init__(self, event, action=[ ]): """ `event` A string giving the event name. `action` An action or list of actions that are run when the event occurs. """ super(OnEvent, self).__init__() self.event_name = event self.action = action def is_event(self, event): if isinstance(self.event_name, basestring): return self.event_name == event else: return event in self.event_name def _handles_event(self, event): if self.is_event(event): return True else: return False def set_transform_event(self, event): if self.is_event(event): run(self.action) def render(self, width, height, st, at): return renpy.display.render.Render(0, 0)