#!/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.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) self.on_tap(self.circulate_color) def circulate_color(self, g): self.color = self.color[2:] + self.color[:2] refresh() 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 margin_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 contains(self, x, y): if draw_bounding_boxes: return self.margin_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 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_offset(root) cr.translate(rx, ry) cr.new_path() for x, y in zip(*self.points): cr.line_to(x, y) cr.set_source_rgb(*self.color) cr.fill() fullscreen = False draw_bounding_boxes = False draw_touch_objects = True W, H = mt.screen.screen_size def toggle_fullscreen(): global fullscreen (window.unfullscreen if fullscreen else window.fullscreen)() fullscreen = not fullscreen refresh() 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 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': toggle_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() def redraw_updated_window(): refresh() return True # 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(redraw_updated_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: centroid = hand.get_centroid() # Check if all fingers have not been removed already if not centroid: continue cx, cy = 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): # Toggle fullscreen on double tap root.on_double_tap(lambda g: toggle_fullscreen()) # 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) # 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): touch_hands.append(gesture.get_hand()) if draw_touch_objects: refresh() def handle_up(gesture): touch_hands.remove(gesture.get_hand()) if draw_touch_objects: refresh() global screen, overlay screen = mt.FullscreenArea() overlay = mt.FullscreenArea() screen.add_area(root) screen.add_area(overlay) overlay.on_hand_down(handle_down) overlay.on_finger_move(lambda g: draw_touch_objects and refresh()) overlay.on_hand_up(handle_up) # 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(screen) 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()