"""Implements drag and drop behaviour"""
import serge.actor
import serge.events
import serge.input
import serge.blocks.actors
[docs]class NotDragging(Exception): """No actor is being dragged"""
[docs]class DuplicateActor(Exception): """The actor is already controlled"""
[docs]class NotATarget(Exception): """The actor is not a target"""
[docs]class AlreadyATarget(Exception): """The actor is a target already"""
[docs]class DropNotAllowed(Exception): """Cannot drop here"""
[docs]class BadConstraint(Exception): """The constraint was invalid"""
[docs]class DragItem(object):
"""An item to be dragged"""
def __init__(self, obj, start, stop, x_constraint, y_constraint):
"""Initialise the object"""
self.obj = obj
self.start = start
self.stop = stop
self.x_constraint = x_constraint
self.y_constraint = y_constraint
[docs] def getXYValue(self, dx, dy):
"""Return the new x and y value honoring the constraints"""
if self.x_constraint:
nx = max(min(self.obj.x + dx, self.x_constraint[1]), self.x_constraint[0])
else:
nx = self.obj.x + dx
if self.y_constraint:
ny = max(min(self.obj.y + dy, self.y_constraint[1]), self.y_constraint[0])
else:
ny = self.obj.y + dy
#
return nx, ny
[docs]class DragController(serge.blocks.actors.ScreenActor):
"""Controls objects which are draggable"""
def __init__(self, tag='controller', name='controller', start=None, stop=None, hit=None, miss=None):
"""Initialise the controller"""
super(DragController, self).__init__(tag, name)
self.draggables = {}
self.targets = {}
self.dragging = self._last_dragged = None
self.drag_x = self.drag_y = 0.0
self.setCallbacks(start, stop)
self.setDropCallbacks(hit, miss)
[docs] def addActor(self, actor, start=None, stop=None, x_constraint=None, y_constraint=None):
"""Add an actor to be controlled and callback to be called when dragging start and stops"""
#
# Reality checks
if actor in self.draggables:
raise DuplicateActor('The actor %s is already controlled by %s' % (actor.getNiceName(), self.getNiceName()))
if x_constraint is not None and not isinstance(x_constraint, tuple):
raise BadConstraint('The x_constraint was not a tuple')
if y_constraint is not None and not isinstance(y_constraint, tuple):
raise BadConstraint('The y_constraint was not a tuple')
#
# Add the draggable
self.draggables[actor] = DragItem(actor, start, stop, x_constraint, y_constraint)
actor.linkEvent(serge.events.E_LEFT_MOUSE_DOWN, self.mouseDown, (actor, start))
actor.linkEvent(serge.events.E_LEFT_CLICK, self.clickedActor, (actor, stop))
[docs] def getDraggable(self, actor):
"""Return the draggable for the actor"""
try:
return self.draggables[actor]
except KeyError:
raise NotDragging('The actor %s is not tracked as a draggable' % actor.getNiceName())
[docs] def removeActor(self, actor):
"""Remove an actor from being controlled"""
del(self.draggables[actor])
actor.unlinkEvent(serge.events.E_LEFT_MOUSE_DOWN, self.mouseDown)
actor.unlinkEvent(serge.events.E_LEFT_CLICK, self.clickedActor)
[docs] def addDropTarget(self, actor, fn=None):
"""Add a target to drop to"""
if actor in self.targets.keys():
raise AlreadyATarget('The target %s is already a drop target for %s' % (actor.getNiceName(), self.getNiceName()))
else:
self.targets[actor] = fn
[docs] def removeDropTarget(self, actor):
"""Remove an actor as a drop target"""
try:
del(self.targets[actor])
except KeyError:
raise NotATarget('The actor %s was not a target in %s' % (actor.getNiceName(), self.getNiceName()))
[docs] def isDragging(self):
"""Return True if we are dragging an object"""
return self.dragging != None
[docs] def getDraggedActor(self):
"""Return the actor being dragged"""
if self.isDragging():
return self.dragging
else:
raise NotDragging('No actor is being dragged')
[docs] def mouseDown(self, obj, (actor, fn)):
"""The mouse was down over an actor"""
if self.active and not self.dragging:
self.dragging = actor
self.drag_x, self.drag_y = self.mouse.getScreenPos()
#
# We allow the callbacks to return an actor, which will be used
# as the dragged object
dragger = None
if fn:
dragger = fn(obj, actor)
if self._start:
dragger = dragger if dragger else self._start(obj, actor)
#
if dragger:
self.dragging = dragger
[docs] def clickedActor(self, obj, (actor, fn)):
"""The mouse was released over an actor"""
if self.active and self.dragging:
#
# Check to see where we are dropping
if self.checkForDrops(self.dragging):
#
# Dropping was allowed - so cancel drag and call any callbacks
if fn:
fn(obj, self.dragging)
if self._stop:
self._stop(obj, self.dragging)
self.dragging = None
else:
#
# The drop target would not allow us to be dropped
self.log.debug('Drop not allowed')
[docs] def checkForDrops(self, actor):
"""Check to see if we dropped our actor onto a target or not - return False if the drop is not allowed
If we dropped on a target then we can call the callback. If
we didn't drop on a target then we call the miss callback.
The callback can raise DropNotAllowed to cause the drop not to occur
"""
#
# Go through all the targets looking for the one we dropped on (use the mouse
# as the test point)
hit = False
allowed = True
test = serge.geometry.Point(*self.mouse.getScreenPos())
for target, fn in self.targets.iteritems():
if actor != target and test.isInside(target):
# Ok, dropped on this target
hit = True
for callback in (fn, self._hit):
if callback:
try:
callback(target, actor)
except DropNotAllowed:
allowed = False
#
# No targets were overlapped - so call the miss callback
if not hit and self._miss:
try:
self._miss(actor)
except DropNotAllowed:
allowed = False
#
return allowed
[docs] def updateActor(self, interval, world):
"""Update the controller"""
super(DragController, self).updateActor(interval, world)
#
if self.active and self.dragging:
#
# Watch for mouse up - if this occurs with the mouse outside
# of the object then we will not get the event
if not self.mouse.isDown(serge.input.M_LEFT):
self.log.debug('Mouse up outside actor - dropping now')
self.clickedActor(self.dragging, (self.dragging, self.getDraggable(self.dragging).stop))
else:
mx, my = self.mouse.getScreenPos()
nx, ny = self.getDraggable(self.dragging).getXYValue(mx-self.drag_x, my-self.drag_y)
self.dragging.moveTo(nx, ny)
self.drag_x, self.drag_y = self.mouse.getScreenPos()
[docs] def setCallbacks(self, start, stop):
"""Set the callbacks to use when starting and stopping a drag"""
self._start = start
self._stop = stop
[docs] def setDropCallbacks(self, hit, miss):
"""Set the callback to use when dropping on a target"""
self._hit = hit
self._miss = miss