#!/usr/bin/env python from __future__ import division import gtk from threading import Thread from math import pi, tan import src as mt from utils import BoundingBoxArea, Flick, FlickThread, GtkEventWindow from hand import HandTracker mt.register_tracker(HandTracker) RED = 1, 0, 0 GREEN = 0, 1, 0 BLUE = 0, 0, 1 WHITE = 1, 1, 1 BLACK = 0, 0, 0 class Rectangle(mt.RectangularArea): def __init__(self, x, y, width, height, color=(1, 0, 0)): super(Rectangle, self).__init__(x, y, width, height) self.color = color self.on_drag(self.handle_drag) def handle_drag(self, g): tx, ty = g.get_translation() self.translate(tx, ty) refresh() def draw(self, cr): cr.rectangle(self.x, self.y, self.width, self.height) cr.set_source_rgb(*self.color) cr.fill() class Polygon(BoundingBoxArea): def __init__(self, x, y, points, margin=0, color=BLUE, border_color=RED): super(Polygon, self).__init__(x, y, points) self.fill_color = color self.border_color = border_color self.margin = margin self.on_drag(self.handle_drag) self.on_pinch(self.handle_pinch) self.on_rotate(self.handle_rotate) self.on_flick(self.handle_flick) def flick_drag(self, amt): tx, ty = self.flick_direction self.translate(tx * amt, ty * amt) refresh() def handle_flick(self, g): trans = g.get_translation() if trans.distance_to((0, 0)) > 10: self.flick_direction = trans flicks.add(Flick(self.flick_drag, 0.7, 0.4)) def contains(self, x, y): if draw_bounding_boxes: return mt.RectangularArea.contains(self, x, y) return BoundingBoxArea.contains(self, x, y) def handle_drag(self, g): tx, ty = g.get_translation() self.translate(tx, ty) refresh() def handle_pinch(self, g): cx, cy = g.get_position() self.scale_points(g.get_scale(), cx, cy) self.update_bounds() refresh() def handle_rotate(self, g): cx, cy = g.get_position() self.rotate_points(g.get_angle(), cx, cy) self.update_bounds() refresh() #def contains(self, x, y): # m = self.margin # return self.x - m <= x < self.x + self.width + m \ # and self.y - m <= y < self.y + self.height + m def draw(self, cr): # Draw bounding box if draw_bounding_boxes: m = self.margin cr.rectangle(self.x - m, self.y - m, self.width + 2 * m, self.height + 2 * m) cr.set_source_rgb(*self.border_color) cr.set_line_width(3) cr.stroke() # Fill polygon rx, ry = self.get_root_offset() cr.translate(rx, ry) cr.new_path() for x, y in zip(*self.points): cr.line_to(x, y) cr.set_source_rgb(*self.fill_color) cr.fill() fullscreen = False draw_bounding_boxes = False draw_touch_objects = True W, H = mt.screen.screen_size def create_context_window(w, h, callback): def create_context(area, event): """Add Cairo context to GTK window and draw state.""" global cr cr = area.window.cairo_create() draw() def update_window(): """Synchronize overlay with GTK window.""" overlay.set_size(*window.get_size()) refresh() return True def handle_key(win, event): """Handle key event. 'f' toggles fullscreen, 'b' toggles bounding boxes, 'i' toggles input points, 'q' exits the program.""" if event.keyval >= 256: return key = chr(event.keyval) if key == 'f': global fullscreen (win.unfullscreen if fullscreen else win.fullscreen)() fullscreen = not fullscreen elif key == 'b': global draw_bounding_boxes draw_bounding_boxes = not draw_bounding_boxes refresh() elif key == 'i': global draw_touch_objects draw_touch_objects = not draw_touch_objects refresh() elif key == 'q': quit() # Root area (will be synchronized with GTK window) global overlay overlay = mt.RectangularArea(0, 0, w, h) # GTK window global window, root window = GtkEventWindow() window.set_title('Cairo test') window.connect('destroy', quit) window.connect('key-press-event', handle_key) window.connect('show', callback) window.on_update(update_window) root = window.get_area() if fullscreen: window.fullscreen() # Drawing area, needed by cairo context for drawing area = gtk.DrawingArea() area.set_size_request(w, h) area.connect('expose-event', create_context) window.add(area) area.show() window.show() def draw(): if not cr: return # Background cr.rectangle(0, 0, *root.get_size()) cr.set_source_rgb(*BLACK) cr.fill() # Drawable objects (use save and restore to allow transformations) for obj in draw_objects: cr.save() obj.draw(cr) cr.restore() if draw_touch_objects: ox, oy = root.get_position() cr.set_source_rgb(*WHITE) for hand in touch_hands: cx, cy = hand.get_centroid() # Filled centroid circle if len(hand) > 1: cr.arc(cx - ox, cy - oy, 20, 0, 2 * pi) cr.fill() for x, y in hand: x -= ox y -= oy # Circle outline cr.set_line_width(3) cr.arc(x, y, 20, 0, 2 * pi) cr.stroke() # Line to centroid if len(hand) > 1: cr.move_to(x, y) cr.line_to(cx - ox, cy - oy) cr.set_line_width(2) cr.stroke() # Cross cr.set_line_width(1) cr.move_to(x - 8, y) cr.line_to(x + 8, y) cr.move_to(x, y - 8) cr.line_to(x, y + 8) cr.stroke() def refresh(*args): window.queue_draw() def quit(*args): gtk.main_quit() # Global variables window = cr = root = overlay = flicks = None draw_objects = [] touch_hands = [] def triangle_height(width): return abs(.5 * width * tan(2 / 3 * pi)) def on_show(window): def root_dtap(g): print 'double tapped on root' root.on_double_tap(root_dtap) # Create blue rectangle x, y, w, h = 0, 0, 250, 150 rect = Polygon(x, y, [(0, 0), (0, h), (w, h), (w, 0)], margin=20) draw_objects.append(rect) root.add_area(rect) def rect_tap(g): print 'tapped on rectangle' rect.on_tap(rect_tap, propagate_up_event=False) # Create green triangle x, y, w = 400, 400, 200 h = triangle_height(w) triangle = Polygon(x, y, [(0, h), (w, h), (w / 2, 0)], margin=20, color=GREEN) draw_objects.append(triangle) root.add_area(triangle) # Overlay catches finger events to be able to draw touch points def handle_down(gesture): if gesture.is_first(): touch_hands.append(gesture.get_hand()) if draw_touch_objects: refresh() def handle_up(gesture): if gesture.is_last(): touch_hands.remove(gesture.get_hand()) if draw_touch_objects: refresh() overlay.on_finger_down(handle_down) overlay.on_finger_move(lambda g: draw_touch_objects and refresh()) overlay.on_finger_up(handle_up) root.add_area(overlay) # Root area rotation leads to rotation of all polygons around the centroid def move_to_polygons(g): gx, gy = g.get_position() for obj in draw_objects: x, y = obj.get_position() g.set_position(gx - x, gy - y) yield obj, g def rotate_root(g): for obj, gesture in move_to_polygons(g): obj.handle_rotate(gesture) def scale_root(g): for obj, gesture in move_to_polygons(g): obj.handle_pinch(gesture) def drag_root(g): for obj, gesture in move_to_polygons(g): obj.handle_drag(gesture) root.on_rotate(rotate_root) root.on_pinch(scale_root) root.on_drag(drag_root) if __name__ == '__main__': from parse_arguments import create_parser, parse_args # Parse arguments parser = create_parser() parser.add_argument('-f', '--fullscreen', action='store_true', default=False, help='run in fullscreen initially') args = parse_args(parser) fullscreen = args.fullscreen # Create a window with a Cairo context in it and a multi-touch area # syncronized with it create_context_window(800, 600, on_show) # Run multi-touch gesture server in separate thread driver = mt.create_driver(root) mt_thread = Thread(target=driver.start) mt_thread.daemon = True mt_thread.start() # Flick movement is also handled in a separate thread flicks = FlickThread() flicks.daemon = True flicks.start() # Initialize threads in GTK so that the thread started above will work gtk.gdk.threads_init() # Start main loop in current thread gtk.main()