diff options
Diffstat (limited to 'unrpyc/renpy/display/dragdrop.py')
-rw-r--r-- | unrpyc/renpy/display/dragdrop.py | 731 |
1 files changed, 731 insertions, 0 deletions
diff --git a/unrpyc/renpy/display/dragdrop.py b/unrpyc/renpy/display/dragdrop.py new file mode 100644 index 0000000..0b99e63 --- /dev/null +++ b/unrpyc/renpy/display/dragdrop.py @@ -0,0 +1,731 @@ +# 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. + +# TODO: Use overlap (rather than simple pointer location) to determine +# drag and drop. + +import renpy.display +from renpy.display.render import render, Render, redraw +from renpy.display.core import absolute +from renpy.display.behavior import map_event, run + +import pygame + +def default_drag_group(): + """ + Gets the default drag group. If it doesn't exist yet, creates it. + """ + + sls = renpy.game.context().scene_lists + + rv = sls.drag_group + + if rv is None: + rv = DragGroup() + sls.drag_group = rv + + return rv + +def default_drag_joined(drag): + return [ (drag, 0, 0) ] + +class Drag(renpy.display.core.Displayable, renpy.python.RevertableObject): + """ + :doc: drag_drop class + :args: (d=None, drag_name=None, draggable=True, droppable=True, drag_raise=True, dragged=None, dropped=None, drag_handle=(0.0, 0.0, 1.0, 1.0), drag_joined=..., clicked=None, hovered=None, unhovered=None, **properties) + + A displayable that represents an object that can be dragged around + its enclosing area. A Drag can also represent an area that + other Drags can be dropped on. + + A Drag can be moved around inside is parent. Generally, its parent + should be either a :func:`Fixed` or :class:`DragGroup`. + + A Drag has one child. The child's state reflects the status + of the drag and drop operation: + + * ``selected_hover`` - when it is being dragged. + * ``selected_idle`` - when it can be dropped on. + * ``hover`` - when the draggable will be dragged when the mouse is + clicked. + * ``idle`` - otherwise. + + The drag handle is a rectangle inside the child. The mouse must be over + a non-transparent pixel inside the drag handle for dragging or clicking + to occur. + + A newly-created draggable is added to the default DragGroup. A draggable + can only be in a single DragGroup - if it's added to a second group, + it's removed from the first. + + When a Drag is first rendered, if it's position cannot be determined + from the DragGroup it is in, the position of its upper-left corner + is computed using the standard layout algorithm. Once that position + + + `d` + If present, the child of this Drag. Drags use the child style + in preference to this, if it's not None. + + `drag_name` + If not None, the name of this draggable. This is available + as the `name` property of draggable objects. If a Drag + with the same name is or was in the DragGroup, the starting + position of this Drag is taken from that Draggable. + + `draggable` + If true, the Drag can be dragged around the screen with + the mouse. + + `droppable` + If true, other Drags can be dropped on this Drag. + + `drag_raise` + If true, this Drag is raised to the top when it is dragged. If + it is joined to other Drags, all joined drags are raised. + + `dragged` + A callback (or list of callbacks) that is called when the Drag + has been dragged. It is called with two arguments. The first is + a list of Drags that are being dragged. The second is either + a Drag that is being dropped onto, or None of a drop did not + occur. If the callback returns a value other than None, that + value is returned as the result of the interaction. + + `dropped` + A callback (or list of callbacks) that is called when this Drag + is dropped onto. It is called with two arguments. The first + is the Drag being dropped onto. The second is a list of Drags that + are being dragged. If the callback returns a value other than None, + that value is returned as the result of the interaction. + + When a dragged and dropped callback are triggered for the same + event, the dropped callback is only called if dragged returns + None. + + `clicked` + A callback this is called, with no arguments, when the Drag is + clicked without being moved. A droppable can also be focused + and clicked. If the callback returns a value othe than None, + that value is returned as the result of the interaction. + + `drag_handle` + A (x, y, width, height) tuple, giving the position of the drag + handle within the child. In this tuple, integers are considered + to be a literal number of pixels, while floats are relative to + the size of the child. + + `drag_joined` + This is called with the current Drag as an argument. It's + expected to return a list of [ (drag, x, y) ] tuples, giving + the draggables to drag as a unit. `x` and `y` are the offsets + of the drags relative to each other, they are not relative + to the corner of this drag. + + Except for `d`, all of the parameters are available as fields (with + the same name) on the Drag object. In addition, after the drag has + been rendered, the following fields become available: + + `x`, `y` + The position of the Drag relative to its parent, in pixels. + + `w`, `h` + The width and height of the Drag's child, in pixels. + """ + + def __init__(self, + d=None, + drag_name=None, + draggable=True, + droppable=True, + drag_raise=True, + dragged=None, + dropped=None, + drag_handle=(0.0, 0.0, 1.0, 1.0), + drag_joined=default_drag_joined, + clicked=None, + hovered=None, + unhovered=None, + replaces=None, + **properties): + + super(Drag, self).__init__(self, **properties) + + self.drag_name = drag_name + self.draggable = draggable + self.droppable = droppable + self.drag_raise = drag_raise + self.dragged = dragged + self.dropped = dropped + self.drag_handle = drag_handle + self.drag_joined = drag_joined + self.clicked = clicked + self.hovered = hovered + self.unhovered = unhovered + + self.child = None + + # Add us to a drag group on creation. + if drag_name: + self.drag_group = default_drag_group() + + # The current x and y coordinates of this displayable. + self.x = None + self.y = None + + # The width and height of the child. + self.w = None + self.h = None + + # The width and height of our parent. + self.parent_width = None + self.parent_height = None + + # The target x and y coordinates of this displayable. (The + # coordinates that we're snapping to.) + self.target_x = None + self.target_y = None + + # The offset from the location of the mouse to the "grab point", + # which is where the things that are being moved are offset from. + self.grab_x = None + self.grab_y = None + + # x and y from the last time we rendered. + self.last_x = None + self.last_y = None + + # The abs_x and abs_y from when we started the grab. + self.start_x = 0 + self.start_y = 0 + + # The last time we were shown, using the animation timebases. + self.at = 0 + + # The (animation timebase) time at which we should reach + # the target coordinates. + self.target_at = 0 + + # The displayable we were last dropping on. + self.last_drop = None + + # Did we move over the course of this drag? + self.drag_moved = False + + if replaces is not None: + self.x = replaces.x + self.y = replaces.y + self.at = replaces.at + self.target_x = replaces.target_x + self.target_y = replaces.target_y + self.target_at = replaces.target_at + + if d is not None: + self.add(d) + + + def snap(self, x, y, delay=0): + """ + :doc: drag_drop method + + Changes the position of the drag. If the drag is not showing, + then the position change is instantaneous. Otherwise, the + position change takes `delay` seconds, and is animated as a + linear move. + """ + + self.target_x = x + self.target_y = y + + if self.x is not None: + self.target_at = self.at + delay + else: + self.target_at = self.at + self.x = x + self.y = y + + redraw(self, 0) + + def set_style_prefix(self, prefix, root): + super(Drag, self).set_style_prefix(prefix, root) + + if self.child is not None: + self.child.set_style_prefix(prefix, False) + + def add(self, d): + if self.child is not None: + raise Exception("Drag expects either zero or one children.") + + self.child = renpy.easy.displayable(d) + + def set_child(self, d): + """ + :doc: drag_drop method + + Changes the child of this drag to `d`. + """ + + d.per_interact() + self.child = renpy.easy.displayable(d) + + def top(self): + """ + :doc: drag_drop method + + Raises this displayable to the top of its drag_group. + """ + + if self.drag_group is not None: + self.drag_group.raise_children([ self ]) + + def visit(self): + return [ self.child ] + + def focus(self, default=False): + super(Drag, self).focus(default) + + rv = None + + if not default: + rv = run(self.hovered) + + return rv + + def unfocus(self, default=False): + super(Drag, self).unfocus(default) + + if not default: + run(self.unhovered) + + def render(self, width, height, st, at): + + child = self.style.child + if child is None: + child = self.child + + self.parent_width = width + self.parent_height = height + + cr = render(child, width, height, st, at) + cw, ch = cr.get_size() + + rv = Render(cw, ch) + rv.blit(cr, (0, 0)) + + self.w = cw + self.h = ch + + # If we don't have a position, then look for it in a drag group. + if (self.x is None) and (self.drag_group is not None) and (self.drag_name is not None): + if self.drag_name in self.drag_group.positions: + self.x, self.y = self.drag_group.positions[self.drag_name] + + # If we don't have a position, run the placement code and use + # that to compute our placement. + if self.x is None: + self.x, self.y = self.place(None, 0, 0, width, height, rv) + self.x = int(self.x) + self.y = int(self.y) + + if self.target_x is None: + self.target_x = self.x + self.target_y = self.y + self.target_at = at + + # Determine if we need to do the snap animation. + if at >= self.target_at: + self.x = self.target_x + self.y = self.target_y + else: + done = (at - self.at) / (self.target_at - self.at) + self.x = absolute(self.x + done * (self.target_x - self.x)) + self.y = absolute(self.y + done * (self.target_y - self.y)) + redraw(self, 0) + + if self.draggable or self.clicked is not None: + + fx, fy, fw, fh = self.drag_handle + + if isinstance(fx, float): + fx = int(fx * cw) + + if isinstance(fy, float): + fy = int(fy * ch) + + if isinstance(fw, float): + fw = int(fw * cw) + + if isinstance(fh, float): + fh = int(fh * ch) + + rv.add_focus(self, None, fx, fy, fw, fh, fx, fy, cr.subsurface((fx, fy, fw, fh))) + + self.last_x = self.x + self.last_y = self.y + self.at = at + + return rv + + def event(self, ev, x, y, st): + + if not self.is_focused(): + return self.child.event(ev, x, y, st) + + # if not self.draggable: + # return self.child.event(ev, x, y, st) + + # Mouse, in parent-relative coordinates. + par_x = self.last_x + x + par_y = self.last_y + y + + grabbed = (renpy.display.focus.get_grab() is self) + + if grabbed: + joined_offsets = self.drag_joined(self) + joined = [ i[0] for i in joined_offsets ] + + elif self.draggable and map_event(ev, "drag_activate"): + + joined_offsets = self.drag_joined(self) + joined = [ i[0] for i in joined_offsets ] + + if not joined: + raise renpy.display.core.IgnoreEvent() + + renpy.display.focus.set_grab(self) + + self.grab_x = x + self.grab_y = y + + # If we're not the only thing we're joined with, we + # might need to adjust our grab point. + for i, xo, yo in joined_offsets: + if i is self: + self.grab_x += xo + self.grab_y += yo + break + + self.drag_moved = False + self.start_x = par_x + self.start_y = par_y + + grabbed = True + + # Handle clicking on droppables. + if not grabbed: + if self.clicked is not None and map_event(ev, "drag_deactivate"): + rv = run(self.clicked) + if rv is not None: + return rv + + raise renpy.display.core.IgnoreEvent() + + return self.child.event(ev, x, y, st) + + # Handle moves by moving things relative to the grab point. + if ev.type in (pygame.MOUSEMOTION, pygame.MOUSEBUTTONUP, pygame.MOUSEBUTTONDOWN): + + if not self.drag_moved and (self.start_x != par_x or self.start_y != par_y): + self.drag_moved = True + + # We may not be in the drag_joined group. + self.set_style_prefix("idle_", True) + + # Set the style. + for i in joined: + i.set_style_prefix("selected_hover_", True) + + # Raise the joined items. + if self.drag_raise and self.drag_group is not None: + self.drag_group.raise_children(joined) + + if self.drag_moved: + for i, xo, yo in joined_offsets: + + new_x = par_x - self.grab_x + xo + new_y = par_y - self.grab_y + yo + new_x = max(new_x, 0) + new_x = min(new_x, i.parent_width - i.w) + new_y = max(new_y, 0) + new_y = min(new_y, i.parent_height - i.h) + + if i.drag_group is not None and i.drag_name is not None: + i.drag_group.positions[i.drag_name] = (new_x, new_y) + + i.x = new_x + i.y = new_y + i.target_x = new_x + i.target_y = new_y + i.target_at = self.at + redraw(i, 0) + + if (self.drag_group is not None) and self.drag_moved: + drop = self.drag_group.get_best_drop(joined) + else: + drop = None + + if drop is not self.last_drop: + + if self.last_drop is not None: + self.last_drop.set_style_prefix("idle_", True) + + if drop is not None: + drop.set_style_prefix("selected_idle_", True) + + self.last_drop = drop + + if map_event(ev, 'drag_deactivate'): + renpy.display.focus.set_grab(None) + + if drop is not None: + drop.set_style_prefix("idle_", True) + + for i in joined: + i.set_style_prefix("idle_", True) + + self.set_style_prefix("hover_", True) + + self.grab_x = None + self.grab_y = None + self.last_drop = None + + if self.drag_moved: + + # Call the drag callback. + drag = joined[0] + if drag.dragged is not None: + rv = run(drag.dragged, joined, drop) + if rv is not None: + return rv + + # Call the drop callback. + if drop is not None and drop.dropped is not None: + rv = run(drop.dropped, drop, joined) + if rv is not None: + return rv + + else: + + # Call the clicked callback. + if self.clicked: + rv = run(self.clicked) + if rv is not None: + return rv + + raise renpy.display.core.IgnoreEvent() + + + def get_placement(self): + + if self.x is not None: + return self.x, self.y, 0, 0, 0, 0, True + else: + return super(Drag, self).get_placement() + + def per_interact(self): + self.set_style_prefix("idle_", True) + super(Drag, self).per_interact() + + +class DragGroup(renpy.display.layout.MultiBox): + """ + :doc: drag_drop class + + Represents a group of Drags. A Drag is limited to the boundary of + its DragGroup. Dropping only works between Drags that are in the + same DragGroup. Drags may only be raised when they are inside a + DragGroup. + + A DragGroup is laid out like a :func:`Fixed`. + + All positional parameters to the DragGroup constructor should be + Drags, that are added to the DragGroup. + """ + + _list_type = renpy.python.RevertableList + + def __init__(self, *children, **properties): + properties.setdefault("style", "fixed") + properties.setdefault("layout", "fixed") + + replaces = properties.pop("replaces", None) + + super(DragGroup, self).__init__(**properties) + + if replaces is not None: + self.positions = renpy.python.RevertableDict(replaces.positions) + self.sensitive = replaces.sensitive + else: + self.positions = renpy.python.RevertableDict() + self.sensitive = True + + for i in children: + self.add(i) + + + def add(self, child): + """ + :doc: drag_drop method + + Adds `child`, which must be a Drag, to this DragGroup. + """ + + if not isinstance(child, Drag): + raise Exception("Only drags can be added to a drag group.") + + child.drag_group = self + super(DragGroup, self).add(child) + + def remove(self, child): + """ + :doc: drag_drop method + + Removes `child` from this DragGroup. + """ + + + if not isinstance(child, Drag): + raise Exception("Only drags can be removed from a drag group.") + + child.x = None + super(DragGroup, self).remove(child) + + + def event(self, ev, x, y, st): + + if not self.sensitive: + return None + + return super(DragGroup, self).event(ev, x, y, st) + + def raise_children(self, l): + """ + Raises the children in `l` to the top of this drag_group, using the + order given in l for those children. + """ + + s = set(l) + + offset_map = { } + + children = [ ] + offsets = [ ] + + for i, c in enumerate(self.children): + if i < len(self.offsets): + o = self.offsets[i] + else: + o = (0, 0) + + if c not in s: + children.append(c) + offsets.append(o) + else: + offset_map[c] = o + + for c in l: + if c in offset_map: + children.append(c) + offsets.append(offset_map[c]) + + self.children = self._list_type(children) + self.offsets = self._list_type(offsets) + + + def get_best_drop(self, joined): + """ + Returns the droppable that the members of joined overlap the most. + """ + + max_overlap = 0 + rv = 0 + + joined_set = set(joined) + + for d in joined: + + r1 = (d.x, d.y, d.w, d.h) + + for c in self.children: + if c in joined_set: + continue + + if not c.droppable: + continue + + r2 = (c.x, c.y, c.w, c.h) + + overlap = rect_overlap_area(r1, r2) + + if overlap >= max_overlap: + rv = c + max_overlap = overlap + + if max_overlap <= 0: + return None + else: + return rv + + def get_children(self): + """ + Returns a list of Drags that are the children of + this DragGroup. + """ + + return renpy.python.RevertableList(self.children) + + def get_child_by_name(self, name): + """ + :doc: drag_drop method + + Returns the first child of this DragGroup that has a drag_name + of name. + """ + + for i in self.children: + if i.drag_name == name: + return i + + return None + + +def rect_overlap_area(r1, r2): + """ + Returns the number of pixels by which rectangles r1 and r2 overlap. + """ + + x1, y1, w1, h1 = r1 + x2, y2, w2, h2 = r2 + + maxleft = max(x1, x2) + minright = min(x1 + w1, x2 + w2) + maxtop = max(y1, y2) + minbottom = min(y1 + h1, y2 + h2) + + if minright < maxleft: + return 0 + + if minbottom < maxtop: + return 0 + + return (minright - maxleft) * (minbottom - maxtop) + + |