|
|
@@ -0,0 +1,269 @@
|
|
|
+#!/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
|
|
|
+
|
|
|
+
|
|
|
+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, color=BLUE, border_color=RED):
|
|
|
+ super(Polygon, self).__init__(x, y, points)
|
|
|
+ self.fill_color = color
|
|
|
+ self.border_color = border_color
|
|
|
+
|
|
|
+ self.on_drag(self.handle_drag)
|
|
|
+ self.on_pinch(self.handle_pinch)
|
|
|
+ self.on_rotate(self.handle_rotate)
|
|
|
+
|
|
|
+ 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:
|
|
|
+ cr.rectangle(self.x, self.y, self.width, self.height)
|
|
|
+ cr.set_source_rgb(*self.border_color)
|
|
|
+ cr.set_line_width(3)
|
|
|
+ cr.stroke()
|
|
|
+
|
|
|
+ # Fill polygon
|
|
|
+ cr.translate(self.x, self.y)
|
|
|
+ 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 = draw_touch_points = 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 move_window(win, event):
|
|
|
+ """Synchronize root multi-touch area with GTK window."""
|
|
|
+ root.set_position(*event.get_coords())
|
|
|
+ root.set_size(event.width, event.height)
|
|
|
+ overlay.set_size(event.width, event.height)
|
|
|
+ draw()
|
|
|
+
|
|
|
+ def handle_key(win, event):
|
|
|
+ """Handle key event. 'f' toggles fullscreen, 'q' exits the program, 'b'
|
|
|
+ toggles bounding boxes, 'p' toggles touch points."""
|
|
|
+ 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 == 'q':
|
|
|
+ quit()
|
|
|
+ elif key == 'b':
|
|
|
+ global draw_bounding_boxes
|
|
|
+ draw_bounding_boxes = not draw_bounding_boxes
|
|
|
+ refresh()
|
|
|
+ elif key == 'p':
|
|
|
+ global draw_touch_points
|
|
|
+ draw_touch_points = not draw_touch_points
|
|
|
+ refresh()
|
|
|
+
|
|
|
+ # Root area (will be synchronized with GTK window)
|
|
|
+ global root, overlay
|
|
|
+ root = mt.RectangularArea(0, 0, w, h)
|
|
|
+ overlay = mt.RectangularArea(0, 0, w, h)
|
|
|
+
|
|
|
+ # GTK window
|
|
|
+ global window
|
|
|
+ window = gtk.Window()
|
|
|
+ window.set_title('Cairo test')
|
|
|
+ window.connect('destroy', quit)
|
|
|
+ window.connect('key-press-event', handle_key)
|
|
|
+ window.connect('configure-event', move_window)
|
|
|
+ window.connect('show', callback)
|
|
|
+
|
|
|
+ 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(*args):
|
|
|
+ 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_points:
|
|
|
+ ox, oy = root.get_position()
|
|
|
+ cr.set_source_rgb(*WHITE)
|
|
|
+
|
|
|
+ for x, y in touch_points.itervalues():
|
|
|
+ x -= ox
|
|
|
+ y -= oy
|
|
|
+
|
|
|
+ cr.set_line_width(3)
|
|
|
+ cr.arc(x, y, 20, 0, 2 * pi)
|
|
|
+ cr.stroke()
|
|
|
+
|
|
|
+ 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():
|
|
|
+ window.queue_draw()
|
|
|
+
|
|
|
+
|
|
|
+def quit(*args):
|
|
|
+ gtk.main_quit()
|
|
|
+
|
|
|
+
|
|
|
+# Initialization
|
|
|
+window = cr = root = overlay = None
|
|
|
+draw_objects = []
|
|
|
+touch_points = {}
|
|
|
+
|
|
|
+
|
|
|
+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)])
|
|
|
+ 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)], color=GREEN)
|
|
|
+ draw_objects.append(triangle)
|
|
|
+ root.add_area(triangle)
|
|
|
+
|
|
|
+ # Overlay catches basic events
|
|
|
+ def handle_down(gesture):
|
|
|
+ point = gesture.get_event().get_touch_object()
|
|
|
+ touch_points[point.get_id()] = point.get_position()
|
|
|
+
|
|
|
+ if draw_touch_points:
|
|
|
+ refresh()
|
|
|
+
|
|
|
+ def handle_up(gesture):
|
|
|
+ point = gesture.get_event().get_touch_object()
|
|
|
+ del touch_points[point.get_id()]
|
|
|
+
|
|
|
+ if draw_touch_points:
|
|
|
+ refresh()
|
|
|
+
|
|
|
+ overlay.on_point_down(handle_down)
|
|
|
+ overlay.on_point_move(handle_down)
|
|
|
+ overlay.on_point_up(handle_up)
|
|
|
+ root.add_area(overlay)
|
|
|
+
|
|
|
+
|
|
|
+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(640, 460, 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()
|
|
|
+
|
|
|
+ # Initialize threads in GTK so that the thread started above will work
|
|
|
+ gtk.gdk.threads_init()
|
|
|
+
|
|
|
+ # Start main loop in current thread
|
|
|
+ gtk.main()
|