Input

Now, let us handle users’ input. We’ll start with mouse handling.

To handle mouse input, you need to define event handlers in your GraphicsObject. These are methods called on_pointer_motion, on_pointer_press, and so on. Each one is called with a pointer (cursor) identifier, coordinates, and possibly other keyword arguments. The pointer identifier is currently always the string 'mouse'. If you hook your program up to a different input system than a traditional mouse-based computer, such as a multi-touch screen, you can use different pointer identifiers to differentiate between different pointers [1].

The coordinates are more interesting. These come in three arguments, x, y, z [2], and represent the position of the pointer in the object’s own local coordinates.

The keyword arguments vary depending on the event type. For example, press and drag events will be called with a “button” argument. The methods need to accept any additional arguments that might be passed to them (for example, if someone connects Gillcup Graphics to a tablet, you might start getting pressure/tilt information in the events).

For motion and press events, the handler can return a true value to “claim” the event. Once claimed, events don’t propagate to any objects that might be below the claimer. For press events, the claimer will also exclusively get subsequent drag and release events.

Enough theory! The following example is a bit less minimal than the previous ones, but it should illustrate typical Gillcup Graphics usage better:

from __future__ import division

import gillcup
import gillcup_graphics


class DemoRect(gillcup_graphics.Rectangle):
    """A rectangle that goes purple on hover and blue on click/hold/drag
    """
    def __init__(self, parent, clock, **kwargs):
        super(DemoRect, self).__init__(parent, **kwargs)
        self.clock = clock
        self.pointers_in = set()
        self.pointers_pressed = set()

    def on_pointer_motion(self, pointer, x, y, z, **kwargs):
        if pointer not in self.pointers_in:
            self.clock.schedule(gillcup.Animation(self, 'red', 0, time=0.15))
        self.pointers_in.add(pointer)
        return True

    def on_pointer_leave(self, pointer, x, y, z, **kwargs):
        self.pointers_in.remove(pointer)
        if not self.pointers_in:
            self.clock.schedule(gillcup.Animation(self, 'red', 1, time=0.15))
        return True

    def on_pointer_press(self, pointer, x, y, z, button, **kwargs):
        if not self.pointers_pressed:
            self.clock.schedule(gillcup.Animation(self, 'green', 0, time=0.15))
        self.pointers_pressed.add((pointer, button))
        return True

    def on_pointer_release(self, pointer, x, y, z, button, **kwargs):
        self.pointers_pressed.remove((pointer, button))
        if not self.pointers_pressed:
            self.clock.schedule(gillcup.Animation(self, 'green', 1, time=0.15))
        return True


class DraggableRect(gillcup_graphics.Rectangle):
    """A rectangle that can be dragged around, but is "transparent" to hover

    N.B.: This simple mechanism will not work with objects that are scaled or
    rotated.
    """
    def __init__(self, parent, **kwargs):
        super(DraggableRect, self).__init__(parent, **kwargs)
        self.drag_starts = {}

    def on_pointer_press(self, pointer, x, y, z, button, **kwargs):
        self.drag_starts[pointer, button] = x, y
        return True

    def on_pointer_drag(self, pointer, x, y, z, button, **kwargs):
        start_x, start_y = self.drag_starts[pointer, button]
        self.x += x - start_x
        self.y += y - start_y
        return True

    def on_pointer_release(self, pointer, x, y, z, button, **kwargs):
        del self.drag_starts[pointer, button]
        return True


class DemoLayer(gillcup_graphics.Layer):
    def __init__(self, clock, *args, **kwargs):
        super(DemoLayer, self).__init__(*args, **kwargs)
        n = 10
        for x in range(n):
            for y in range(n):
                rct = DemoRect(self, clock, scale=(1 / n, 1 / n),
                    position=(x / n, y / n))
                if (x, y) == (5, 5):
                    rct.rotation = 30
                    rct.blue = 0.7
        DraggableRect(self, size=(0.05, 0.05), color=(0, 1, 0),
            position=(1 / 3, 1 / 3))


if __name__ == '__main__':
    clock = gillcup_graphics.RealtimeClock()
    layer = DemoLayer(clock)
    window = gillcup_graphics.Window(layer, width=500, height=500)
    gillcup_graphics.run()

Let’s take it one class at a time, this time.

The DemoRect class defines a rectangle that changes color depending on mouse input. In the on_pointer_motion and on_pointer_leave, we keep track of which pointers are “hovering” over. If there are any, it changes color to cyan (or more precisely, becomes “less red”). When the last pointer leaves, it switches back to white (or, “full red”). The on_pointer_press and on_pointer_leave do a similar thing with button-press-based events, except for each pointer they also remember which buttons are pressed. From the time a button is pressed over the rectangle, to the time the last such button is released, the square is “less green”. This nicely illustrates the dragging concept: even if you drag your mouse out of a rectangle, it still counts as dragging that rectangle, and the “release” events will fire even if the mouse is outside at that time.

The DraggableRect uses press, drag, and release events to implement an object that can be dragged around: it remembers where, relative to itself, the drag started, and when dragged, moves so that the pointer is at the same place (again, relative to the DraggableRect).

Finally, the DemoLayer is just a container for a hundred instances of DemoRect and a DraggableRect. One DemoRect is rotated and discolored to show what happens with transformations and overlapping.

[1]Of course, you’d need to do such hooking up manually, by calling pointer_event() of your root layer.
[2]The z coordinate will generally be zero.