diff options
Diffstat (limited to 'unrpyc/renpy/display/behavior.py')
-rw-r--r-- | unrpyc/renpy/display/behavior.py | 1531 |
1 files changed, 1531 insertions, 0 deletions
diff --git a/unrpyc/renpy/display/behavior.py b/unrpyc/renpy/display/behavior.py new file mode 100644 index 0000000..02eccf2 --- /dev/null +++ b/unrpyc/renpy/display/behavior.py @@ -0,0 +1,1531 @@ +# Copyright 2004-2013 Tom Rothamel <pytom@bishoujo.us> +# +# 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. + + +import renpy.display +import renpy.audio + +from renpy.display.render import render, Render + +import pygame + +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 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. + if part[0] == "joy": + if keydown: + return "(ev.type == %d and ev.press and ev.press == renpy.game.preferences.joymap.get(%r, None))" % (renpy.display.core.JOYEVENT, key) + else: + return "(ev.type == %d and ev.release and ev.release == renpy.game.preferences.joymap.get(%r, None))" % (renpy.display.core.JOYEVENT, key) + + # Otherwise, deal with it as a key. + if keydown: + rv = "(ev.type == %d" % pygame.KEYDOWN + else: + rv = "(ev.type == %d" % pygame.KEYUP + + if part[0] == "alt": + part.pop(0) + rv += " and (ev.mod & %d)" % pygame.KMOD_ALT + else: + rv += " and not (ev.mod & %d)" % pygame.KMOD_ALT + + if part[0] == "meta": + part.pop(0) + rv += " and (ev.mod & %d)" % pygame.KMOD_META + else: + rv += " and not (ev.mod & %d)" % pygame.KMOD_META + + if part[0] == "shift": + part.pop(0) + rv += " and (ev.mod & %d)" % pygame.KMOD_SHIFT + + if part[0] == "noshift": + part.pop(0) + 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)" + + key = "_".join(part) + + 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 map_event(ev, name): + """Returns true if the event matches the named keycode being pressed.""" + + check_code = event_cache.get(name, None) + if check_code is None: + check_code = eval("lambda ev : " + compile_event(name, True), globals()) + event_cache[name] = check_code + + return check_code(ev) + +def map_keyup(ev, name): + """Returns true if the event matches the named keycode being released.""" + + 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 map_event(ev, "skip"): + renpy.config.skipping = "slow" + renpy.exports.restart_interaction() + + if map_keyup(ev, "skip"): + 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(var, *args, **kwargs): + """ + Runs a variable. This is done by calling all the functions, and + iterating over the lists and tuples. + """ + + if var is None: + return None + + if isinstance(var, (list, tuple)): + rv = None + + for i in var: + new_rv = run(i, *args, **kwargs) + + if new_rv is not None: + rv = new_rv + + return rv + + return var(*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 is_selected(clicked): + + if isinstance(clicked, (list, tuple)): + return any(is_selected(i) for i in clicked) + + elif isinstance(clicked, renpy.ui.Action): + return clicked.get_selected() + else: + return False + + +def is_sensitive(clicked): + + if isinstance(clicked, (list, tuple)): + return all(is_sensitive(i) for i in clicked) + + elif isinstance(clicked, renpy.ui.Action): + return clicked.get_sensitive() + else: + return True + + +############################################################################## +# 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, **keymap): + super(Keymap, self).__init__(style='default') + self.keymap = keymap + + def event(self, ev, x, y, st): + + for name, action in self.keymap.items(): + if map_event(ev, name): + + 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.values(): + 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"): + renpy.game.interface.suppress_transition = True + renpy.game.after_rollback = True + renpy.game.log.rolled_forward = True + return 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. + """ + + def __init__(self, delay, result=False, **properties): + super(PauseBehavior, self).__init__(**properties) + + self.delay = delay + self.result = result + + def event(self, ev, x, y, st): + + if st >= self.delay: + + # 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 + + def __init__(self, default=True, afm=None, dismiss=[ 'dismiss' ], allow_dismiss=None, **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 set_afm_length(self, afm_length): + self.afm_length = max(afm_length, 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 renpy.game.preferences.text_cps: + afm_delay += 1.0 / renpy.game.preferences.text_cps * self.afm_length + + if st > afm_delay: + if renpy.config.afm_callback: + if renpy.config.afm_callback(): + return True + else: + renpy.game.interface.timeout(0.1) + else: + return True + else: + renpy.game.interface.timeout(afm_delay - st) + + for dismiss in self.dismiss: + + if map_event(ev, dismiss) and self.is_focused(): + + if renpy.config.skipping: + renpy.config.skipping = None + renpy.exports.restart_interaction() + raise renpy.display.core.IgnoreEvent() + + if renpy.game.preferences.using_afm_enable and renpy.game.preferences.afm_enable: + 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.allow_skipping and renpy.config.skipping: + + if 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.game.interface.timeout(skip_delay - st) + + + return None + + +############################################################################## +# Button + +class Button(renpy.display.layout.Window): + + keymap = { } + action = None + + def __init__(self, child=None, style='button', clicked=None, + hovered=None, unhovered=None, action=None, role=None, + time_policy=None, keymap={}, + **properties): + + super(Button, self).__init__(child, style=style, **properties) + + if isinstance(clicked, renpy.ui.Action): + action = clicked + + if action is not None: + clicked = action + + if not is_sensitive(action): + clicked = None + + if role is None: + if action: + if is_selected(action): + role = 'selected_' + else: + role = '' + else: + role = '' + + self.action = action + self.activated = False + self.clicked = clicked + self.hovered = hovered + self.unhovered = unhovered + self.focusable = clicked is not None + self.role = role + self.keymap = keymap + + self.time_policy_data = None + + def predict_one_action(self): + predict_action(self.clicked) + predict_action(self.hovered) + predict_action(self.unhovered) + + if self.keymap: + for v in self.keymap.values(): + 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: + mask = renpy.easy.displayable(mask) + mask = renpy.display.render.render(mask, rv.width, rv.height, st, at) + + 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) + + if self.activated: + return None + + rv = None + + if not default: + rv = run(self.hovered) + + self.set_transform_event(self.role + "hover") + self.child.set_transform_event(self.role + "hover") + + return rv + + + def unfocus(self, default=False): + super(Button, self).unfocus(default) + + if self.activated: + return None + + if not default: + run_unhovered(self.hovered) + run(self.unhovered) + + self.set_transform_event(self.role + "idle") + self.child.set_transform_event(self.role + "idle") + + + def per_interact(self): + if not self.clicked: + self.set_style_prefix(self.role + "insensitive_", True) + else: + self.set_style_prefix(self.role + "idle_", True) + + super(Button, self).per_interact() + + def event(self, ev, x, y, st): + + # 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 self.is_focused() or not (ev.type == pygame.KEYDOWN or ev.type == pygame.KEYUP): + rv = super(Button, self).event(ev, x, y, st) + if rv is not None: + return rv + + # If not focused, ignore all events. + if not self.is_focused(): + return None + + # Check the keymap. + for name, action in self.keymap.items(): + if map_event(ev, name): + return run(action) + + # Ignore as appropriate: + if map_event(ev, "button_ignore") and self.clicked: + raise renpy.display.core.IgnoreEvent() + + # If clicked, + if map_event(ev, "button_select") and self.clicked: + + self.activated = True + self.style.set_prefix(self.role + 'activate_') + + if self.style.sound: + renpy.audio.music.play(self.style.sound, channel="sound") + + rv = run(self.clicked) + + if rv is not None: + return rv + else: + self.activated = False + + if self.is_focused(): + self.set_style_prefix(self.role + "hover_", True) + else: + self.set_style_prefix(self.role + "idle_", True) + + raise renpy.display.core.IgnoreEvent() + + return None + + + def set_style_prefix(self, prefix, root): + if root: + super(Button, self).set_style_prefix(prefix, root) + + +# 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 = renpy.text.text.Text(text, style=text_style) #@UndefinedVariable + return Button(text, style=style, clicked=clicked, **properties) + +class ImageButton(Button): + """ + Used to implement the guts of an image button. + """ + + def __init__(self, + idle_image, + hover_image, + 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): + + 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__(renpy.display.layout.Null(), + style=style, + clicked=clicked, + hovered=hovered, + **properties) + + def visit(self): + return list(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() + + +class Input(renpy.text.text.Text): #@UndefinedVariable + """ + This is a Displayable that takes text as input. + """ + + changed = None + prefix = "" + suffix = "" + caret_pos = 0 + + def __init__(self, + default="", + length=None, + style='input', + allow=None, + exclude=None, + prefix="", + suffix="", + changed=None, + button=None, + replaces=None, + editable=True, + **properties): + + super(Input, self).__init__("", style=style, replaces=replaces, substitute=False, **properties) + + self.content = str(default) + self.length = length + + self.allow = allow + self.exclude = exclude + self.prefix = prefix + self.suffix = suffix + + self.changed = changed + + self.editable = editable + + 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) + + 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.update_text(self.content, self.editable) + + + def update_text(self, content, editable): + + if content != self.content or editable != self.editable: + renpy.display.render.redraw(self, 0) + + if content != self.content: + self.content = content + + if self.changed: + self.changed(content) + + if content == "": + content = "\u200b" + + self.editable = editable + + # Choose the caret. + caret = self.style.caret + if caret is None: + caret = self.caret + + if editable: + l = len(content) + self.set_text([self.prefix, content[0:self.caret_pos].replace("{", "{{"), caret, + content[self.caret_pos:l].replace("{", "{{"), self.suffix]) + else: + self.set_text([self.prefix, content.replace("{", "{{"), self.suffix ]) + + # 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 event(self, ev, x, y, st): + + if not self.editable: + return None + + l = len(self.content) + + 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"): + if not self.changed: + return self.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 ev.type == pygame.KEYDOWN and ev.str: + if ord(ev.str[0]) < 32: + return None + + if self.length and len(self.content) >= self.length: + raise renpy.display.core.IgnoreEvent() + + if self.allow and ev.str not in self.allow: + raise renpy.display.core.IgnoreEvent() + + if self.exclude and ev.str in self.exclude: + raise renpy.display.core.IgnoreEvent() + + content = self.content[0:self.caret_pos] + ev.str + self.content[self.caret_pos:l] + self.caret_pos += 1 + + self.update_text(content, self.editable) + + raise renpy.display.core.IgnoreEvent() + +# 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. + + + """ + + def __init__(self, range=1, value=0, step=None, page=0, changed=None, adjustable=None, ranged=None): #@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. + + .. 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 + + def get_value(self): + 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 + +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) + 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): + self.focusable = self.adjustment.adjustable + self.adjustment.register(self) + + def predict_one(self): + pd = renpy.display.predict.displayable + style = self.style + + pd(style.insensitive_fore_bar) + pd(style.idle_fore_bar) + pd(style.hover_fore_bar) + pd(style.selected_idle_fore_bar) + pd(style.selected_hover_fore_bar) + + pd(style.insensitive_aft_bar) + pd(style.idle_aft_bar) + pd(style.hover_aft_bar) + pd(style.selected_idle_aft_bar) + pd(style.selected_hover_aft_bar) + + pd(style.insensitive_thumb) + pd(style.idle_thumb) + pd(style.hover_thumb) + pd(style.selected_idle_thumb) + pd(style.selected_hover_thumb) + + pd(style.insensitive_thumb_shadow) + pd(style.idle_thumb_shadow) + pd(style.hover_thumb_shadow) + pd(style.selected_idle_thumb_shadow) + pd(style.selected_hover_thumb_shadow) + + 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) + + # 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 + + if not grabbed and map_event(ev, "bar_activate"): + renpy.display.focus.set_grab(self) + just_grabbed = True + grabbed = True + + if grabbed: + + if vertical: + increase = "bar_down" + decrease = "bar_up" + else: + increase = "bar_right" + decrease = "bar_left" + + if map_event(ev, decrease): + value -= self.adjustment.step + + if map_event(ev, increase): + value += self.adjustment.step + + 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 + + if isinstance(range, int): + value = int(value) + + if value < 0: + value = 0 + + if value > range: + value = range + + if invert: + value = range - value + + if grabbed and not just_grabbed and map_event(ev, "bar_deactivate"): + renpy.display.focus.set_grab(None) + + if value != old_value: + return self.adjustment.change(value) + + return None + + +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): + + 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 + + return Render(width, height) + + def event(self, ev, x, y, st): + + if 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) + + |