├── optional.txt ├── docs ├── video.mp4 ├── preview.gif ├── video-final.py └── demo.py ├── xpybutil ├── __init__.py ├── compat.py ├── event.py ├── util.py └── keybind.py ├── examples ├── empty.py ├── line_demo.py └── functions.py ├── requirements.txt ├── unikeysym.py ├── config.py ├── const.py ├── compgeo.py ├── tkinter_doc.py ├── ogl_doc.py ├── sdl_doc.py ├── tree_lang.py ├── gui_lang.py ├── xcb_doc.py ├── draw.py ├── node.py ├── wrap_event.py ├── readme.md ├── undoable.py └── flow_editor.py /optional.txt: -------------------------------------------------------------------------------- 1 | PyOpenGL 2 | xcffib 3 | cairocffi 4 | -------------------------------------------------------------------------------- /docs/video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asrp/guitktk/HEAD/docs/video.mp4 -------------------------------------------------------------------------------- /docs/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asrp/guitktk/HEAD/docs/preview.gif -------------------------------------------------------------------------------- /xpybutil/__init__.py: -------------------------------------------------------------------------------- 1 | from xpybutil.compat import xcb, xcb_ConnectException 2 | 3 | try: 4 | conn = xcb.connect() 5 | root = conn.get_setup().roots[0].root 6 | except xcb_ConnectException: 7 | conn = None 8 | root = None 9 | -------------------------------------------------------------------------------- /examples/empty.py: -------------------------------------------------------------------------------- 1 | from draw import collide, simplify_transform 2 | from const import rounded, identity, get_matrix 3 | import numpy 4 | import persistent.document as pdocument 5 | from persistent.document import Expr, Ex 6 | 7 | pdocument.scope = {"P": P} 8 | 9 | input_callbacks = "" 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e git://github.com/asrp/persistent_doc#egg=persistent_doc 2 | -e git://github.com/asrp/tkui#egg=tkui 3 | -e git://github.com/asrp/pymetaterp#egg=pymetaterp 4 | -e git://github.com/asrp/uielem#egg=uielem 5 | -e git://github.com/asrp/undoable#egg=undoable 6 | pyrsistent 7 | numpy 8 | -------------------------------------------------------------------------------- /unikeysym.py: -------------------------------------------------------------------------------- 1 | codes = [l.split()[:5] for l in open("keysyms.txt") if not l.startswith("#")] 2 | codes = [l for l in codes if l] 3 | keys = {eval(key): {"unicode": unichr(int(uni[1:], 16)), 4 | "status": status, 5 | "keyname": keyname} 6 | for key, uni, status, comment, keyname in codes} 7 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | #BACKEND = "xcb" 2 | BACKEND = "tkinter" 3 | #BACKEND = "opengl" 4 | #BACKEND = "sdl" 5 | 6 | MAINLOOP = "tkinter" 7 | # Only for opengl backend. Ignores frequencies. 8 | #MAINLOOP = "glut" 9 | 10 | LOG_EVENTS = True 11 | # Wait time in ms. Larger = slower. 12 | POLL_FREQUENCY = 60 13 | POLL_FREQUENCY = 30 14 | #POLL_FREQUENCY = 10 15 | #DRAW_FREQUENCY = 240 16 | #DRAW_FREQUENCY = 120 17 | DRAW_FREQUENCY = 20 18 | # Always draw when an event is bpolled 19 | DRAW_FREQUENCY = None 20 | GRID = 20 21 | -------------------------------------------------------------------------------- /xpybutil/compat.py: -------------------------------------------------------------------------------- 1 | try: 2 | import xcffib as xcb 3 | import xcffib.xproto as xproto 4 | import xcffib.xinerama as xinerama 5 | import xcffib.randr as randr 6 | import xcffib.render as render 7 | from xcffib import XcffibException as xcb_Exception 8 | from xcffib import ConnectionException as xcb_ConnectException 9 | 10 | except ImportError: 11 | import xcb 12 | import xcb.xproto as xproto 13 | import xcb.xinerama as xinerama 14 | import xcb.randr as randr 15 | import xcb.render as render 16 | from xcb import Exception as xcb_Exception 17 | from xcb import ConnectException as xcb_ConnectException 18 | -------------------------------------------------------------------------------- /const.py: -------------------------------------------------------------------------------- 1 | import numpy, math 2 | from config import GRID 3 | from persistent_doc.document import Expr, Ex 4 | 5 | transformed_layers = ["drawing", "editor", "grid"] 6 | 7 | default = {"line_width": 2, 8 | "stroke_color": (0, 0, 0), 9 | "fill_color": None, 10 | "dash": ([], 0), 11 | "font_size": 20, 12 | "font_face": None, 13 | "skip_points": False, 14 | "angle": (0, 2*math.pi), 15 | "icon": "point_icon", 16 | "visible": True, 17 | "opacity": 1.0, 18 | "render": (), 19 | "topleft": {"value": (0, 0)}} 20 | 21 | #identity = numpy.matrix(numpy.identity(3, dtype = int)) 22 | identity = numpy.identity(3, dtype = int) 23 | 24 | def default_get(d, key): 25 | return d.get(key, default[key]) 26 | 27 | def transformed(point, transform=identity): 28 | transform = transform.dot(point.transform) 29 | return transform.dot(numpy.append(point["value"], 1))[:2] 30 | 31 | def P(*args): 32 | return numpy.array(args) 33 | 34 | def exr(expr): 35 | return Ex(expr, "reeval") 36 | 37 | def exc(expr): 38 | return Ex(expr, "on first read") 39 | 40 | def get_translate(node, key): 41 | transform = node.transforms.get(key, ("translate", P(0,0))) 42 | if transform[0] != "translate": 43 | raise Exception('Transform is not a translation: %s' % transform) 44 | else: 45 | return transform[1] 46 | 47 | def get_scale(node, key): 48 | transform = node.transforms.get(key, ("scale", P(1.0, 1.0))) 49 | if transform[0] != "scale": 50 | raise Exception('Transform is not a scaling: %s' % transform) 51 | else: 52 | return transform[1] 53 | 54 | def rounded(point): 55 | return (point + GRID/2) // GRID * GRID 56 | 57 | def get_matrix(transform): 58 | if transform is None: 59 | return identity 60 | operation, args = transform 61 | if operation == "linear": 62 | matrix = args 63 | elif operation == "translate": 64 | matrix = numpy.array([[1, 0, args[0]], 65 | [0, 1, args[1]], 66 | [0, 0, 1]]) 67 | elif operation == "scale": 68 | matrix = numpy.array([[args[0], 0, 0], 69 | [0, args[1], 0], 70 | [0, 0, 1]]) 71 | elif operation == "rotate": 72 | matrix = numpy.array([[math.cos(args), -math.sin(args), 0], 73 | [math.sin(args), math.cos(args), 0], 74 | [0, 0, 1]]) 75 | else: 76 | raise Exception('Unknown transform %s' % operation) 77 | return matrix 78 | -------------------------------------------------------------------------------- /docs/video-final.py: -------------------------------------------------------------------------------- 1 | from draw import collide, simplify_transform 2 | from const import rounded, identity, get_matrix 3 | import numpy 4 | import persistent_doc.document as pdocument 5 | from persistent_doc.document import Expr, Ex 6 | 7 | pdocument.scope = {"P": P} 8 | 9 | input_callbacks = """ 10 | exec = key_press(Return) 11 | (~key_press(Return) (key_press !add_letter(console) | @anything))* 12 | key_press(Return) !run_text(console) !clear(console) 13 | button = mouse_press(1) ?run_button mouse_release(1) 14 | text = key_press(t) (?edit_text | !create_text) 15 | (~key_press(Return) (key_press !add_letter | @anything))* 16 | key_press(Return) !finished_edit_text 17 | grammar = ( @exec | @button | @text | @anything)* 18 | """ 19 | 20 | def finished_edit_text(): 21 | node = doc[doc["editor.focus"]] 22 | text = node["value"] 23 | if text.startswith("!"): 24 | node["on_click"] = text[1:] 25 | elif text.startswith("="): 26 | node["value"] = Ex(text[1:], calc="reeval") 27 | 28 | def edit_text(): 29 | root = doc[doc["selection.root"]] 30 | for child, transform in root.dfs(): 31 | if child.name == "text" and\ 32 | collide(child, doc["editor.mouse_xy"], transform=transform, tolerance=8): 33 | doc["editor.focus"] = child["id"] 34 | return True 35 | return False 36 | 37 | def create_text(): 38 | doc["drawing"].append(Node("text", value="", 39 | p_botleft=doc["editor.mouse_xy"])) 40 | doc["editor.focus"] = doc["drawing"][-1]["id"] 41 | 42 | def run_button(): 43 | root = doc[doc["selection.root"]] 44 | xy = doc["editor.mouse_xy"] 45 | for child in reversed(root): 46 | if collide(child, xy): 47 | print "clicked on", child["id"] 48 | if "on_click" in child: 49 | run_text(child["id"], "on_click") 50 | return True 51 | return False 52 | 53 | def mouse_press(event, button=None): 54 | return event.type == Event.mouse_press and\ 55 | (button is None or event.button == int(button)) 56 | 57 | def mouse_release(event, button=None): 58 | return event.type == Event.mouse_release and\ 59 | (button is None or event.button == int(button)) 60 | 61 | def add_letter(node_id=None): 62 | node_id = node_id if node_id is not None else doc["editor.focus"] 63 | if doc["editor.key_name"] == "BackSpace": 64 | doc[node_id + ".value"] = doc[node_id + ".value"][:-1] 65 | else: 66 | doc[node_id + ".value"] += doc["editor.key_char"] 67 | 68 | def run_text(node_id, param="value"): 69 | try: 70 | co = compile(doc[node_id][param], "", "single") 71 | exec co in globals() 72 | except: 73 | traceback.print_exc() 74 | 75 | def clear(node_id): 76 | doc[node_id]["value"] = "" 77 | 78 | "### Add keypress handling" 79 | 80 | def key_press(event, key_name=None): 81 | return event.type == Event.key_press and\ 82 | (key_name is None or event.key_name == key_name) 83 | 84 | 85 | # doc['drawing'].append(Node("text", value="hello world", p_botleft=P(100, 100))) 86 | # doc['drawing'][-1].change_id("console") 87 | # doc["console"]["value"] = "" 88 | # doc['drawing'].append(Node("text", id="button1", value="Click me!", p_botleft=P(10, 210))) 89 | # doc['button1.on_click'] = "doc['button1.value'] = 'Clicked!'" 90 | -------------------------------------------------------------------------------- /compgeo.py: -------------------------------------------------------------------------------- 1 | from numpy import dot, array, sum 2 | from numpy.linalg import norm 3 | import math 4 | from config import BACKEND 5 | 6 | surface = None 7 | rot90 = array([[0, -1], [1, 0]]) 8 | 9 | def norm2(v): 10 | return sum(dot(v.T,v)) 11 | 12 | def distance2(point, line): 13 | start = line[0] - point 14 | end = line[1] - point 15 | perp = rot90.dot(end - start) 16 | proj = (start.dot(perp)) / norm(perp) 17 | if norm2(proj - start) > norm2(end - start): 18 | return norm2(end) 19 | elif norm2(proj - end) > norm2(end - start): 20 | return norm2(start) 21 | else: 22 | return norm2(proj) 23 | 24 | def arc_endpoints(center, radius, angle): 25 | end0 = (radius * math.cos(angle[0]), 26 | radius * math.sin(angle[0])) 27 | end1 = (radius * math.cos(angle[1]), 28 | radius * math.sin(angle[1])) 29 | return end0 + center, end1 + center 30 | 31 | def distance_to_arc(point, center, radius, angle): 32 | dist_to_circle = abs(norm(center - point) - radius) 33 | point_angle = math.atan2(*reversed(point - center)) 34 | point_angle = point_angle + (point_angle<0)*2*math.pi 35 | if angle[0] <= point_angle <= angle[1]: 36 | return dist_to_circle 37 | else: 38 | end0, end1 = arc_endpoints(center, radius, angle) 39 | return min(norm(end0 - point), norm(end1 - point)) 40 | 41 | def is_left(point, segment): 42 | area1 = (segment[1][0] - segment[0][0]) * (point[1] - segment[0][1]) 43 | area2 = (segment[1][1] - segment[0][1]) * (point[0] - segment[0][0]) 44 | return cmp(area1, area2) 45 | 46 | def point_in_closed_path(point, path): 47 | # Compute the winding number 48 | # Pick any direction from point and corresponding half infinite segment. 49 | # See how mnay times that segment is crossed. 50 | # We pick the half infinite line going straight right 51 | winding_number = 0 52 | for segment in path: 53 | # Cross up 54 | if segment[0][1] <= point[1] < segment[1][1] and is_left(point, segment) > 0: 55 | winding_number += 1 56 | # Cross down 57 | if segment[0][1] > point[1] >= segment[1][1] and is_left(point, segment) < 0: 58 | winding_number -= 1 59 | # print "Winding number", winding_number 60 | return winding_number != 0 61 | 62 | def extents(text, font_size, font_face=None): 63 | if BACKEND == "tkinter": 64 | font = (font_face if font_face else "TkFixedFont", 65 | int(round(font_size))) 66 | item = surface.canvas.create_text(0, 0, 67 | font=font, 68 | text=text, anchor="sw") 69 | x1, x2, y1, y2 = surface.canvas.bbox(item) 70 | return (0, x2 - x1), (x1 - x2, y1 - y2), (y1 - y2, 0) 71 | elif BACKEND == "xcb": 72 | surface.context.set_font_size(font_size * 1.5) 73 | if font_face: 74 | surface.context.select_font_face(font_face) 75 | else: 76 | surface.context.set_font_face(None) 77 | x, y, width, height, dx, dy = surface.context.text_extents(text) 78 | return (x, y), (width, height), (dx, dy) 79 | elif BACKEND == "sdl": 80 | import sdl_doc 81 | font_size = int(round(font_size * 1.5)) 82 | text = sdl_doc.get_font((font_face, font_size)).render(text, True, (0, 0, 0)) 83 | w, h = text.get_width(), text.get_height() 84 | return (0, -h), (w, h), (w, 0) 85 | else: 86 | # TODO: Get actual text extents 87 | w, h = (len(text) * font_size, font_size) 88 | #import pdb; pdb.set_trace() 89 | return (0, -h), (w, h), (w, 0) 90 | -------------------------------------------------------------------------------- /tkinter_doc.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | from const import default_get, identity, transformed 3 | import math 4 | from draw import flatten 5 | import Tkinter 6 | from collections import OrderedDict 7 | import logging 8 | logger = logging.getLogger("tk_draw") 9 | 10 | def hexcol(color): 11 | if color is None: 12 | return "" 13 | return '#%02x%02x%02x' % tuple(c*255 for c in color) 14 | 15 | class TkCanvas: 16 | def __init__(self, width, height, root=None): 17 | self.width = width 18 | self.height = height 19 | self.canvas = Tkinter.Canvas(root, width=width, height=height) 20 | self.layers = OrderedDict() 21 | 22 | def draw(self, root): 23 | points = [] 24 | for func, args in flatten(root): 25 | #if func not in ["begin_region", "move_to", "line_to"]: continue 26 | if func == "stroke_and_fill": 27 | # Hack for arcs 28 | if len(points) <= 1: continue 29 | flat_pts = [coord for point in points for coord in point] 30 | self.canvas.create_polygon(*flat_pts, 31 | width=default_get(args, "line_width"), 32 | dash=default_get(args, "dash")[0], 33 | outline=hexcol(default_get(args, "stroke_color")), 34 | fill=hexcol(default_get(args, "fill_color"))) 35 | elif func == "text": 36 | font = (args["font_face"] if args.get("font_face") else "TkFixedFont", 37 | int(round(args["font_size"]))) 38 | self.canvas.create_text(*args["botleft"], font=font, 39 | fill=hexcol(default_get(args, "stroke_color")), 40 | text=args["text"], anchor="sw") 41 | elif func == "group": 42 | pass 43 | elif func == "begin_region": 44 | points = [] 45 | elif func == "end_region": 46 | pass 47 | elif func in ["move_to", "line_to"]: 48 | points.append(args) 49 | elif func == "arc": 50 | # Doesn't work inside polygons yet. 51 | x, y = args["center"] 52 | r = args["radius"] 53 | start = args["angle"][0] * 180/math.pi, 54 | angle_diff = (args["angle"][0] - args["angle"][1]) * 180/math.pi 55 | slice_type = Tkinter.PIESLICE if default_get(args["style"], "fill_color") else Tkinter.ARC 56 | if angle_diff % 360 == 0: 57 | #self.canvas.create_arc(x-r, y-r, x+r, y+r, start=0, extent=360, fill="red") 58 | start = 0.0 59 | # Must be a tkinter bug 60 | angle_diff = 359 61 | self.canvas.create_arc(x-r, y-r, x+r, y+r, 62 | start=start, 63 | extent=angle_diff, 64 | style=slice_type, 65 | outline=hexcol(default_get(args["style"], "stroke_color")), 66 | fill=hexcol(default_get(args["style"], "fill_color"))) 67 | else: 68 | raise Exception('Unknown function %s, %s' % (func, args)) 69 | 70 | def addlayer(self, doc, root_id): 71 | self.layers[root_id] = (doc, root_id) 72 | 73 | def expose(self): 74 | import time 75 | starttime = time.time() 76 | self.canvas.delete("all") 77 | logger.debug("Emptied %.5f", (time.time() - starttime)) 78 | for doc, root_id in self.layers.values(): 79 | root = doc[root_id] 80 | self.draw(root) 81 | logger.debug("Finished %s %.5f", root["id"], time.time() - starttime) 82 | logger.debug("Draw finished %.5f", time.time() - starttime) 83 | 84 | def update_all(self): 85 | self.expose() 86 | return True 87 | 88 | def show(self): 89 | self.canvas.pack() 90 | 91 | def update_layer(self, root_id): 92 | pass 93 | -------------------------------------------------------------------------------- /ogl_doc.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | from const import default_get, identity, transformed 3 | from config import MAINLOOP 4 | import math 5 | from draw import flatten 6 | from collections import OrderedDict 7 | import ctypes 8 | from OpenGL.GL import * 9 | from OpenGL.GLUT import * 10 | import logging 11 | logger = logging.getLogger("ogl_draw") 12 | 13 | class OGLCanvas: 14 | def __init__(self, width, height, root=None): 15 | self.width = width 16 | self.height = height 17 | glutInitWindowSize(width, height) 18 | self.win_id = glutCreateWindow("pyxcbcairo") 19 | glutDisplayFunc(self.show) 20 | glClearColor(1.0, 1.0, 1.0, 1.0) 21 | glOrtho(0, width, height, 0, 0, 1) 22 | glLineWidth(2) 23 | #glMatrixMode(GL_PROJECTION) 24 | #glLoadIdentity() 25 | #glViewport(0, 0, self.width, self.height) 26 | self.layers = OrderedDict() 27 | if MAINLOOP == "glut": 28 | glutDisplayFunc(self.update_all) 29 | glutPostRedisplay() 30 | 31 | def draw(self, root): 32 | points = [] 33 | for func, args in flatten(root): 34 | #if func not in ["begin_region", "move_to", "line_to"]: continue 35 | if func == "stroke_and_fill": 36 | # Hack for arcs 37 | if len(points) <= 1: continue 38 | if default_get(args, "fill_color"): 39 | glColor3f(*default_get(args, "fill_color")) 40 | glBegin(GL_POLYGON) 41 | for point in points: 42 | glVertex2d(*(point)) 43 | glEnd() 44 | if default_get(args, "stroke_color"): 45 | glColor3f(*default_get(args, "stroke_color")) 46 | glBegin(GL_LINE_LOOP) 47 | for point in points: 48 | glVertex2d(*(point)) 49 | glEnd() 50 | elif func == "text": 51 | glWindowPos2f(args["botleft"][0], self.height-args["botleft"][1]) 52 | for ch in (args["text"] or ""): 53 | glutBitmapCharacter(GLUT_BITMAP_TIMES_ROMAN_24, 54 | ctypes.c_int(ord(ch))) 55 | elif func == "group": 56 | pass 57 | elif func == "begin_region": 58 | points = [] 59 | elif func == "end_region": 60 | pass 61 | elif func in ["move_to", "line_to"]: 62 | points.append(args) 63 | elif func == "arc": 64 | # Doesn't work inside polygons yet. 65 | x, y = args["center"] 66 | r = args["radius"] 67 | start = args["angle"][0] * 180/math.pi, 68 | angle_diff = (args["angle"][0] - args["angle"][1]) * 180/math.pi 69 | if "fill_color" in args["style"]: 70 | glColor3f(*default_get(args["style"], "fill_color")) 71 | glBegin(GL_POLYGON) 72 | else: 73 | glColor3f(*default_get(args["style"], "stroke_color")) 74 | glBegin(GL_LINES) 75 | N = 20 76 | for i in xrange(N): 77 | theta = i * 2*math.pi / N 78 | glVertex2d(x + r * math.sin(theta), 79 | y + r * math.cos(theta)) 80 | glEnd() 81 | else: 82 | raise Exception('Unknown function %s, %s' % (func, args)) 83 | 84 | def addlayer(self, doc, root_id): 85 | self.layers[root_id] = (doc, root_id) 86 | 87 | def expose(self): 88 | import time 89 | starttime = time.time() 90 | glutSetWindow(self.win_id) 91 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) 92 | logger.debug("Emptied %.5f", (time.time() - starttime)) 93 | for doc, root_id in self.layers.values(): 94 | self.draw(doc[root_id]) 95 | logger.debug("Finished %s %.5f", root_id, time.time() - starttime) 96 | glutSwapBuffers() 97 | logger.debug("Draw finished %.5f", time.time() - starttime) 98 | 99 | def update_all(self): 100 | self.expose() 101 | return True 102 | 103 | def show(self): 104 | pass 105 | 106 | def update_layer(self, root_id): 107 | pass 108 | -------------------------------------------------------------------------------- /sdl_doc.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | from const import default_get, identity, transformed, P 3 | import math 4 | from draw import flatten 5 | from collections import OrderedDict 6 | import pygame 7 | import logging 8 | logger = logging.getLogger("sdl_draw") 9 | 10 | def intcol(color): 11 | if color is None: 12 | return None 13 | return tuple(c*255 for c in color) 14 | 15 | fonts = {} 16 | font_files = {"monospace": "/usr/share/fonts/TTF/DejaVuSansMono.ttf", 17 | None: None} 18 | 19 | def get_font(font): 20 | if font not in fonts: 21 | fonts[font] = pygame.font.Font(font_files[font[0]], font[1]) 22 | return fonts[font] 23 | 24 | class SDLCanvas: 25 | def __init__(self, width, height, root=None): 26 | self.width = width 27 | self.height = height 28 | self.screen = pygame.display.set_mode((self.width, self.height), 29 | pygame.DOUBLEBUF) 30 | self.layers = OrderedDict() 31 | 32 | def draw(self, root, surface=None): 33 | surface = surface if surface is not None else self.screen 34 | points = [] 35 | for func, args in flatten(root): 36 | #if func not in ["begin_region", "move_to", "line_to"]: continue 37 | if func == "stroke_and_fill": 38 | # Hack for arcs 39 | if len(points) <= 1: continue 40 | #flat_pts = [coord for point in points for coord in point] 41 | # Problem: with alpha, want a bbox and then translate... 42 | if default_get(args, "fill_color") and len(points) > 2: 43 | pygame.draw.polygon(surface, 44 | intcol(default_get(args, "fill_color")), 45 | map(tuple, points), 46 | 0) 47 | if default_get(args, "stroke_color"): 48 | if default_get(args, "dash"): 49 | #[start*t + end*(1-t) for t in midpoints] 50 | pass 51 | pygame.draw.lines(surface, 52 | intcol(default_get(args, "stroke_color")), 53 | False, 54 | points, 55 | int(default_get(args, "line_width"))) 56 | elif func == "text": 57 | font_size = args['font_size'] if numpy.array_equal(args["transform"], identity) or args['transform'][0][1] or args['transform'][1][0]\ 58 | else args['font_size'] * args["transform"][0][0] 59 | #font_size = args['font_size'] 60 | font_param = (args.get("font_face"), 61 | int(round(1.5 * font_size))) 62 | color = intcol(default_get(args, "stroke_color")) 63 | text = get_font(font_param).render(str(args["text"]), True, color) 64 | surface.blit(text, args["botleft"] - P(0, text.get_height())) 65 | elif func == "group": 66 | pass 67 | elif func == "begin_region": 68 | points = [] 69 | elif func == "end_region": 70 | pass 71 | elif func in ["move_to", "line_to"]: 72 | points.append(args) 73 | elif func == "arc": 74 | # Doesn't work inside polygons yet. 75 | topleft = args["center"] - P(args["radius"], args["radius"]) 76 | wh = P(2*args["radius"], 2*args["radius"]) 77 | angles = args["angle"] 78 | if angles == (0, 2*math.pi): 79 | pygame.draw.circle(surface, 80 | intcol(default_get(args["style"], "stroke_color")), 81 | map(int, args["center"]), 82 | args["radius"], 83 | 0) 84 | else: 85 | pygame.draw.arc(surface, 86 | intcol(default_get(args["style"], "stroke_color")), 87 | (topleft, wh), 88 | angles[0], angles[1], 89 | default_get(args, "line_width")) 90 | else: 91 | raise Exception('Unknown function %s, %s' % (func, args)) 92 | 93 | def addlayer(self, doc, root_id): 94 | self.layers[root_id] = (doc, root_id) 95 | 96 | def expose(self): 97 | import time 98 | starttime = time.time() 99 | self.screen.fill((255, 255, 255)) 100 | logger.debug("Emptied %.5f", (time.time() - starttime)) 101 | for doc, root_id in self.layers.values(): 102 | root = doc[root_id] 103 | self.draw(root) 104 | logger.debug("Finished %s %.5f", root["id"], time.time() - starttime) 105 | logger.debug("Draw finished %.5f", time.time() - starttime) 106 | #pygame.display.update() 107 | pygame.display.flip() 108 | 109 | def update_all(self): 110 | self.expose() 111 | return True 112 | 113 | def show(self): 114 | pass 115 | 116 | def update_layer(self, root_id): 117 | pass 118 | -------------------------------------------------------------------------------- /tree_lang.py: -------------------------------------------------------------------------------- 1 | #from gui_lang import simple_wrap_tree, boot_tree, boot, boot_grammar, python 2 | from pymetaterp.util import simple_wrap_tree 3 | from pymetaterp import boot_tree, boot_stackless as boot, boot_grammar 4 | from pymetaterp.boot_stackless import Eval, Frame, MatchError 5 | from pymetaterp import python 6 | from node import Node 7 | from pdb import set_trace as bp 8 | 9 | def paramdict(params, children): 10 | d = {} if params is None else\ 11 | dict([params] if type(params) == tuple else\ 12 | params) 13 | if children: 14 | if d.get('children'): 15 | d['children'].extend(children) 16 | else: 17 | d['children'] = children 18 | return d 19 | 20 | tree_grammar = r""" 21 | grammar = { (INDENT NEWLINE+ SAME_INDENT node (NEWLINE+ | ~anything))+ 22 | | node } spaces 23 | escaped_char! = '\\' {'n'|'r'|'t'|'b'|'f'|'"'|'\''|'\\'} 24 | node = ("." {NAME} "=")?:child_id ({NAME:name} ':') (spaces {param})*:params 25 | hspaces suite:children -> DNode(name, child_id=child_id, **paramdict(params, children)) 26 | param = NAME:name "=" expr:val -> (name, val) 27 | expr = STRING:val -> val 28 | | NUMBER:val -> float(val) if '.' in val else int(val) 29 | | (NAME balanced | NAME | balanced | list_balanced):val -> eval(val) # val 30 | # | LAZY_EVAL:val -> val 31 | suite = (INDENT (NEWLINE+ SAME_INDENT node)+ DEDENT 32 | | node | void):value -> to_list(value) 33 | balanced = '(' (escaped_char | balanced | ~')' anything)* ')' 34 | list_balanced = '[' (escaped_char | balanced | ~']' anything)* ']' 35 | STRING = hspaces ( '"' {(~'"' anything)*} '"' 36 | | '\'' {(~'\'' anything)*} '\'') 37 | NEWLINE = hspaces ('\n' | '\r') {} | COMMENT_LINE 38 | COMMENT_LINE = hspaces {comment} hspaces ('\n' | '\r') 39 | SAME_INDENT = hspaces:s ?(self.indentation[-1] == (len(s) if s != None else 0)) 40 | INDENT = ~~(NEWLINE hspaces:s ?(self.indentation[-1] < (len(s) if s != None else 0))) !(self.indentation.append(len(s) if s != None else 0)) 41 | DEDENT = !(self.indentation.pop()) 42 | NAME = (letter | '_') (letter | digit | '_' | '.')* 43 | NUMBER = '-'? (digit | '.')+ 44 | 45 | comment = ('#' {(~'\n' {anything})*})=comment 46 | space = '\n' | '\r' | hspace 47 | spaces = space* 48 | spacesp = space+ 49 | hspaces = (' ' | '\t')* 50 | hspacesp = (' ' | '\t')+ 51 | """ 52 | # expr should join everything together 53 | 54 | inp = """ 55 | group: id="root" 56 | group: id="references" 57 | group: id="drawing" 58 | transforms=dict(zoom=("scale" P(1.0 1.0)) 59 | scroll_xy=("translate" P(0 0))) 60 | group: id="ui" 61 | group: id="editor" stroke_color=tuple(0 0.5 0) mode="edit" 62 | .path=path: stroke_color=tuple(0 0.5 0) 63 | .lastxy=point: value=(0 0) 64 | .text=text: value="" botleft=Ex("`self.parent.lastxy") 65 | group: id="overlay" 66 | group: id="selection" root="drawing" 67 | group: id="selection_bbox" stroke_color=tuple(0.5 0 0) 68 | dash=tuple([5 5] 0) skip_points=True 69 | group: id="clipboard" visible=False 70 | group: id="mouseover" root="drawing" 71 | group: id="status" root="drawing" 72 | text: ref_id="editor.lastxy" ref_param="value" 73 | .botleft=point: value=(0 600) 74 | text: ref_id="ui" ref_param="mode" 75 | .botleft=point: value=(100 600) 76 | group: id="grid" line_width=1 stroke_color=tuple(0 0 1) 77 | skip_points=True 78 | 79 | """ 80 | inp2 = """ 81 | group: id="root" 82 | group: id="references" 83 | group: id="drawing" 84 | text: value="inner1" 85 | group: id="overlay" 86 | """ 87 | inp3 = """ 88 | group: transforms=dict(pos=("translate", (100, 200))) 89 | .left=text: value='a' p_botleft=(-30, -10) 90 | .middle=text: value='b' p_botleft=(-10, -10) 91 | .right=text: value='c' p_botleft=( 10, -10) 92 | """ 93 | inp4 = """ 94 | group: foo=1.0 95 | """ 96 | 97 | def interpreter(): 98 | i1 = boot.Interpreter(simple_wrap_tree(boot_tree.tree)) 99 | grammar = boot_grammar.bootstrap + boot_grammar.extra + boot_grammar.diff 100 | match_tree1 = i1.match(i1.rules['grammar'][-1], grammar) 101 | i2 = boot.Interpreter(match_tree1) 102 | match_tree2 = i2.match(i2.rules['grammar'][-1], tree_grammar + boot_grammar.extra) 103 | return python.Interpreter(match_tree2) 104 | 105 | interp = interpreter() 106 | interp.source = tree_grammar 107 | def parse(tree_str, **kwargs): 108 | global interp 109 | # Problem with nested matches! 110 | # Need a different inner interpreter in that case! 111 | if getattr(interp, "stack", []): 112 | interp = interpreter() 113 | interp.source = tree_grammar 114 | if 'locals' in kwargs: 115 | kwargs['locals'].update({'DNode': Node, 'paramdict': paramdict}) 116 | out = interp.parse("grammar", tree_str, **kwargs) 117 | #out.pprint() 118 | return out 119 | 120 | if __name__ == "__main__": 121 | import persistent_doc.document as pdocument 122 | #python.DNode = DNode 123 | #python.Node = DNode 124 | pdocument.default_doc = pdocument.Document(Node("group", id="root")) 125 | #out = interp.parse("grammar", inp, locals={"DNode": Node}) 126 | out = parse(inp4, locals=globals(), debug=True) 127 | #pdocument.default_doc.tree_root.append(out) 128 | out.pprint() 129 | 130 | -------------------------------------------------------------------------------- /xpybutil/event.py: -------------------------------------------------------------------------------- 1 | """ 2 | A module that can send client events to windows. It also allows 3 | registering callback functions to particular events. It can also 4 | run the main event loop. 5 | """ 6 | from collections import defaultdict, deque 7 | import struct 8 | import sys 9 | import traceback 10 | 11 | from xpybutil.compat import xcb_Exception, xproto 12 | 13 | from xpybutil import conn, root, util 14 | 15 | __queue = deque() 16 | __callbacks = defaultdict(list) 17 | EM = xproto.EventMask 18 | 19 | stringtype = str if sys.version_info[0] >= 3 else basestring 20 | 21 | class Event(object): 22 | KeyPressEvent = 2 23 | KeyReleaseEvent = 3 24 | ButtonPressEvent = 4 25 | ButtonReleaseEvent = 5 26 | MotionNotifyEvent = 6 27 | EnterNotifyEvent = 7 28 | LeaveNotifyEvent = 8 29 | FocusInEvent = 9 30 | FocusOutEvent = 10 31 | KeymapNotifyEvent = 11 32 | ExposeEvent = 12 33 | GraphicsExposureEvent = 13 34 | NoExposureEvent = 14 35 | VisibilityNotifyEvent = 15 36 | CreateNotifyEvent = 16 37 | DestroyNotifyEvent = 17 38 | UnmapNotifyEvent = 18 39 | MapNotifyEvent = 19 40 | MapRequestEvent = 20 41 | ReparentNotifyEvent = 21 42 | ConfigureNotifyEvent = 22 43 | ConfigureRequestEvent = 23 44 | GravityNotifyEvent = 24 45 | ResizeRequestEvent = 25 46 | CirculateNotifyEvent = 26 47 | CirculateRequestEvent = 27 48 | PropertyNotifyEvent = 28 49 | SelectionClearEvent = 29 50 | SelectionRequestEvent = 30 51 | SelectionNotifyEvent = 31 52 | ColormapNotifyEvent = 32 53 | ClientMessageEvent = 33 54 | MappingNotifyEvent = 34 55 | 56 | def replay_pointer(): 57 | conn.core.AllowEventsChecked(xproto.Allow.ReplayPointer, 58 | xproto.Time.CurrentTime).check() 59 | 60 | def send_event(destination, event_mask, event, propagate=False): 61 | return conn.core.SendEvent(propagate, destination, event_mask, event) 62 | 63 | def send_event_checked(destination, event_mask, event, propagate=False): 64 | return conn.core.SendEventChecked(propagate, destination, event_mask, event) 65 | 66 | def pack_client_message(window, message_type, *data): 67 | assert len(data) <= 5 68 | 69 | if isinstance(message_type, stringtype): 70 | message_type = util.get_atom(message_type) 71 | 72 | data = list(data) 73 | data += [0] * (5 - len(data)) 74 | 75 | # Taken from 76 | # http://xcb.freedesktop.org/manual/structxcb__client__message__event__t.html 77 | return struct.pack('BBH7I', Event.ClientMessageEvent, 32, 0, window, 78 | message_type, *data) 79 | 80 | def root_send_client_event(window, message_type, *data): 81 | mask = EM.SubstructureNotify | EM.SubstructureRedirect 82 | packed = pack_client_message(window, message_type, *data) 83 | return send_event(root, mask, packed) 84 | 85 | def root_send_client_event_checked(window, message_type, *data): 86 | mask = EM.SubstructureNotify | EM.SubstructureRedirect 87 | packed = pack_client_message(window, message_type, *data) 88 | return send_event_checked(root, mask, packed) 89 | 90 | def is_connected(event_name, window, callback): 91 | member = '%sEvent' % event_name 92 | assert hasattr(xproto, member) 93 | 94 | key = (getattr(xproto, member), window) 95 | return key in __callbacks and callback in __callbacks[key] 96 | 97 | def connect(event_name, window, callback): 98 | member = '%sEvent' % event_name 99 | assert hasattr(xproto, member) 100 | 101 | key = (getattr(xproto, member), window) 102 | __callbacks[key].append(callback) 103 | 104 | def disconnect(event_name, window): 105 | member = '%sEvent' % event_name 106 | assert hasattr(xproto, member) 107 | 108 | key = (getattr(xproto, member), window) 109 | if key in __callbacks: 110 | del __callbacks[key] 111 | 112 | def read(block=False): 113 | if block: 114 | e = conn.wait_for_event() 115 | __queue.appendleft(e) 116 | 117 | while True: 118 | e = conn.poll_for_event() 119 | 120 | if not e: 121 | break 122 | 123 | __queue.appendleft(e) 124 | 125 | def main(): 126 | try: 127 | while True: 128 | read(block=True) 129 | for e in queue(): 130 | w = None 131 | if isinstance(e, xproto.MappingNotifyEvent): 132 | w = None 133 | elif isinstance(e, xproto.MapRequestEvent): 134 | # Force all MapRequestEvents to go to the root window so 135 | # a window manager using xpybutil can get them. 136 | w = root 137 | elif hasattr(e, 'window'): 138 | w = e.window 139 | elif hasattr(e, 'event'): 140 | w = e.event 141 | elif hasattr(e, 'owner'): 142 | w = e.owner 143 | elif hasattr(e, 'requestor'): 144 | w = e.requestor 145 | 146 | key = (e.__class__, w) 147 | for cb in __callbacks.get(key, []): 148 | cb(e) 149 | except xcb_Exception: 150 | traceback.print_exc() 151 | sys.exit(1) 152 | 153 | def queue(): 154 | while len(__queue): 155 | yield __queue.pop() 156 | 157 | def peek(): 158 | return list(__queue) 159 | 160 | -------------------------------------------------------------------------------- /gui_lang.py: -------------------------------------------------------------------------------- 1 | from pymetaterp.util import simple_wrap_tree 2 | from pymetaterp import boot_tree, boot_stackless as boot, boot_grammar 3 | # Otherwise creates strange inheritance bug when tree_lang is imported... 4 | from pymetaterp import python 5 | from pymetaterp.boot_stackless import Eval, Frame, MatchError 6 | from pdb import set_trace as bp 7 | 8 | # apply! = ('\t'|' ')* {name ('(' {balanced=args} ')')?} 9 | gui_grammar = r""" 10 | grammar = {rule*} spaces 11 | rule = spaces {name=rule_name '!'?=flags and=args ("=" {or})} 12 | name = (letter | '_') (letter | digit | '_')* 13 | 14 | or = and ("|" {and})* 15 | and = bound* 16 | bound = quantified ('=' {name=inline})? 17 | quantified = not (('*' | '+' | '?')=quantifier)? 18 | not = "~" {expr=negation} | expr 19 | expr = call | apply | parenthesis 20 | 21 | call! = indentation? {('!'|'?')?=type name ('(' {balanced=args} ')')?} 22 | apply! = indentation? '@' {name ('(' {balanced=args} ')')?} 23 | parenthesis = "(" {or} ")" 24 | escaped_char! = '\\' {'n'|'r'|'t'|'b'|'f'|'"'|'\''|'\\'} 25 | balanced = (escaped_char | '(' balanced ')' | ~')' anything)* 26 | 27 | comment = '#' (~'\n' anything)* 28 | hspace = ' ' | '\t' | comment 29 | indentation = (hspace* ('\r' '\n' | '\r' | '\n'))* hspace+ 30 | space = '\n' | '\r' | hspace 31 | """ 32 | 33 | inp = """ 34 | text = key_press(Return) 35 | (~key_press(Return) key_press !add_letter(editor.text))* 36 | !run_text(editor.text) !clear 37 | grammar = (@text | ())* 38 | """ 39 | 40 | class Wait: 41 | pass 42 | 43 | def pop(input): 44 | input[1] += 1 45 | try: 46 | return input[0][input[1]] 47 | except IndexError: 48 | input[1] -= 1 49 | return Wait 50 | 51 | class Interpreter(boot.Interpreter): 52 | def match(self, root, input=None, pos=-1, scope=None): 53 | """ >>> g.match(g.rules['grammar'][-1], "x='y'") """ 54 | self.input = [input, pos] 55 | self.stack = [Frame(root, self.input)] 56 | self.join_str = False 57 | self.scope = scope 58 | self.memoizer = {} 59 | #return self.match_loop(True) 60 | 61 | def match_loop(self, new): 62 | output = self.new_step() if new else self.next_step() 63 | while output is not Wait: 64 | new = output is Eval 65 | if output is Eval: 66 | root = self.stack[-1].calls[len(self.stack[-1].outputs)] 67 | self.stack.append(Frame(root, self.input)) 68 | output = self.new_step() 69 | else: 70 | self.stack.pop() 71 | if not self.stack: 72 | return True, output 73 | #print len(self.stack)*" ", "returned", output 74 | self.stack[-1].outputs.append(output) 75 | output = self.next_step() 76 | return False, new 77 | 78 | def new_step(self): 79 | root = self.stack[-1].root 80 | name = root.name 81 | calls = self.stack[-1].calls 82 | if name == "call": 83 | if len(root[0]) == 0: 84 | event = pop(self.input) 85 | if event is Wait: 86 | return Wait 87 | args = root[2] if len(root) > 2 else [] 88 | if not eval(root[1], self.scope)(event, *args): 89 | return MatchError("Not exactly %s" % (root[1])) 90 | elif root[0][0] == '!': 91 | # Should return None instead? 92 | args = root[2] if len(root) > 2 else [] 93 | return eval(root[1], self.scope)(*args) 94 | elif root[0][0] == '?': 95 | # Should return None instead? 96 | args = root[2] if len(root) > 2 else [] 97 | if not eval(root[1], self.scope)(*args): 98 | return MatchError("Predicate %s is false" % root[1]) 99 | else: 100 | raise Exception() 101 | else: 102 | return boot.Interpreter.new_step(self) 103 | 104 | def interpreter(inp): 105 | i1 = boot.Interpreter(simple_wrap_tree(boot_tree.tree)) 106 | match_tree1 = i1.match(i1.rules['grammar'][-1], gui_grammar + boot_grammar.extra) 107 | i2 = boot.Interpreter(match_tree1) 108 | match_tree2 = i2.match(i2.rules['grammar'][-1], inp) 109 | return Interpreter(match_tree2) 110 | 111 | if __name__ == "__main__": 112 | from wrap_event import Event 113 | 114 | interp = interpreter(inp) 115 | events = [] 116 | interp.match(interp.rules['grammar'][-1], events) 117 | print interp.stack 118 | new = True 119 | finished, new = interp.match_loop(new) 120 | print finished, new 121 | finished, new = interp.match_loop(new) 122 | print finished, new 123 | 124 | event = Event() 125 | event.__dict__.update({"type": "key_press", 126 | "key_name": "Return"}) 127 | events.append(event) 128 | 129 | def key_press(event, key_name=None): 130 | return event.type == Event.key_press and\ 131 | (key_name is None or event.key_name == key_name) 132 | 133 | finished, new = interp.match_loop(new) 134 | print finished, new 135 | 136 | event = Event() 137 | event.__dict__.update({"type": "key_press", 138 | "key_name": "h"}) 139 | events.append(event) 140 | 141 | def add_letter(*args): 142 | print "adding", args 143 | 144 | finished, new = interp.match_loop(new) 145 | print finished, new 146 | 147 | event = Event() 148 | event.__dict__.update({"type": "key_press", 149 | "key_name": "Return"}) 150 | events.append(event) 151 | 152 | def run_text(*args): 153 | print "running", args 154 | 155 | def clear(): 156 | print "clear" 157 | 158 | finished, new = interp.match_loop(new) 159 | print finished, new 160 | 161 | #finished, self.new = interp.match_loop(self.new) 162 | #if finished: 163 | # return new 164 | -------------------------------------------------------------------------------- /xcb_doc.py: -------------------------------------------------------------------------------- 1 | import cairocffi as cairo 2 | import xcffib as xcb 3 | from xcffib.xproto import GC, CW, EventMask, WindowClass, ExposeEvent, ButtonPressEvent 4 | import numpy 5 | from const import default_get, identity, transformed 6 | import math 7 | from draw import flatten 8 | import time 9 | from collections import OrderedDict 10 | 11 | class Surface: 12 | def __init__(self, width, height): 13 | self.width = width 14 | self.height = height 15 | self.connection = xcb.connect() 16 | self.xsetup = self.connection.get_setup() 17 | self.window = self.connection.generate_id() 18 | self.pixmap = self.connection.generate_id() 19 | self.gc = self.connection.generate_id() 20 | events = [self.xsetup.roots[0].white_pixel, 21 | EventMask.ButtonPress | EventMask.ButtonRelease | EventMask.EnterWindow | EventMask.LeaveWindow | EventMask.Exposure | EventMask.PointerMotion | EventMask.ButtonMotion | EventMask.KeyPress | EventMask.KeyRelease] 22 | self.connection.core.CreateWindow(self.xsetup.roots[0].root_depth, 23 | self.window, 24 | # Parent is the root window 25 | self.xsetup.roots[0].root, 26 | 0, 0, self.width, self.height, 27 | 0, WindowClass.InputOutput, 28 | self.xsetup.roots[0].root_visual, 29 | CW.BackPixel | CW.EventMask, 30 | events) 31 | 32 | self.connection.core.CreatePixmap(self.xsetup.roots[0].root_depth, 33 | self.pixmap, 34 | self.xsetup.roots[0].root, 35 | self.width, 36 | self.height) 37 | 38 | self.connection.core.CreateGC(self.gc, 39 | self.xsetup.roots[0].root, 40 | GC.Foreground | GC.Background, 41 | [self.xsetup.roots[0].black_pixel, 42 | self.xsetup.roots[0].white_pixel]) 43 | 44 | self.surface = cairo.XCBSurface (self.connection, 45 | self.pixmap, 46 | self.xsetup.roots[0].allowed_depths[0].visuals[0], 47 | self.width, 48 | self.height) 49 | self.context = cairo.Context(self.surface) 50 | self.surfaces = {"screen":self.surface} 51 | self.contexts = {"screen":self.context} 52 | self.layers = OrderedDict() #Layer roots 53 | self.lastupdate = {} 54 | self.lastdrawn = {} 55 | 56 | def addlayer(self, doc, root_id): 57 | layer = root_id 58 | self.surfaces[layer] = self.surface.create_similar(cairo.CONTENT_COLOR_ALPHA, self.width, self.height) 59 | self.contexts[layer] = cairo.Context(self.surfaces[layer]) 60 | #self.contexts[layer].set_operator(cairo.OPERATOR_ONTO) 61 | self.contexts[layer].set_operator(cairo.OPERATOR_SOURCE) 62 | self.contexts[layer].set_antialias(cairo.ANTIALIAS_DEFAULT) 63 | self.layers[layer] = (doc, root_id) 64 | self.clear(layer) 65 | 66 | def clear(self, layer = "drawing"): 67 | if layer == "drawing": 68 | self.contexts[layer].set_source_rgb (1, 1, 1) 69 | else: 70 | self.contexts[layer].set_source_rgba (1, 1, 1, 0) 71 | self.contexts[layer].set_operator(cairo.OPERATOR_SOURCE) 72 | self.contexts[layer].paint() 73 | self.contexts[layer].set_operator(cairo.OPERATOR_OVER) 74 | #self.context.scale(self.width, self.height) 75 | 76 | def show(self): 77 | self.connection.core.MapWindow(self.window) 78 | self.connection.flush() 79 | 80 | def draw(self, layer = "drawing", root = None): 81 | if root is None: 82 | doc, root_id = self.layers[layer] 83 | root = doc[root_id] 84 | context = self.contexts[layer] 85 | for func, args in flatten(root): 86 | if func == "stroke_and_fill": 87 | opacity = default_get(args, "opacity") 88 | if default_get(args, "fill_color"): 89 | context.set_source_rgba(*default_get(args, "fill_color"), alpha=opacity) 90 | if args.get("stroke_color") is not None: 91 | context.fill_preserve() 92 | else: 93 | context.fill() 94 | if default_get(args, "stroke_color"): 95 | context.set_line_width(default_get(args, "line_width")) 96 | context.set_source_rgba(*default_get(args, "stroke_color"), alpha=opacity) 97 | context.set_dash(*default_get(args, "dash")) 98 | context.stroke() 99 | elif func == "text": 100 | if args["text"] is None: 101 | continue 102 | opacity = default_get(args, "opacity") 103 | context.set_source_rgba(*default_get(args, "stroke_color"), 104 | alpha=opacity) 105 | context.move_to(*args["botleft"]) 106 | context.set_font_size(1.5 * args["font_size"]) 107 | if args.get("font_face"): 108 | context.select_font_face(args["font_face"], 109 | cairo.FONT_SLANT_NORMAL, 110 | cairo.FONT_WEIGHT_NORMAL) 111 | else: 112 | context.set_font_face(None) 113 | if numpy.array_equal(args["transform"], identity): 114 | context.show_text(unicode(args["text"])) 115 | else: 116 | context.save() 117 | matrix = cairo.Matrix(*args["transform"].T[:,:2].flatten()) 118 | context.transform(matrix) 119 | context.show_text(unicode(args["text"])) 120 | context.restore() 121 | elif func == "image": 122 | # PNG only for the moment 123 | img = cairo.ImageSurface.create_from_png(args["filename"]) 124 | if numpy.array_equal(args["transform"], identity): 125 | context.set_source_surface(img, *args["topleft"]) 126 | context.paint() 127 | else: 128 | context.save() 129 | matrix = cairo.Matrix(*args["transform"].T[:,:2].flatten()) 130 | context.transform(matrix) 131 | context.set_source_surface(img, *args["topleft"]) 132 | context.paint() 133 | context.restore() 134 | elif func == "group": 135 | pass 136 | elif func in ["begin_region", "end_region"]: 137 | pass 138 | elif func == "arc": 139 | flat = list(args["center"]) + [args["radius"]] + list(args["angle"]) 140 | context.arc(*flat) 141 | else: 142 | getattr(context, func)(*args) 143 | 144 | def redraw(self, layer): 145 | self.clear(layer) 146 | self.draw(layer) 147 | self.expose() 148 | 149 | def update_all(self): 150 | drawn = False 151 | for layer_id in self.contexts: 152 | if layer_id == "screen": 153 | continue 154 | if True: # self.lastupdate.get(layer_id, 0) > self.lastdrawn.get(layer_id, -1): 155 | self.clear(layer_id) 156 | self.draw(layer_id) 157 | self.lastdrawn[layer_id] = time.time() 158 | drawn = True 159 | if drawn: 160 | self.expose() 161 | return drawn 162 | 163 | def update_layer(self, layer_id): 164 | self.lastupdate[layer_id] = time.time() 165 | 166 | def expose(self): 167 | self.context.set_source_rgb (1, 1, 1) 168 | self.context.paint() 169 | for layer in ["drawing", "ui"]: 170 | self.context.set_source_surface(self.surfaces[layer]) 171 | self.context.paint() 172 | self.connection.core.CopyArea(self.pixmap, self.window, self.gc, 0, 0, 0, 0, self.width, self.height) 173 | self.connection.flush() 174 | 175 | def poll(self): 176 | return self.connection.poll_for_event() 177 | -------------------------------------------------------------------------------- /draw.py: -------------------------------------------------------------------------------- 1 | from compgeo import distance2, norm2, distance_to_arc, point_in_closed_path 2 | import numpy 3 | from node import Node 4 | from const import default_get, identity, transformed 5 | import time 6 | 7 | def flatten_seg(root, style = {}, transform = identity): 8 | if root.name in ["line", "curve", "arc"]: 9 | if root.name in "line": 10 | yield ("line_to", transformed(root["end"], transform)) 11 | elif root.name == "curve": 12 | yield ("curve_to", transformed(root["start_control"], transform) +\ 13 | transformed(root["end_control"], transform) +\ 14 | transformed(root["end"], transform)) 15 | elif root.name == "arc": 16 | center = transformed(root["center"], transform) 17 | yield ("arc", {"center": center, 18 | "radius": root["radius"], 19 | "angle": default_get(root, "angle"), 20 | "style": style}) 21 | 22 | _cache = {} 23 | def flatten(root, style={}, transform=identity, skip_transform=False): 24 | """ Cached version of _flatten. """ 25 | if True or not (root["id"] in _cache and _cache[root["id"]][0] >= root.lastchange and\ 26 | style == _cache[root["id"]][1] and (transform == _cache[root["id"]][2]).all()): 27 | _cache[root["id"]] = (time.time(), style.copy(), transform.copy(), 28 | list(_flatten(root, style, transform, skip_transform))) 29 | return _cache[root["id"]][3] 30 | 31 | def _flatten(root, style={}, transform=identity, skip_transform=False): 32 | """ Flatten tree into drawing commands.""" 33 | assert(root.doc is not None) 34 | if not default_get(root, "visible"): 35 | return 36 | style = style.copy() 37 | for key in ["line_width", "stroke_color", "fill_color", "dash", 38 | "skip_points"]: 39 | if key in root: 40 | style[key] = root[key] 41 | if "opacity" in root: 42 | style["opacity"] = default_get(style, "opacity") * root["opacity"] 43 | if not skip_transform: 44 | transform = transform.dot(root.transform) 45 | if root.name in ["line", "curve", "arc", "path"]: 46 | if not default_get(root, "visible"): 47 | return 48 | children = root if root.name == "path" else [root] 49 | border = [] 50 | if children: 51 | yield ("begin_region", ()) 52 | if children[0].name == "arc": 53 | yield ("move_to", transformed(children[0]["center"], transform)) 54 | else: 55 | yield ("move_to", transformed(children[0]["start"], transform)) 56 | for child in children: 57 | for segment in flatten_seg(child, style = style, 58 | transform = transform): 59 | yield segment 60 | yield ("end_region", ()) 61 | yield ("stroke_and_fill", style) 62 | if not default_get(style, "skip_points"): 63 | for child in children: 64 | for grandchild in child: 65 | for elem in flatten(grandchild, style = style, 66 | transform = transform): 67 | yield elem 68 | elif root.name in ["clip"]: 69 | # Clipping needs to specify both the clipped region and 70 | # the clipped elements! 71 | # Maybe only allow clipped rectangles at first? 72 | yield ("begin_clipped", ()) 73 | for child in root["clip_path"]: 74 | for segment in flatten_seg(child, style = style, 75 | transform = transform): 76 | yield segment 77 | yield ("end_clip", ()) 78 | yield ("end_clipped", ()) 79 | elif root.name == "point": 80 | if default_get(style, "skip_points"): 81 | return 82 | matrix = numpy.array([[1, 0, root["value"][0]], 83 | [0, 1, root["value"][1]], 84 | [0, 0, 1]]) 85 | transform = transform.dot(matrix) 86 | for elem in flatten(root.doc[default_get(root, "icon")], 87 | transform=transform, style={"skip_points":True}): 88 | yield elem 89 | elif root.name == "text": 90 | if "value" in root: 91 | value = root["value"] 92 | elif "ref_id" in root: 93 | value = str(root.doc[root["ref_id"]][root["ref_param"]]) 94 | else: 95 | raise Exception('Text node with no value or ref_id: %s' % root) 96 | yield ("text", {"text": value, 97 | "transform": transform, 98 | "font_size": default_get(root, "font_size"), 99 | "font_face": default_get(root, "font_face"), 100 | "stroke_color": default_get(style, "stroke_color"), 101 | "botleft": transformed(root["botleft"], transform), 102 | "opacity": default_get(style, "opacity")}) 103 | elif root.name == "image": 104 | yield ("image", {"filename": root['filename'], 105 | "topleft": default_get(root, "topleft")['value'], 106 | "transform": transform}) 107 | elif root.name == "group": 108 | yield ("group", ()) 109 | if root.name in ["group", "text", "image"]: 110 | #if default_get(root, "visible"): 111 | for child in root: 112 | for elem in flatten(child, style = style, 113 | transform = transform): 114 | yield elem 115 | if default_get(root, "render"): 116 | for child in root["render"]: 117 | for elem in flatten(child, style = style, 118 | transform = transform): 119 | yield elem 120 | 121 | def collide(root, xy, style = {}, transform=identity, tolerance=3, skip=False): 122 | style = style.copy() 123 | for key in ["line_width", "stroke_color", "fill_color", "dash"]: 124 | if key in root: 125 | style[key] = root[key] 126 | if root.name in ["path", "group"]: 127 | if not skip: 128 | transform = transform.dot(root.transform) 129 | if root.name == "path" and style.get("fill_color"): 130 | # print "Testing fill" 131 | path = [(transformed(seg["start"], transform), 132 | transformed(seg["end"], transform)) 133 | for seg in root if seg.name == 'line'] 134 | # print "Input", xy, path 135 | if point_in_closed_path(xy, path): 136 | return True 137 | return any(collide(child, xy, style, transform, tolerance) 138 | for child in root) 139 | elif root.name == "line": 140 | line = (transformed(root["start"], transform), 141 | transformed(root["end"], transform)) 142 | xy = numpy.array(xy) 143 | dist2 = default_get(style, "line_width") + tolerance**2 144 | #print "dist", distance2(transform[:2,:2] * xy, line), dist2 145 | return distance2(xy, line) < dist2 146 | elif root.name == "arc": 147 | center=transformed(root["center"], transform) 148 | return distance_to_arc(xy, center, root["radius"], default_get(root, "angle")) < tolerance 149 | elif root.name == "point": 150 | point = transformed(root, transform) 151 | xy = numpy.array(xy) 152 | return norm2(xy - point) < tolerance**2 153 | elif root.name == "text": 154 | top_left, bottom_right = root.bbox(transform) 155 | def contains(top_left, bottom_right, xy): 156 | if numpy.array_equal((top_left, bottom_right), (None, None)): 157 | return False 158 | return all(top_left <= xy) and all(xy <= bottom_right) 159 | #return numpy.all(top_left <= xy <= bottom_right) 160 | tol = (tolerance, tolerance) 161 | return contains(top_left - tol, 162 | bottom_right + tol, 163 | xy) 164 | else: 165 | # Not yet implemented 166 | return False 167 | 168 | def simplify_transform(node, transform = identity): 169 | if node.name == "point": 170 | node["value"] = transformed(node, transform) 171 | if "transforms" in node: 172 | #node["transforms"] = TransformDict(node=node) 173 | node.L["transforms"].clear() 174 | #node["transforms"].replace({}) 175 | else:#if node.name in ["path", "group", ]: 176 | transform = transform.dot(node.transform) 177 | if "transforms" in node: 178 | node.L["transforms"].clear() 179 | for child in node: 180 | simplify_transform(child, transform) 181 | -------------------------------------------------------------------------------- /xpybutil/util.py: -------------------------------------------------------------------------------- 1 | """ 2 | A vast assortment of utility functions. The ones of interest to 3 | you are probably 'get_atom' and 'get_atom_name'. The rest are 4 | heavily used throughout the rest of xpybutil. 5 | """ 6 | import struct 7 | import sys 8 | 9 | from xpybutil.compat import xproto 10 | 11 | from xpybutil import conn 12 | 13 | __atom_cache = {} 14 | __atom_nm_cache = {} 15 | 16 | class Cookie(object): 17 | """ 18 | The base Cookie class. The role of a cookie is to serve as an intermediary 19 | between you and the X server. After calling one of the many functions 20 | in the ewmh or icccm modules, the X server is typically not contacted 21 | immediately. Usually, you'll need to call the 'check()' or 'reply()' 22 | methods on the cookie returned by one of the functions in the ewmh or 23 | icccm modules. (Alternatively, you could flush the X buffer using 24 | ``conn_obj.flush()``.) 25 | """ 26 | def __init__(self, cookie): 27 | self.cookie = cookie 28 | 29 | def check(self): 30 | return self.cookie.check() 31 | 32 | class PropertyCookie(Cookie): 33 | """ 34 | A regular property cookie that uses 'get_property_value' to return a nicer 35 | version to you. (Instead of raw X data.) 36 | """ 37 | def reply(self): 38 | return get_property_value(self.cookie.reply()) 39 | 40 | class PropertyCookieSingle(Cookie): 41 | """ 42 | A cookie that should only be used when one is guaranteed a single logical 43 | result. Namely, 'get_property_value' will be stupid and return a single 44 | list. This class checks for that, and takes the head of that list. 45 | """ 46 | def reply(self): 47 | ret = get_property_value(self.cookie.reply()) 48 | 49 | if ret and isinstance(ret, list) and len(ret) == 1: 50 | return ret[0] 51 | return ret 52 | 53 | class AtomCookie(Cookie): 54 | """ 55 | Pulls the ATOM identifier out of the reply object. 56 | """ 57 | def reply(self): 58 | return self.cookie.reply().atom 59 | 60 | class AtomNameCookie(Cookie): 61 | """ 62 | Converts the null terminated list of characters (that represents an 63 | ATOM name) to a string. 64 | """ 65 | def reply(self): 66 | return bytes(self.cookie.reply().name.buf()).decode('utf-8') 67 | 68 | def get_property_value(property_reply): 69 | """ 70 | A function that takes a property reply object, and turns its value into 71 | something nice for us. 72 | 73 | In particular, if the format of the reply is '8', then assume that it 74 | is a string. Moreover, it could be a list of strings that are null 75 | terminated. In this case, return a list of Python strings. Otherwise, just 76 | convert it to a string and remove the null terminator if it exists. 77 | 78 | Otherwise, the format must be a list of integers that has to be unpacked. 79 | Sometimes, these integers are ATOM identifiers, so it is useful to map 80 | 'get_atom_name' over this list if that's the case. 81 | 82 | :param property_reply: An object returned by a cookie's "reply" method. 83 | :type property_reply: xcb.xproto.GetPropertyReply 84 | :return: Either a string, a list of strings or a list of integers depending 85 | upon the format of the property reply. 86 | """ 87 | if property_reply.format == 8: 88 | ret = bytes(property_reply.value.buf()).split(b'\0') 89 | if ret[-1] == '': ret.pop() 90 | ret = [ x.decode('utf-8') for x in ret ] 91 | return ret[0] if len(ret) == 1 else ret 92 | elif property_reply.format in (16, 32): 93 | return list(struct.unpack('I' * property_reply.value_len, 94 | property_reply.value.buf())) 95 | 96 | return None 97 | 98 | def get_property(window, atom): 99 | """ 100 | Abstracts the process of issuing a GetProperty request. 101 | 102 | You'll typically want to call the ``reply`` method on the return value of 103 | this function, and pass that result to 104 | 'get_property_value' so that the data is nicely formatted. 105 | 106 | :param window: A window identifier. 107 | :type window: int 108 | :param atom: An atom identifier. 109 | :type atom: int OR str 110 | :rtype: xcb.xproto.GetPropertyCookie 111 | """ 112 | stringtype = str if sys.version_info[0] >= 3 else basestring 113 | if isinstance(atom, stringtype): 114 | atom = get_atom(atom) 115 | return conn.core.GetProperty(False, window, atom, 116 | xproto.GetPropertyType.Any, 0, 117 | 2 ** 32 - 1) 118 | 119 | def get_property_unchecked(window, atom): 120 | """ 121 | Abstracts the process of issuing a GetPropertyUnchecked request. 122 | 123 | You'll typically want to call the ``reply`` method on the return value of 124 | this function, and pass that result to 125 | 'get_property_value' so that the data is nicely formatted. 126 | 127 | :param window: A window identifier. 128 | :type window: int 129 | :param atom: An atom identifier. 130 | :type atom: int OR str 131 | :rtype: xcb.xproto.GetPropertyCookie 132 | """ 133 | stringtype = str if sys.version_info[0] >= 3 else basestring 134 | if isinstance(atom, stringtype): 135 | atom = get_atom(atom) 136 | return conn.core.GetPropertyUnchecked(False, window, atom, 137 | xproto.GetPropertyType.Any, 0, 138 | 2 ** 32 - 1) 139 | 140 | def build_atom_cache(atoms): 141 | """ 142 | Quickly builds a cache of ATOM names to ATOM identifiers (and the reverse). 143 | You'll only need to use this function if you're using atoms not defined in 144 | the ewmh, icccm or motif modules. (Otherwise, those modules will build this 145 | cache for you.) 146 | 147 | The 'get_atom' and 'get_atom_name' function automatically use this cache. 148 | 149 | :param atoms: A list of atom names. 150 | :rtype: void 151 | """ 152 | global __atom_cache, __atom_nm_cache 153 | 154 | if conn is None: 155 | return 156 | 157 | for atom in atoms: 158 | __atom_cache[atom] = __get_atom_cookie(atom, only_if_exists=False) 159 | for atom in __atom_cache: 160 | if isinstance(__atom_cache[atom], AtomCookie): 161 | __atom_cache[atom] = __atom_cache[atom].reply() 162 | 163 | __atom_nm_cache = dict((v, k) for k, v in __atom_cache.items()) 164 | 165 | def get_atom(atom_name, only_if_exists=False): 166 | """ 167 | Queries the X server for an ATOM identifier using a name. If we've already 168 | cached the identifier, then we don't contact the X server. 169 | 170 | If the identifier is not cached, it is added to the cache. 171 | 172 | If 'only_if_exists' is false, then the atom is created if it does not exist 173 | already. 174 | 175 | :param atom_name: An atom name. 176 | :type atom_name: str 177 | :param only_if_exists: If false, the atom is created if it didn't exist. 178 | :type only_if_exists: bool 179 | :return: ATOM identifier. 180 | :rtype: int 181 | """ 182 | global __atom_cache 183 | 184 | a = __atom_cache.setdefault(atom_name, 185 | __get_atom_cookie(atom_name, 186 | only_if_exists).reply()) 187 | if isinstance(a, AtomCookie): 188 | a = a.reply() 189 | 190 | return a 191 | 192 | def get_atom_name(atom): 193 | """ 194 | Queries the X server for an ATOM name using the specified identifier. 195 | If we've already cached the name, then we don't contact the X server. 196 | 197 | If the atom name is not cached, it is added to the cache. 198 | 199 | :param atom: An atom identifier. 200 | :type atom: int 201 | :return: ATOM name. 202 | :rtype: str 203 | """ 204 | global __atom_nm_cache 205 | 206 | a = __atom_nm_cache.setdefault(atom, __get_atom_name_cookie(atom).reply()) 207 | 208 | if isinstance(a, AtomNameCookie): 209 | a = a.reply() 210 | 211 | return a 212 | 213 | def __get_atom_cookie(atom_name, only_if_exists=False): 214 | """ 215 | Private function that issues the xpyb call to intern an atom. 216 | 217 | :type atom_name: str 218 | :type only_if_exists: bool 219 | :rtype: xcb.xproto.InternAtomCookie 220 | """ 221 | atom_bytes = atom_name.encode('ascii') 222 | atom = conn.core.InternAtomUnchecked(only_if_exists, len(atom_bytes), 223 | atom_bytes) 224 | return AtomCookie(atom) 225 | 226 | def __get_atom_name_cookie(atom): 227 | """ 228 | Private function that issues the xpyb call to get an ATOM identifier's name. 229 | 230 | :type atom: int 231 | :rtype: xcb.xproto.GetAtomNameCookie 232 | """ 233 | return AtomNameCookie(conn.core.GetAtomNameUnchecked(atom)) 234 | 235 | -------------------------------------------------------------------------------- /node.py: -------------------------------------------------------------------------------- 1 | from persistent_doc.document import FrozenNode, map_type, get_eval, Ex, Expr, wrap_children 2 | from persistent_doc.transform import TransformDict 3 | import persistent_doc.document as pdocument 4 | from const import default_get, identity, transformed, get_matrix 5 | from compgeo import arc_endpoints, extents 6 | from collections import OrderedDict 7 | import numpy 8 | from pdb import set_trace as bp 9 | 10 | def _repr(value): 11 | from pyrsistent import PMap 12 | if isinstance(value, TransformDict): 13 | return dict(value.dict_).__repr__() 14 | elif isinstance(value, Expr): 15 | if value.expr is not None and value.expr.calc == "reeval": 16 | return "exr(%r)" % value.expr 17 | else: 18 | if value.expr is None: 19 | return repr(value.cache) 20 | else: 21 | return "exc(%r)" % value.expr 22 | elif isinstance(value, Ex): 23 | if value is None: 24 | return value 25 | return "exc(%r)" % value 26 | elif isinstance(value, numpy.ndarray) and value.shape == (2,): 27 | return "P" + str(tuple(value)) 28 | else: 29 | return repr(value) 30 | 31 | def gen_id(doc, old_id): 32 | i = 0 33 | while True: 34 | new_id = "%s_copy%s" % (old_id, i) 35 | if new_id not in doc.m: 36 | return new_id 37 | i += 1 38 | 39 | class PointRepr: 40 | def __init__(self, array): 41 | self.array = array 42 | 43 | def __repr__(self): 44 | return "P(%s)" % ", ".join(map(str, self.array)) 45 | 46 | node_num = 0 47 | class Node(FrozenNode): 48 | def __new__(cls, name=None, params=None, children=(), doc=None, parent=None, _parent=None, **kwargs): 49 | global node_num 50 | doc = doc if doc is not None else pdocument.default_doc 51 | params = kwargs if params is None else params 52 | children = list(children) 53 | if type(params) == map_type: 54 | params = params.evolver() 55 | if "id" not in params: 56 | params["id"] = "n_%s" % node_num 57 | node_num += 1 58 | if "child_id" in params and params["child_id"] is None: 59 | del params["child_id"] 60 | for key, value in kwargs.items(): 61 | if key.startswith("p_"): 62 | children.append(Node("point", child_id=key[2:], value=value)) 63 | del kwargs[key] 64 | params[key[2:]] = Ex("`%s" % children[-1]["id"], "reeval") 65 | elif key.startswith("px_"): 66 | # bp() 67 | child_id = key[3:] 68 | children.append(Node("point", child_id=child_id, value=value)) 69 | del kwargs[key] 70 | params[child_id] = Ex("`%s" % children[-1]["id"], "on first read") 71 | elif key.startswith("r_"): 72 | raise Exception("ref nodes are depricated! Use expressions with a single id instead.") 73 | elif key == "transform": 74 | # Should not have both transform and transforms. 75 | params["transforms"] = TransformDict(dict_={"singleton":value}, node=params["id"], doc=doc) 76 | del kwargs["transform"] 77 | elif key == "transforms": 78 | if type(value) not in [map_type, Ex]: 79 | params["transforms"] = TransformDict(dict_=value, node=params["id"], doc=doc) 80 | if "transforms" not in params: 81 | params["transforms"] = TransformDict(node=params["id"], dict_={}, doc=doc) 82 | # Had problem where this overwrote value passed by param! 83 | # Need to know if "children" is the intended change or 84 | # params is the intended change! 85 | # Setting to "params overwrites children" for now. 86 | # NO, there are bigger problems with children not synced to the 87 | # new param values 88 | # Could use the latest one of the two if we had timestamps 89 | 90 | # General problem: This loop is called at modification instead of 91 | # only at initialization 92 | # Should decide on the semantics of appending a node with 93 | # child_id (or removing one with child_id) means for params 94 | # anyways. 95 | for child_id in children: 96 | child = get_eval(child_id) 97 | if "child_id" in child and child["child_id"] not in params: 98 | # Want the wrapped version of the child! 99 | # params[child["child_id"]] = child 100 | params[child["child_id"]] = wrap_children(params["id"], doc, [child])[0] 101 | if type(params).__name__ == '_Evolver': 102 | params = params.persistent() 103 | return FrozenNode.__new__(cls, name=name, params=params, 104 | children=children, doc=doc, parent=parent, 105 | _parent=_parent) 106 | 107 | def set(self, **kwargs): 108 | return FrozenNode.set(self, **kwargs) 109 | 110 | def __setitem__(self, key, value): 111 | #return self.set_path([key], value) 112 | return self.set_path(key.split("."), value) 113 | 114 | def change_id(self, new_id): 115 | # Should do something about selection and probably other rdepend 116 | index = self.parent.index(self) 117 | self["id"] = new_id 118 | self = self.doc[new_id] 119 | self.parent.set_child(index, self) 120 | #for child in self: 121 | # child.change(_parent=wrap3(new_id, doc)) 122 | 123 | def latest(self): 124 | return self.doc.get_node(self["id"]) 125 | 126 | @property 127 | def transform(self): 128 | transform = identity 129 | if "transforms" not in self.params: 130 | return identity 131 | else: 132 | for key in get_eval(self.params["transforms"]): 133 | matrix = get_matrix(self["transforms"][key]) 134 | transform = transform.dot(matrix) 135 | return transform 136 | 137 | @property 138 | def transforms(self): 139 | if self['id'] in self.doc: 140 | self = self.L 141 | if "transforms" not in self: 142 | self["transforms"] = TransformDict(node=self["id"], doc=self.doc) 143 | self = self.latest() 144 | return self["transforms"] 145 | 146 | def params_repr(self, exclude=(), exclude_empty=True, points=True, 147 | sep=", "): 148 | empty = [] 149 | if exclude_empty: 150 | #if self["id"].startswith("n_"): 151 | # empty.append("id") 152 | if not self.transforms: 153 | empty.append("transforms") 154 | 155 | items = list(self.params.items()) 156 | if points: 157 | for child in self: 158 | if "child_id" in child: 159 | empty.append(child["child_id"]) 160 | if child.name == "point": 161 | value = child.get_expr("value") 162 | value = PointRepr(value) if isinstance(value, numpy.ndarray) else value 163 | prefix = "px_" if child.parent.get_expr(child["child_id"]).expr.calc == "on first read" else "p_" 164 | items.append(("%s%s" % (prefix, child["child_id"]), value)) 165 | if child.name == "ref": 166 | items.append(("r_%s" % child["child_id"], 167 | child["ref_id"])) 168 | return sep.join("%s=%s" % (key, _repr(value)) for key, value in items 169 | if key not in exclude and key not in empty) 170 | 171 | def pprint(self, exclude=["transform_updated"]): 172 | for line in self.pprint_string(exclude=exclude): 173 | print line 174 | 175 | def pprint_string(self, indent = 0, exclude = ["transform_updated"]): 176 | params = self.params_repr(exclude, sep=" ") 177 | yield "%s%s: %s" % (indent*" ", self.name, params) 178 | for child in self: 179 | if not (child.name in ["point", "ref"] and "child_id" in child): 180 | for line in child.pprint_string(indent + 2, exclude): 181 | yield line 182 | 183 | def code(self, indent = 0, exclude = ["transform_updated"]): 184 | params = self.params_repr(exclude) 185 | if params: 186 | params = ", " + params 187 | s = '%sNode("%s"%s' % (indent*" ", self.name, params) 188 | children = [child for child in self 189 | if not (child.name in ["point", "ref"] and "child_id" in child)] 190 | if len(children): 191 | s += ", children = [\n" 192 | s += ",\n".join(child.code(indent + 2, exclude) 193 | for child in children) 194 | s += "]" 195 | s += ")" 196 | return s 197 | 198 | def combined_transform(self, stop=None): 199 | """ Cumulate all transforms on the path from parent of node to stop. 200 | Probably works poorly with refs.""" 201 | if stop is None: 202 | stop = self.doc.tree_root 203 | if self == stop: 204 | return identity 205 | else: 206 | return self.parent.combined_transform(stop).dot(self.parent.transform) 207 | 208 | def bbox(self, transform=identity, skip=False): 209 | """ Bound box. Includes self's transform.""" 210 | # Want some way to cache the answer for children? 211 | # Would need transforms to be applied after instead of before. 212 | # Could almost make this an expression 213 | 214 | # transformed() already applies the last transform to points 215 | if not skip and self.name != "point": 216 | transform = transform.dot(self.transform) 217 | if self.name in ["group", "path"]: 218 | boxes = [child.bbox(transform) 219 | for child in self] 220 | boxes = zip(*[box for box in boxes if not numpy.array_equal(box, (None, None))]) 221 | if not boxes: 222 | return (None, None) 223 | return (numpy.min(numpy.vstack(boxes[0]), 0), 224 | numpy.max(numpy.vstack(boxes[1]), 0)) 225 | elif self.name == "ref": 226 | return self.reference().bbox(transform, skip=True) 227 | elif self.name == "line": 228 | m = numpy.vstack([transformed(self["start"], transform), 229 | transformed(self["end"], transform)]) 230 | line_width = default_get(self, "line_width") 231 | return numpy.min(m, 0)-line_width, numpy.max(m, 0)+line_width 232 | elif self.name == "arc": 233 | end0, end1 = arc_endpoints(transformed(self["center"], transform), 234 | self["radius"], 235 | default_get(self, "angle")) 236 | m = numpy.vstack([end0, end1]) 237 | line_width = default_get(self, "line_width") 238 | return numpy.min(m, 0)-line_width, numpy.max(m, 0)+line_width 239 | elif self.name == "point": 240 | point = transformed(self, transform) 241 | return point, point 242 | elif self.name == "text": 243 | botleft = transformed(self["botleft"], transform) 244 | xy, wh, dxy = extents(unicode(self["value"]), 245 | default_get(self, "font_size"), 246 | default_get(self, "font_face")) 247 | botright = botleft + dxy 248 | topleft = botleft + xy 249 | return topleft, botright 250 | 251 | def dfs(self): 252 | visited = [(self, identity)] 253 | for node, transform in visited: 254 | yield node, transform 255 | transform = transform.dot(node.transform) 256 | visited.extend((child, transform) for child in node) 257 | 258 | def clone(self): 259 | new_id = "%s_copy" % self["id"] 260 | self.parent.append(self.set(params=self.params.set("id", new_id))) 261 | 262 | def deepcopy(self): 263 | params = dict(self.params.set('id', gen_id(self.doc, self["id"]))) 264 | if "transforms" in params: 265 | transforms = dict(params['transforms'].dict_) 266 | del params['transforms'] 267 | else: 268 | transforms = {} 269 | children = [] 270 | for child in self: 271 | children.append(child.deepcopy()) 272 | if "child_id" in child: 273 | #params = params.set(child["child_id"], Ex("`%s_copy" % child["id"])) 274 | params[child["child_id"]] = Ex("`%s" % children[-1]["id"]) 275 | return Node(self.name, params=params, children=children, 276 | transforms=transforms) 277 | -------------------------------------------------------------------------------- /docs/demo.py: -------------------------------------------------------------------------------- 1 | # This is a DIY demo. To follow along, 2 | # Start python flow_editor.py docs/demo.py 3 | # Read the lines below and follow the instructions in the 4 | # string comments 5 | 6 | from draw import collide, simplify_transform 7 | from const import rounded, identity, get_matrix 8 | import numpy 9 | import persistent.document as pdocument 10 | from persistent.document import Expr, Ex 11 | from time import time as cur_time 12 | 13 | pdocument.excluded = {"P": P, "cur_time": cur_time} 14 | 15 | input_callbacks = "" 16 | 17 | "# Hello world" 18 | 19 | """ 20 | Instructions: 21 | 1. Uncomment the line of code below 22 | 2. Save this file 23 | 3. Press ctrl-r to reload in guitktk's main window 24 | 4. See "hello world" appear 25 | 5. Recomment the line of code below 26 | 27 | Move down this document. Do the same every time you see 28 | commented blocks of code. 29 | """ 30 | 31 | # doc['drawing'].append(Node("text", value="hello world", p_botleft=P(100, 100))) 32 | 33 | 34 | 35 | 36 | 37 | 38 | "# Interpreter" 39 | 40 | """ 41 | Instructions: do the same as above: uncomment, save, reload, recomment 42 | Don't forget to always recomment! 43 | """ 44 | 45 | # doc['drawing'][-1].change_id("console") 46 | # doc["console"]["value"] = "" 47 | 48 | 49 | """ 50 | Instructions: Delete the triple single quote below and paste them 51 | below "Paste triple quotes here!" 52 | """ 53 | 54 | ''' 55 | input_callbacks = """ 56 | exec = key_press(Return) 57 | (~key_press(Return) (key_press !add_letter(console) | @anything))* 58 | key_press(Return) !run_text(console) !clear(console) 59 | grammar = ( @exec | @anything)* 60 | """ 61 | 62 | "## Add missing function calls" 63 | 64 | "### Core of add_letter is the last line" 65 | 66 | def add_letter(node_id=None): 67 | node_id = node_id if node_id is not None else doc["editor.focus"] 68 | if doc["editor.key_name"] == "BackSpace": 69 | doc[node_id + ".value"] = doc[node_id + ".value"][:-1] 70 | else: 71 | doc[node_id + ".value"] += doc["editor.key_char"] 72 | 73 | def run_text(node_id, param="value"): 74 | try: 75 | co = compile(doc[node_id][param], "", "single") 76 | exec co in globals() 77 | except: 78 | traceback.print_exc() 79 | 80 | def clear(node_id): 81 | doc[node_id]["value"] = "" 82 | 83 | "### Add keypress handling" 84 | 85 | def key_press(event, key_name=None): 86 | return event.type == Event.key_press and\ 87 | (key_name is None or event.key_name == key_name) 88 | 89 | "Paste triple quotes here! You'll be asked to move them again later." 90 | "And reload the main window (ctrl-r)" 91 | 92 | """ 93 | Instructions: After saving this file and reloading, the console should be enabled. 94 | 95 | To test it, in the main guitktk window, press enter and type 96 | 97 | doc['console.botleft.value'] = P(0, 100) 98 | 99 | (Backspace should work but there is otherwise no cursor.) 100 | 101 | Or you can try a simpler command like "print(1)". 102 | 103 | After you're done, move the triple quote to the next "Paste triple quotes here!" below. 104 | 105 | In the future always save this file and reload in the main window after moving triple quotes. 106 | """ 107 | 108 | 109 | 110 | 111 | 112 | 113 | "# Buttons" 114 | 115 | input_callbacks += """ 116 | button = mouse_press(1) ?run_button mouse_release(1) 117 | grammar = ( @exec | @button | @anything)* 118 | """ 119 | 120 | "## Add missing function calls" 121 | 122 | def run_button(): 123 | root = doc[doc["selection.root"]] 124 | xy = doc["editor.mouse_xy"] 125 | for child in reversed(root): 126 | if collide(child, xy): 127 | print "clicked on", child["id"] 128 | if "on_click" in child: 129 | run_text(child["id"], "on_click") 130 | return True 131 | return False 132 | 133 | "### Detect single button or any button click" 134 | 135 | def mouse_press(event, button=None): 136 | return event.type == Event.mouse_press and\ 137 | (button is None or event.button == int(button)) 138 | 139 | def mouse_release(event, button=None): 140 | return event.type == Event.mouse_release and\ 141 | (button is None or event.button == int(button)) 142 | 143 | # doc['drawing'].append(Node("text", id="button1", value="Click me!", p_botleft=P(10, 210))) 144 | # doc['button1.on_click'] = "doc['button1.value'] = 'Clicked!'" 145 | 146 | "Paste triple quotes here!" 147 | 148 | """ 149 | Instructions: 150 | After moving the single triple quotes to the previous line, uncomment the two commented line, save, reload. recomment the above. 151 | 152 | This should add "Click me!" to the document. Click it and it should turn to "Clicked!". 153 | 154 | Try adding other buttons by modifying the two commented lines (then uncomment, save, reload, recomment). 155 | 156 | When you're down, move the triple single quotes to the next "Paste triple quotes here!" below. 157 | """ 158 | 159 | 160 | 161 | "# Labels" 162 | 163 | input_callbacks += """ 164 | text = key_press(t) !create_text 165 | (~key_press(Return) (key_press !add_letter | @anything))* 166 | key_press(Return) 167 | grammar = (@exec | @button | @text | @anything)* 168 | """ 169 | 170 | def create_text(): 171 | doc["drawing"].append(Node("text", value="", 172 | p_botleft=doc["editor.mouse_xy"])) 173 | doc["editor.focus"] = doc["drawing"][-1]["id"] 174 | 175 | "Paste triple quotes here!" 176 | 177 | """ 178 | Instructions: 179 | [Text labels added!] 180 | 181 | Create text by pressing t. Then type and press enter when you're done. 182 | 183 | Create a few more text labels this way. 184 | 185 | When you're down, move the triple single quotes to the next "Paste triple quotes here!" below. 186 | """ 187 | 188 | 189 | 190 | 191 | 192 | "# Text input" 193 | 194 | input_callbacks += """ 195 | text = key_press(t) (?edit_text | !create_text) 196 | (~key_press(Return) (key_press !add_letter | @anything))* 197 | key_press(Return) 198 | """ 199 | 200 | def edit_text(): 201 | root = doc[doc["selection.root"]] 202 | for child, transform in root.dfs(): 203 | if child.name == "text" and\ 204 | collide(child, doc["editor.mouse_xy"], transform=transform, tolerance=8): 205 | doc["editor.focus"] = child["id"] 206 | return True 207 | return False 208 | 209 | "Paste triple quotes here!" 210 | 211 | """ 212 | Instructions: 213 | [Label/textfield editing added!] 214 | 215 | Press t over a label you added to the previous step. Press backspace a few times to delete some of it. Then type some new text and press enter when you're done. 216 | """ 217 | 218 | 219 | 220 | 221 | "# Buttons again" 222 | 223 | input_callbacks += """ 224 | text = key_press(t) (?edit_text | !create_text) 225 | (~key_press(Return) (key_press !add_letter | @anything))* 226 | key_press(Return) !finished_edit_text 227 | """ 228 | 229 | def finished_edit_text(): 230 | node = doc[doc["editor.focus"]] 231 | text = node["value"] 232 | if text.startswith("!"): 233 | node["on_click"] = text[1:] 234 | 235 | "Paste triple quotes here!" 236 | 237 | """ 238 | Instructions: 239 | [We got better buttons!] 240 | 241 | Create a label containing 242 | 243 | !doc['drawing'].pop() 244 | 245 | Create some more text labels with anything in them. 246 | 247 | Click on the "!doc['drawing'].pop()" and it should remove the newest of the other labels. Anything starting with "!" is now a button! 248 | """ 249 | 250 | 251 | 252 | 253 | 254 | "# Status bar" 255 | 256 | def finished_edit_text(): 257 | node = doc[doc["editor.focus"]] 258 | text = node["value"] 259 | if text.startswith("!"): 260 | node["on_click"] = text[1:] 261 | elif text.startswith("="): 262 | node["value"] = Ex(text[1:], calc="reeval") 263 | 264 | "Paste triple quotes here!" 265 | 266 | """ 267 | Instructions: 268 | 269 | Create a label with text 270 | 271 | =`editor.mouse_xy 272 | 273 | Create another one with 274 | 275 | =`editor.mouse_xy + P(100, 0) 276 | 277 | Move your mouse around the main window. 278 | """ 279 | 280 | "#### End" 281 | "#### Of" 282 | "#### This" 283 | "#### Demo" 284 | 285 | """ 286 | Instructions: 287 | 288 | You can try to move the triple quotes further along but explicit 289 | instructions are missing. 290 | """ 291 | 292 | 293 | 294 | "# Move points" 295 | 296 | input_callbacks += """ 297 | move_point = key_press(e) ?grab_point (~key_press(e) @anything)* key_press(e) !drop_point 298 | grammar = (@exec | @button | @text | @move_point | @anything)* 299 | """ 300 | 301 | 302 | def grab_point(): 303 | root = doc[doc["selection"]["root"]] 304 | for child, transform in root.dfs(): 305 | if child.name == "point" and\ 306 | collide(child, doc["editor.mouse_xy"], transform=transform, tolerance=8): 307 | doc["editor.drag_start"] = doc["editor.mouse_xy"] 308 | doc["editor.grabbed"] = child["id"] 309 | child.transforms["editor"] = Ex("('translate', `editor.mouse_xy - `editor.drag_start)", 'reeval') 310 | return True 311 | return False 312 | 313 | def drop_point(): 314 | node = doc[doc["editor.grabbed"]] 315 | simplify_transform(node) 316 | doc["editor.drag_start"] = None 317 | doc["editor.grabbed"] = None 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | "# Add lines" 327 | 328 | input_callbacks += """ 329 | new_line = key_press(l) !add_line 330 | grammar = ( @exec | @button | @text | @move_point 331 | | @new_line | @anything)* 332 | """ 333 | 334 | def add_line(): 335 | doc["drawing"].append(Node("path", fill_color=None, children=[ 336 | Node("line", p_start=doc["editor.mouse_xy"], 337 | p_end=doc["editor.mouse_xy"] + P(50, 50))])) 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | "# Layouts" 346 | "## Alignment" 347 | 348 | 349 | def bboxes(nodes, transform=identity): 350 | boxes = [child.bbox(child.transform.dot(transform)) 351 | for child in nodes] 352 | boxes = zip(*[box for box in boxes if box != (None, None)]) 353 | if not boxes: 354 | return (None, None) 355 | return (numpy.min(numpy.vstack(boxes[0]), 0), 356 | numpy.max(numpy.vstack(boxes[1]), 0)) 357 | 358 | def align(nodes, side=0, axis=0, all_bbox=None): 359 | all_bbox = bboxes(nodes) if all_bbox is None else all_bbox 360 | for node in nodes: 361 | diff = all_bbox[side][axis] - node.bbox(node.transform)[side][axis] 362 | if diff and axis == 0: 363 | node.transforms["align"] = ('translate', P(diff, 0)) 364 | elif diff and axis == 1: 365 | node.transforms["align"] = ('translate', P(0, diff)) 366 | 367 | # Try 368 | # align(doc['drawing'][-3:]) 369 | 370 | 371 | 372 | 373 | "# Evenly distributing elements" 374 | 375 | def distribute(nodes, side=0, axis=0, spacing=10, all_bbox=None): 376 | all_bbox = bboxes(nodes) if all_bbox is None else all_bbox 377 | val = all_bbox[side][axis] 378 | for node in nodes: 379 | bbox = node.bbox(node.transform) 380 | diff = val - bbox[side][axis] 381 | node.transforms["distribute"] = ('translate', 382 | P(diff, 0) if axis == 0 else P(0, diff)) 383 | val += abs(bbox[1-side][axis] - bbox[side][axis]) 384 | val += spacing 385 | 386 | 387 | # Try 388 | # distribute(doc['drawing'][-5:]) 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | "# Automatically aligned groups" 397 | 398 | def layout_callback(source, func, *args): 399 | if source.get('auto_layout'): 400 | self = source if type(source) == Node else source.node 401 | nodes = self[1:] 402 | for node in nodes: 403 | if "distribute" in node.transforms: 404 | del node.transforms["distribute"] 405 | if "align" in node.transforms: 406 | del node.transforms["align"] 407 | all_bbox = self[0].bbox(self[0].transform) 408 | align(nodes, side=self["side"], axis=1-self["axis"], 409 | all_bbox=all_bbox) 410 | distribute(nodes, self["side"], self["axis"], 411 | all_bbox=all_bbox) 412 | 413 | 414 | 415 | # Try 416 | """ 417 | doc['drawing'].append(Node('group', id='layout', 418 | auto_layout=True, 419 | side=0, axis=1, children=[ 420 | Node("line", p_start=P(400, 200), 421 | p_end=P(600, 500))])) 422 | doc['layout'].callbacks.append(layout_callback) 423 | 424 | and see that it places its contents as they are added 425 | 426 | doc['layout'].append(doc['drawing'][4]) 427 | doc['layout'].append(doc['drawing'][4]) 428 | doc['layout'].append(doc['drawing'][4]) 429 | """ 430 | 431 | 432 | 433 | 434 | 435 | input_callbacks = """ 436 | exec = key_press(Return) 437 | (~key_press(Return) (key_press !add_letter(console) | @anything))* 438 | key_press(Return) !run_text(console) !clear(console) 439 | button = mouse_press(1) ?run_button mouse_release(1) 440 | text = key_press(t) (?edit_text | !create_text) 441 | (~key_press(Return) (key_press !add_letter | @anything))* 442 | key_press(Return) !finished_edit_text 443 | move_point = key_press(e) ?grab_point (~key_press(e) @anything)* key_press(e) !drop_point 444 | new_line = key_press(l) !add_line 445 | grammar = ( @exec | @button | @text | @move_point 446 | | @new_line | @anything)* 447 | """ 448 | ''' 449 | -------------------------------------------------------------------------------- /wrap_event.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | import numpy.linalg 3 | from collections import deque, defaultdict 4 | from config import BACKEND, DRAW_FREQUENCY 5 | import logging 6 | logger = logging.getLogger("event") 7 | proc_logger = logging.getLogger("proc") 8 | 9 | class Event: 10 | pass 11 | 12 | all_events = ["mouse_press", "mouse_release", "motion", "key_press", "key_release", "expose"] 13 | 14 | for event in all_events: 15 | setattr(Event, event, event) 16 | 17 | if BACKEND == "xcb": 18 | from xcffib.xproto import GC, CW, EventMask, WindowClass, ExposeEvent, ButtonPressEvent, ButtonReleaseEvent, MotionNotifyEvent, KeyPressEvent, KeyReleaseEvent, KeyButMask, NoExposureEvent 19 | from xpybutil import keybind 20 | import unikeysym 21 | 22 | xcb_event = {ButtonPressEvent: "mouse_press", 23 | ButtonReleaseEvent: "mouse_release", 24 | MotionNotifyEvent: "motion", 25 | KeyPressEvent: "key_press", 26 | KeyReleaseEvent: "key_release", 27 | ExposeEvent: "expose", 28 | NoExposureEvent: "no_exposure"} 29 | 30 | def key_name(keycode): 31 | return keybind.get_keysym_string(keybind.get_keysym(keycode)) 32 | 33 | startdrag = None 34 | 35 | def wrap_xcb_event(event, doc): 36 | global startdrag 37 | event.type = xcb_event.get(event.__class__, event.__class__) 38 | # Should include key_release! 39 | if event.type == Event.key_press: 40 | event.key_name = key_name(event.detail).lower() 41 | keysym = keybind.get_keysym(event.detail, event.state & 0b111) 42 | event.char = unikeysym.keys[keysym]["unicode"]\ 43 | if keysym in unikeysym.keys else u"" 44 | if event.char == unichr(0): 45 | event.char = "" 46 | event.mods = [] 47 | for attrib in dir(KeyButMask): 48 | if not attrib.startswith("__"): 49 | if event.state & getattr(KeyButMask, attrib): 50 | event.mods.append(attrib.lower()) 51 | elif event.type in [Event.mouse_press, Event.mouse_release]: 52 | event.button = event.detail 53 | event.xy = numpy.array([event.event_x, event.event_y]) 54 | event.txy = numpy.linalg.solve(doc["drawing"].transform, numpy.append(event.xy, [1]))[:2] 55 | event.mods = [] 56 | for attrib in dir(KeyButMask): 57 | if not attrib.startswith("__"): 58 | if event.state & getattr(KeyButMask, attrib): 59 | event.mods.append(attrib.lower()) 60 | if event.type == Event.mouse_press and event.button == 1: 61 | startdrag = event.xy 62 | logger.info("Wrapped: %s", (event.__class__.__name__, event.button, event.xy, event.mods)) 63 | elif event.type == Event.motion: 64 | event.xy = numpy.array([event.event_x, event.event_y]) 65 | event.txy = numpy.linalg.solve(doc["drawing"].transform, numpy.append(event.xy, [1]))[:2] 66 | if event.state & EventMask.Button1Motion: 67 | event.drag_button = 1 68 | elif event.state & EventMask.Button2Motion: 69 | event.drag_button = 2 70 | elif event.state & EventMask.Button1Motion: 71 | event.drag_button = 3 72 | else: 73 | event.drag_button = None 74 | if event.drag_button: 75 | event.diff = event.xy - startdrag 76 | if event.drag_button: 77 | logger.info("Wrapped: %s", (event.__class__.__name__, event.drag_button, event.xy, event.diff)) 78 | logger.info("Wrapped: %s", (event.type, event.__class__.__name__, event.xy)) 79 | 80 | def xcb_poll(doc): 81 | event = doc.surface.poll() 82 | if event is None: return None 83 | wrap_xcb_event(event, doc) 84 | return event 85 | 86 | backend_poll = xcb_poll 87 | 88 | elif BACKEND == "tkinter": 89 | tk_event = {"": "mouse_press", 90 | "": "mouse_release", 91 | "": "motion", 92 | "": "motion", 93 | "": "key_press", 94 | "": "key_release", 95 | "": "expose"} 96 | 97 | tk_mods = ["Shift", "Caps", "Control", "Left_Alt", "NumLock", 98 | "Right_Alt", "Button 1", "Button 2", "Button 3"] 99 | tk_mods = [m.lower() for m in tk_mods] 100 | 101 | tk_events = deque() 102 | 103 | def newevent(event_type, event): 104 | #print "Tk event", event_type, event.__dict__ 105 | event.raw_type = event_type 106 | tk_events.append(event) 107 | 108 | def tk_bind(tk_root): 109 | for event in ["", "", ""]: 110 | for button_num in range(1, 6): 111 | tk_root.bind(event % button_num, 112 | lambda e, t=(event, button_num): newevent(t, e)) 113 | tk_root.bind("", lambda e: newevent(("", None), e)) 114 | tk_root.bind("", lambda e: newevent(("", 0), e)) 115 | 116 | def wrap_tk_event(event, doc): 117 | global startdrag 118 | event.type = tk_event[event.raw_type[0]] 119 | # Should include key_release! 120 | if event.type == Event.key_press: 121 | event.key_name = event.keysym.lower() 122 | event.mods = [] 123 | for i, attrib in enumerate(tk_mods): 124 | if event.state & 1< 101 | 102 | `` is the *source file* being edited and defaults to `examples/functions.py`. 103 | 104 | Also try `examples/empty.py`. 105 | 106 | ## Basic usage 107 | 108 | After running guitktk, two windows open: a main window and a debugging window (more on their usage below). Open the source file (default: `functions.py`) in a text editor. 109 | 110 | - Edit and use the interface in the main window. 111 | - Edit the source file in your text editor, save and press Ctrl-r in the main window to load your changes (this runs `execfile` on the source). 112 | - Repeat. 113 | 114 | A number of functions are provided for convenience but they aren't connected to any UI elements or keyboard shortcut by default. Edit the source file and interface to add them. Paste code snippets from sample source files in `examples`. 115 | 116 | ## Default globals 117 | 118 | `doc` - the current document. See [persistent_doc](https://github.com/asrp/persistent_doc) for basic usage. 119 | 120 | Typically, you'd get nodes by id with `doc['']` (or some dot separated path instead of `id`) and alter values there, 121 | 122 | `input_callbacks` - string containing a description of the user's interaction, written in the language `gui_lang.py`. Input events are treated as a stream of "characters" defined by the "language" described in `input_callbacks`. 123 | 124 | # Examples 125 | 126 | [See this post first](https://blog.asrpo.com/gui_toolkit) 127 | 128 | **DIY demo**: There's now a Do-It Yourself demo of that post where you can progressively move down a triple quoted comment marker down a file, save and reload (control-r in the main window). The demo is `docs/demo.py`. 129 | 130 | [Example of adding (selection) rectangles](http://blog.asrpo.com/removing_polling) 131 | 132 | ## Different interfaces for adding a line 133 | 134 | As another showcase, here are different possible interfaces for adding a line. (The source is available in `example/line_demo.py`. Delete definitions of `new_line` in `input_callbacks` to get different behaviour.) 135 | 136 | All example assume the root grammar rule is something like 137 | 138 | grammar = (@new_line | @other_rule1 | @other_rule1 | ... | anything )* 139 | 140 | and each defines a different `new_line` rule. 141 | 142 | ### Single keypress 143 | 144 | First up is the default in `functions.py`. Add a line at the mouse cursor when `l` is pressedd. 145 | 146 | Grammar rule: 147 | 148 | new_line = key_press(l) !add_line 149 | 150 | Functions: 151 | 152 | def add_line(): 153 | doc["drawing"].append(Node("path", fill_color=None, children=[ 154 | Node("line", p_start=doc["editor.mouse_txy"], 155 | p_end=doc["editor.mouse_txy"] + P(50, 50))])) 156 | 157 | def key_press(event, key_name=None): 158 | return event.type == Event.key_press and\ 159 | (key_name is None or event.key_name == key_name) 160 | 161 | ### Modal 162 | 163 | In this variation, pressing `l` puts us in "line mode" and each left mouse click creates a line. Exit line mode by pressing any key (switching to a different mode in a more complex UI). 164 | 165 | Grammar: 166 | 167 | new_line = key_press(l) (~key_press mouse_press(1) !add_line)* 168 | 169 | Functions: 170 | 171 | def mouse_press(event, button=None): 172 | return event.type == Event.mouse_press and\ 173 | (button is None or event.button == int(button)) 174 | 175 | Other functions are the same as before. 176 | 177 | ### Modal two endpoints 178 | 179 | Same as above but the first click gives the first endpoint of the line and a second click puts the other endpoint. 180 | 181 | Grammar: 182 | 183 | new_line = key_press(l) 184 | (mouse_press(1) !add_line_start 185 | (~mouse_press(1) @anything)* mouse_press(1) !drop_point 186 | | ~key_press @anything)* 187 | 188 | Functions: 189 | 190 | def add_line_start(): 191 | line = Node("line", p_start=doc["editor.mouse_txy"], 192 | p_end=doc["editor.mouse_txy"]) 193 | doc["drawing"].append(Node("path", fill_color=None, children=[line])) 194 | doc["editor.drag_start"] = doc["editor.mouse_txy"] 195 | doc["editor.grabbed"] = line[1]["id"] 196 | line[1].transforms["editor"] = Ex("('translate', `editor.mouse_txy - `editor.drag_start)", calc='on first read') 197 | 198 | def drop_point(): 199 | node = doc[doc["editor.grabbed"]] 200 | simplify_transform(node) 201 | doc["editor.drag_start"] = None 202 | doc["editor.grabbed"] = None 203 | 204 | # Document 205 | 206 | `p_something` is shorthand for a child `point` Node with `child_id="something"`. 207 | 208 | ## Node types 209 | 210 | Create with `Node("", **params, children=[])`. `.param_name` below refers to keys in `params`. 211 | 212 | - `group`: Node for holding other nodes 213 | - `point`: a point at position `.value`, represented as a 2 by 1 `numpy.array` (shorthand: `P`) 214 | - `text`: the string `.value` rendered at `.font_size` with the bottom left corner at Point `.botleft` 215 | - `line`: line from Point `.start` to Point `.end` 216 | - `path`: group of multiple lines 217 | - `arc`: arc of radius `.radius` centered at `.center` between the angles of `.angle` (a tuple or `None`) in radians. 218 | 219 | [TODO: Add examples of each. See examples in the sample interfaces for the moment.] 220 | 221 | ## Special properties 222 | 223 | - `id` 224 | - `child_id`: To get a "named" child accessible with `paramt[""]` 225 | - `transforms`: dictionary (`pysistent.pmap`) from string to 3 by 3 `numpy.array` (same format as SVG). 226 | 227 | ## Propagates to subtree 228 | 229 | `line_width, stroke_color, fill_color, dash, skip_points` 230 | 231 | The value of a parameter is that of the first ancestor of a node (including itself) that defines that parameter. 232 | 233 | # Default interface 234 | 235 | ## Hard-coded keys 236 | 237 | - **Ctrl-r**: Reload the source file 238 | - **Ctrl-z**: Attempts to undo the last reload wiht `Ctrl-r`. 239 | 240 | ## Default UI 241 | 242 | Its best to read the `input_callbacks` from the respective files but here's a high level description for `funtions.py`. 243 | 244 | The UI is essentially modeless and the entire keyboard is thought of as many more buttons for the mouse. 245 | 246 | - Add new text: `t`, edit the text and press enter when done 247 | - Edit text: `t` with mouse cursor over text 248 | - Add line: `l` 249 | - Move points (blue filled circles): `e` with mouse over the point, move the mouse cursor, `e` again when done 250 | - Select: `s` with mouse cursor over element toggles selection 251 | - Group selection: `g` 252 | - Ungroup selection: `u` 253 | - Move selection: `m`, move mouse cursor, `m` again when done 254 | - Zoom: Ctrl + mouse wheel 255 | - Scroll: mouse wheel and shift + mouse wheel 256 | 257 | ### Special elements 258 | 259 | **Buttons**: Any text starting with `!` is a button that runs the rest of that text when clicked. 260 | **Status bars**: Any text starting with `=` will evaluate the remaining expression. References to the rest of the document should be preceeded with a backtick. 261 | 262 | ## Debugging window 263 | 264 | Right now there's a second window that pops up and helps with debugging. In future versions, that window may no longer be needed. It includes a text representation of the `drawing` subtree and a console to run commands (outputs are in the terminal where Python was started). 265 | 266 | Some debug buttons are there to make quick post-mortem debugging easier. 267 | 268 | ## Changing the default grammar 269 | 270 | The grammar in which the grammar is written is in `gui_lang.py` and can be edited. It is written in the default [pymetaterp](https://github.com/asrp/pymetaterp) language (and thus can itself be modified if needed). 271 | 272 | ## Formulas referencing nodes in the document 273 | 274 | See [persistent_doc's readme](https://github.com/asrp/persistent_doc). 275 | 276 | ## Changing backends 277 | 278 | Manually edit `config.py`. 279 | 280 | ## Helper languages for describing trees 281 | 282 | # Undo redo and debugging 283 | 284 | Because guitktk uses `eval` and `execfile` quite a bit in order to gives you a lot of expressive power, it makes it very eazy to break things. 285 | 286 | ## Undo reload 287 | 288 | By default, pressing **Ctrl-z** attempts to undo to before the last reload (with `Ctrl-r`). 289 | 290 | ## Undo individual changes 291 | 292 | `doc.undo()` and `doc.redo()` are very low level and undoes a single modification to `doc`. Even simple actions usually result in many changes at this level. 293 | 294 | A more practical approach is to save a `doc.m` pointer and restore to it with `doc.log("undo", pointer)`. For example, the sample file `functions.py` defines 295 | 296 | def doc_undo(): 297 | doc.undo_index -= 1 298 | doc.log("undo", doc.saved[doc.undo_index]) 299 | doc.dirty.clear() 300 | 301 | def doc_redo(): 302 | if doc.undo_index < -1: 303 | doc.undo_index += 1 304 | doc.log("redo", doc.saved[doc.undo_index]) 305 | doc.dirty.clear() 306 | 307 | def save_undo(): 308 | if doc.undo_index != -1: 309 | del doc.saved[doc.undo_index+1:] 310 | doc.saved.append(doc.m) 311 | doc.undo_index = -1 312 | 313 | def fail(): 314 | return False 315 | 316 | if __init__: 317 | doc.saved = [doc.m] 318 | doc.undo_index = -1 319 | 320 | and adds a wrapper to the root grammar rule that calls save_undo on successful interactions 321 | 322 | grammar = (@command !save_undo | @anything)* 323 | 324 | and adds keyboards shortcuts (`z` for undo, `shift-z` for redo) 325 | 326 | command = ... | @undo | @redo 327 | undo = key_press(z) !doc_undo ?fail 328 | redo = key_press(Z) !doc_redo ?fail 329 | 330 | ## Saving and loading documents 331 | 332 | `doc.save()` or `doc.save('.py')`. And later `doc.load()` or `doc.load('')`. 333 | 334 | As you can see from the file generated, this does *not* save the document's edit history so undo after a document load is not possible. 335 | 336 | ## Reading the source 337 | 338 | Here are a few hints about reading the source. `flow_editor.py` is the starting point for execution but I would probably look at `node.py` (and maybe some of the source for dependency [persistent_doc](https://github.com/asrp/persistent_doc)) first. Or `gui_lang.py` which describes the language `input_callbacks` is written in. 339 | 340 | For the backend, look at `wrap_events.py` and `draw.py` first and then one of `tkinter_doc.py`, `xcb_doc` or `ogl_doc.py` 341 | 342 | `compgeo.py` contains a small number of geometry algorithms. 343 | 344 | ## To document 345 | 346 | - Helper `tree_lang` 347 | - Event record and replay 348 | - Document graphic elements 349 | - Backend renders 350 | - Computational geometry helpers 351 | - Things to try in the default document 352 | -------------------------------------------------------------------------------- /undoable.py: -------------------------------------------------------------------------------- 1 | def deepwrap(elem, callbacks=[], undocallbacks=[], wrapper=None, skiproot=False): 2 | """ Wrap nested list and dict. """ 3 | if wrapper: 4 | output = wrapper(elem) 5 | if output is not None: 6 | return output 7 | if type(elem) == list: 8 | inner = [deepwrap(subelem, callbacks, undocallbacks, wrapper) 9 | for subelem in elem] 10 | if skiproot: 11 | return inner 12 | return observed_list(inner, callbacks=callbacks, undocallbacks=undocallbacks) 13 | elif type(elem) == dict: 14 | inner = dict((key, deepwrap(value, callbacks, undocallbacks, wrapper)) 15 | for key, value in elem.items()) 16 | if skiproot: 17 | return inner 18 | return observed_dict(inner, callbacks=callbacks, undocallbacks=undocallbacks) 19 | else: 20 | return elem 21 | 22 | class UndoLog(object): 23 | def __init__(self): 24 | # self.root: root of the undo tree 25 | # self.undoroot: root of the current event being treated 26 | # self.index: marks the position between undo and redo. Always negative 27 | # (counting from the back). 28 | self.root = self.undoroot = observed_tree("undo root") 29 | self.watched = [] 30 | self.index = -1 31 | 32 | def add(self, elem): 33 | """ Add element to watch. """ 34 | self.watched.append(elem) 35 | elem.undocallbacks.append(self.log) 36 | 37 | def log(self, elem, undoitem, redoitem): 38 | if elem.skiplog > 0: 39 | return 40 | self.clear_redo() 41 | self.undoroot.append(observed_tree(name=(elem, undoitem, redoitem))) 42 | 43 | def clear_redo(self): 44 | if self.undoroot == self.root and self.index > -1: 45 | # Need to delete everything if we aren't the last index! 46 | del self.root[self.index+1:] 47 | self.index = -1 48 | 49 | def start_group(self, name, new_only=False): 50 | if new_only and self.undoroot.name == name: 51 | return False 52 | self.clear_redo() 53 | self.undoroot.append(observed_tree(name)) 54 | self.index = -1 55 | self.undoroot = self.undoroot[-1] 56 | return True 57 | 58 | def end_group(self, name, skip_unstarted=False, delete_if_empty=False): 59 | if name and self.undoroot.name != name: 60 | if skip_unstarted: return 61 | raise Exception("Ending group %s but the current group is %s!" %\ 62 | (name, self.undoroot.name)) 63 | if not self.undoroot.parent: 64 | raise Exception("Attempting to end root group!") 65 | self.undoroot = self.undoroot.parent 66 | if delete_if_empty and len(self.undoroot) == 0: 67 | self.undoroot.pop() 68 | self.index = -1 69 | 70 | def undo(self, node=None): 71 | if node is None: 72 | node = self.root[self.index] 73 | self.index -= 1 74 | if type(node.name) == str: 75 | for child in reversed(node): 76 | self.undo(child) 77 | else: 78 | self.unredo_event(node.name[0], node.name[1]) 79 | 80 | def redo(self, node=None): 81 | if node is None: 82 | node = self.root[self.index + 1] 83 | self.index += 1 84 | if type(node.name) == str: 85 | for child in node: 86 | self.redo(child) 87 | else: 88 | self.unredo_event(node.name[0], node.name[2]) 89 | 90 | def unredo_event(self, elem, item): 91 | elem.skiplog += 1 92 | getattr(elem, item[0])(*item[1:]) 93 | elem.skiplog -= 1 94 | 95 | def pprint(self, node=None): 96 | for line in self.pprint_string(node): 97 | print line 98 | 99 | def pprint_string(self, node=None, indent=0): 100 | if node is None: 101 | node = self.root 102 | if type(node.name) != str: 103 | yield "%s%s" % (indent*" ", node.name[2][0]) 104 | return 105 | name = node.name if node.name else "" 106 | yield "%s%s" % (indent*" ", name) 107 | for child in node: 108 | for line in self.pprint_string(child, indent + 2): 109 | yield line 110 | 111 | class observed_list(list): 112 | """ A list that calls all functions in self.undocallbacks 113 | with an (undo, redo) pair any time an operation is applied to the list. 114 | Every function in self.callbacks is called with *redo instead. 115 | 116 | Contains a self.replace function not in python's list for conenience. 117 | """ 118 | def __init__(self, *args, **kwargs): 119 | list.__init__(self, *args) 120 | self.callbacks = kwargs.get("callbacks", []) 121 | self.undocallbacks = kwargs.get("undocallbacks", []) 122 | self.skiplog = 0 123 | 124 | def callback(self, undo, redo): 125 | for callback in self.callbacks: 126 | callback(self, *redo) 127 | for callback in self.undocallbacks: 128 | callback(self, undo, redo) 129 | 130 | def __deepcopy__(self, memo): 131 | return observed_list(self) 132 | 133 | def __setitem__(self, key, value): 134 | try: 135 | oldvalue = self.__getitem__(key) 136 | except KeyError: 137 | list.__setitem__(self, key, value) 138 | self.callback(("__delitem__", key), ("__setitem__", key, value)) 139 | else: 140 | list.__setitem__(self, key, value) 141 | self.callback(("__setitem__", key, oldvalue), 142 | ("__setitem__", key, value)) 143 | 144 | def __delitem__(self, key): 145 | oldvalue = list.__getitem__(self, key) 146 | list.__delitem__(self, key) 147 | self.callback(("__setitem__", key, oldvalue), ("__delitem__", key)) 148 | 149 | def __setslice__(self, i, j, sequence): 150 | oldvalue = list.__getslice__(self, i, j) 151 | self.callback(("__setslice__", i, j, oldvalue), 152 | ("__setslice__", i, j, sequence)) 153 | list.__setslice__(self, i, j, sequence) 154 | 155 | def __delslice__(self, i, j): 156 | oldvalue = list.__getitem__(self, slice(i, j)) 157 | list.__delslice__(self, i, j) 158 | self.callback(("__setslice__", i, i, oldvalue), ("__delslice__", i, j)) 159 | 160 | def append(self, value): 161 | list.append(self, value) 162 | self.callback(("pop",), ("append", value)) 163 | 164 | def pop(self, index=-1): 165 | oldvalue = list.pop(self, index) 166 | self.callback(("append", oldvalue), ("pop", index)) 167 | return oldvalue 168 | 169 | def extend(self, newvalue): 170 | oldlen = len(self) 171 | list.extend(self, newvalue) 172 | self.callback(("__delslice__", oldlen, len(self)), 173 | ("extend", newvalue)) 174 | 175 | def insert(self, i, element): 176 | list.insert(self, i, element) 177 | self.callback(("pop", i), ("insert", i, element)) 178 | 179 | def remove(self, element): 180 | if element in self: 181 | oldindex = self.index(element) 182 | list.remove(self, element) 183 | self.callback(("insert", oldindex, element), ("remove", element)) 184 | 185 | def reverse(self): 186 | list.reverse(self) 187 | self.callback(("reverse",), ("reverse",)) 188 | 189 | def sort(self, cmpfunc=None): 190 | oldlist = self[:] 191 | list.sort(self, cmpfunc) 192 | self.callback(("replace", oldlist), ("sort",)) 193 | 194 | def replace(self, newlist): 195 | oldlist = self[:] 196 | self.skiplog += 1 197 | del self[:] 198 | try: 199 | self.extend(newlist) 200 | except: 201 | self.replace(oldlist) # Hopefully no infinite loops happens 202 | self.skiplog -= 1 203 | raise 204 | self.skiplog -= 1 205 | self.callback(("replace", oldlist), ("replace", newlist)) 206 | 207 | class observed_dict(dict): 208 | def __init__(self, *args, **kwargs): 209 | self.callbacks = kwargs.pop("callbacks", []) 210 | self.undocallbacks = kwargs.pop("undocallbacks", []) 211 | dict.__init__(self, *args, **kwargs) 212 | self.skiplog = 0 213 | 214 | def callback(self, undo, redo): 215 | for callback in self.callbacks: 216 | callback(self, *redo) 217 | for callback in self.undocallbacks: 218 | callback(self, undo, redo) 219 | 220 | def __deepcopy__(self, memo): 221 | return observed_dict(self) 222 | 223 | def __setitem__(self, key, value): 224 | try: 225 | oldvalue = self.__getitem__(key) 226 | except KeyError: 227 | dict.__setitem__(self, key, value) 228 | self.callback(("__delitem__", key), ("__setitem__", key, value)) 229 | else: 230 | dict.__setitem__(self, key, value) 231 | self.callback(("__setitem__", key, oldvalue), 232 | ("__setitem__", key, value)) 233 | 234 | def __delitem__(self, key): 235 | oldvalue = self[key] 236 | dict.__delitem__(self, key) 237 | self.callback(("__setitem__", key, oldvalue), ("__delitem__", key)) 238 | 239 | def clear(self): 240 | oldvalue = self.copy() 241 | dict.clear(self) 242 | self.callback(("update", oldvalue), ("clear",)) 243 | 244 | def update(self, update_dict): 245 | oldvalue = self.copy() 246 | dict.update(self, update_dict) 247 | self.callback(("replace", oldvalue), ("update", update_dict)) 248 | 249 | def setdefault(self, key, value=None): 250 | if key not in self: 251 | dict.setdefault(self, key, value) 252 | self.callback(("__delitem__", key), ("setdefault", key, value)) 253 | return value 254 | else: 255 | return self[key] 256 | 257 | def pop(self, key, default=None): 258 | if key in self: 259 | value = dict.pop(self, key, default) 260 | self.callback(("__setitem__", key, value), ("pop", key, default)) 261 | return value 262 | else: 263 | return default 264 | 265 | def popitem(self): 266 | key, value = dict.popitem(self) 267 | self.callback(("__setitem__", key, default), ("popitem",)) 268 | return key, value 269 | 270 | def replace(self, newdict): 271 | oldvalue = self.copy() 272 | self.skiplog += 1 273 | self.clear() 274 | try: 275 | self.update(newdict) 276 | except: 277 | self.replace(oldvalue) # Hopefully no infinite loops happens 278 | self.skiplog -= 1 279 | raise 280 | self.skiplog -= 1 281 | self.callback(("replace", newdict), ("replace", oldvalue)) 282 | 283 | class observed_tree(list): 284 | """ An ordered list of children. The only difference with list is a maintained parent pointer. All elements of this list are expected to be trees or have parent pointers and a reparent function. 285 | 286 | Expects to contain at most one copy of any elements. 287 | 288 | Can be used to model XML, JSON documents, DOM, etc.""" 289 | def __init__(self, name=None, value=[], parent=None, 290 | callbacks=None, undocallbacks=None): 291 | list.__init__(self, value) 292 | self.parent = parent 293 | self.name = name 294 | self.callbacks = callbacks if callbacks else [] 295 | self.undocallbacks = undocallbacks if undocallbacks else [] 296 | self.skiplog = 0 297 | 298 | def callback(self, undo, redo, origin=None): 299 | # TODO: Need to think of a better way to pass self along 300 | if origin == None: 301 | origin = self 302 | for callback in self.callbacks: 303 | callback(origin, *redo) 304 | for callback in self.undocallbacks: 305 | callback(origin, undo, redo) 306 | if self.parent: 307 | self.parent.callback(undo, redo, origin) 308 | 309 | def _reparent(self, newparent, remove=False): 310 | if remove and self.parent: 311 | self.parent.remove(self, reparent=False) 312 | self.parent = newparent 313 | 314 | def __setitem__(self, key, value): 315 | try: 316 | oldvalue = self.__getitem__(key) 317 | except KeyError: 318 | value._reparent(self, True) 319 | list.__setitem__(self, key, value) 320 | # What to do about undo on that reparent 321 | self.callback(("__delitem__", key), ("__setitem__", key, value)) 322 | else: 323 | oldvalue._reparent(None) 324 | value._reparent(self, True) 325 | list.__setitem__(self, key, value) 326 | self.callback(("__setitem__", key, oldvalue), 327 | ("__setitem__", key, value)) 328 | 329 | def __delitem__(self, key): 330 | oldvalue = list.__getitem__(self, key) 331 | list.__delitem__(self, key) 332 | oldvalue._reparent(None) 333 | # What to do about undo on that reparent? 334 | # __setitem__ takes care of this at the moment. 335 | self.callback(("__setitem__", key, oldvalue), ("__delitem__", key)) 336 | 337 | def __setslice__(self, i, j, sequence): 338 | oldvalue = list.__getslice__(self, i, j) 339 | self.callback(("__setslice__", i, j, oldvalue), 340 | ("__setslice__", i, j, sequence)) 341 | for child in oldvalue: 342 | child._reparent(None, True) 343 | for child in sequence: 344 | child._reparent(self, True) 345 | list.__setslice__(self, i, j, sequence) 346 | 347 | def __delslice__(self, i, j): 348 | oldvalue = list.__getitem__(self, slice(i, j)) 349 | for child in oldvalue: 350 | child._reparent(None, True) 351 | list.__delslice__(self, i, j) 352 | self.callback(("__setslice__", i, i, oldvalue), ("__delslice__", i, j)) 353 | 354 | def __eq__(self, other): 355 | return self is other 356 | 357 | def append(self, value): 358 | list.append(self, value) 359 | value._reparent(self, True) 360 | self.callback(("pop",), ("append", value)) 361 | 362 | def pop(self, index=-1): 363 | oldvalue = list.pop(self, index) 364 | oldvalue._reparent(None) 365 | self.callback(("append", oldvalue), ("pop", index)) 366 | return oldvalue 367 | 368 | def extend(self, newvalue): 369 | oldlen = len(self) 370 | list.extend(self, newvalue) 371 | newvalue = newvalue[:] 372 | for value in newvalue: 373 | value._reparent(self, True) 374 | self.callback(("__delslice__", oldlen, len(self)), 375 | ("extend", newvalue)) 376 | 377 | def insert(self, i, element): 378 | element._reparent(self, True) 379 | list.insert(self, i, element) 380 | self.callback(("pop", i), ("insert", i, element)) 381 | 382 | def remove(self, element, reparent=True): 383 | if element in self: 384 | oldindex = self.index(element) 385 | list.remove(self, element) 386 | if reparent: 387 | element._reparent(None) 388 | self.callback(("insert", oldindex, element), ("remove", element)) 389 | 390 | def reverse(self): 391 | list.reverse(self) 392 | self.callback(("reverse",), ("reverse",)) 393 | 394 | def sort(self, cmpfunc=None): 395 | list.sort(self, cmpfunc) 396 | self.callback(("replace", oldlist), ("sort",)) 397 | 398 | def replace(self, newlist): 399 | oldlist = self[:] 400 | self.skiplog += 1 401 | del self[:] 402 | try: 403 | self.extend(newlist) 404 | except: 405 | self.replace(oldlist) # Hopefully no infinite loops happens 406 | self.skiplog -= 1 407 | raise 408 | self.skiplog -= 1 409 | self.callback(("replace", oldlist), ("replace", newlist)) 410 | 411 | # Helper functions. Maybe they should be elsewhere. 412 | def tops(self, condition): 413 | """ Return the top (incomparable maximal) anti-chain amongst descendants of widget that satisfy condition""" 414 | for child in self: 415 | if condition(child): 416 | yield child 417 | else: 418 | for gc in child.tops(condition): 419 | yield gc 420 | 421 | @property 422 | def descendants(self): 423 | for child in self: 424 | for gc in child.descendants: 425 | yield gc 426 | yield child 427 | 428 | # Add this to callbacks for debugging 429 | def printargs(*args): 430 | print args 431 | 432 | if __name__ == '__main__': 433 | # minimal demonstration 434 | u = UndoLog() 435 | d = observed_dict({1: "one", 2: "two"}) 436 | l = observed_list([1, 2, 3]) 437 | u.add(d) 438 | u.add(l) 439 | l.append(1) 440 | d[3] = "Hello" 441 | u.start_group("foo") 442 | d[53] = "user" 443 | del d[1] 444 | u.end_group("foo") 445 | u.start_group("foo") 446 | d["bar"] = "baz" 447 | u.end_group("foo") 448 | deep = {"abc": "def", "alist": [1, 2, 3]} 449 | obs_deep = deepwrap(deep, undocallbacks=[u.log]) 450 | u.watched.append(obs_deep) 451 | obs_deep["d"] = "e" 452 | obs_deep["alist"].append(4) 453 | # Now run multiple u.undo(), u.redo() 454 | -------------------------------------------------------------------------------- /xpybutil/keybind.py: -------------------------------------------------------------------------------- 1 | """ 2 | A set of functions devoted to binding key presses and registering 3 | callbacks. This will automatically hook into the event callbacks 4 | in event.py. 5 | 6 | The two functions of interest here are 'bind_global_key' and 'bind_key'. Most 7 | of the other functions facilitate the use of those two, but you may need them 8 | if you're getting down and dirty. 9 | """ 10 | from collections import defaultdict 11 | import sys 12 | 13 | from xpybutil.compat import xproto 14 | 15 | from xpybutil import conn, root, event 16 | from xpybutil.keysymdef import keysyms, keysym_strings 17 | 18 | __kbmap = None 19 | __keysmods = None 20 | 21 | __keybinds = defaultdict(list) 22 | __keygrabs = defaultdict(int) # Key grab key -> number of grabs 23 | 24 | EM = xproto.EventMask 25 | GM = xproto.GrabMode 26 | TRIVIAL_MODS = [ 27 | 0, 28 | xproto.ModMask.Lock, 29 | xproto.ModMask._2, 30 | xproto.ModMask.Lock | xproto.ModMask._2 31 | ] 32 | 33 | def bind_global_key(event_type, key_string, cb): 34 | """ 35 | An alias for ``bind_key(event_type, ROOT_WINDOW, key_string, cb)``. 36 | 37 | :param event_type: Either 'KeyPress' or 'KeyRelease'. 38 | :type event_type: str 39 | :param key_string: A string of the form 'Mod1-Control-a'. 40 | Namely, a list of zero or more modifiers separated by 41 | '-', followed by a single non-modifier key. 42 | :type key_string: str 43 | :param cb: A first class function with no parameters. 44 | :type cb: function 45 | :return: True if the binding was successful, False otherwise. 46 | :rtype: bool 47 | """ 48 | return bind_key(event_type, root, key_string, cb) 49 | 50 | def bind_key(event_type, wid, key_string, cb): 51 | """ 52 | Binds a function ``cb`` to a particular key press ``key_string`` on a 53 | window ``wid``. Whether it's a key release or key press binding is 54 | determined by ``event_type``. 55 | 56 | ``bind_key`` will automatically hook into the ``event`` module's dispatcher, 57 | so that if you're using ``event.main()`` for your main loop, everything 58 | will be taken care of for you. 59 | 60 | :param event_type: Either 'KeyPress' or 'KeyRelease'. 61 | :type event_type: str 62 | :param wid: The window to bind the key grab to. 63 | :type wid: int 64 | :param key_string: A string of the form 'Mod1-Control-a'. 65 | Namely, a list of zero or more modifiers separated by 66 | '-', followed by a single non-modifier key. 67 | :type key_string: str 68 | :param cb: A first class function with no parameters. 69 | :type cb: function 70 | :return: True if the binding was successful, False otherwise. 71 | :rtype: bool 72 | """ 73 | assert event_type in ('KeyPress', 'KeyRelease') 74 | 75 | mods, kc = parse_keystring(key_string) 76 | key = (wid, mods, kc) 77 | 78 | if not kc: 79 | print >> sys.stderr, 'Could not find a keycode for %s' % key_string 80 | return False 81 | 82 | if not __keygrabs[key] and not grab_key(wid, mods, kc): 83 | return False 84 | 85 | __keybinds[key].append(cb) 86 | __keygrabs[key] += 1 87 | 88 | if not event.is_connected(event_type, wid, __run_keybind_callbacks): 89 | event.connect(event_type, wid, __run_keybind_callbacks) 90 | 91 | return True 92 | 93 | def parse_keystring(key_string): 94 | """ 95 | A utility function to turn strings like 'Mod1-Mod4-a' into a pair 96 | corresponding to its modifiers and keycode. 97 | 98 | :param key_string: String starting with zero or more modifiers followed 99 | by exactly one key press. 100 | 101 | Available modifiers: Control, Mod1, Mod2, Mod3, Mod4, 102 | Mod5, Shift, Lock 103 | :type key_string: str 104 | :return: Tuple of modifier mask and keycode 105 | :rtype: (mask, int) 106 | """ 107 | modifiers = 0 108 | keycode = None 109 | 110 | for part in key_string.split('-'): 111 | if hasattr(xproto.KeyButMask, part): 112 | modifiers |= getattr(xproto.KeyButMask, part) 113 | else: 114 | if len(part) == 1: 115 | part = part.lower() 116 | keycode = lookup_string(part) 117 | 118 | return modifiers, keycode 119 | 120 | def lookup_string(kstr): 121 | """ 122 | Finds the keycode associated with a string representation of a keysym. 123 | 124 | :param kstr: English representation of a keysym. 125 | :return: Keycode, if one exists. 126 | :rtype: int 127 | """ 128 | if kstr in keysyms: 129 | return get_keycode(keysyms[kstr]) 130 | elif len(kstr) > 1 and kstr.capitalize() in keysyms: 131 | return get_keycode(keysyms[kstr.capitalize()]) 132 | 133 | return None 134 | 135 | def lookup_keysym(keysym): 136 | """ 137 | Finds the english string associated with a keysym. 138 | 139 | :param keysym: An X keysym. 140 | :return: English string representation of a keysym. 141 | :rtype: str 142 | """ 143 | return get_keysym_string(keysym) 144 | 145 | def get_min_max_keycode(): 146 | """ 147 | Return a tuple of the minimum and maximum keycode allowed in the 148 | current X environment. 149 | 150 | :rtype: (int, int) 151 | """ 152 | return conn.get_setup().min_keycode, conn.get_setup().max_keycode 153 | 154 | def get_keyboard_mapping(): 155 | """ 156 | Return a keyboard mapping cookie that can be used to fetch the table of 157 | keysyms in the current X environment. 158 | 159 | :rtype: xcb.xproto.GetKeyboardMappingCookie 160 | """ 161 | mn, mx = get_min_max_keycode() 162 | 163 | return conn.core.GetKeyboardMapping(mn, mx - mn + 1) 164 | 165 | def get_keyboard_mapping_unchecked(): 166 | """ 167 | Return an unchecked keyboard mapping cookie that can be used to fetch the 168 | table of keysyms in the current X environment. 169 | 170 | :rtype: xcb.xproto.GetKeyboardMappingCookie 171 | """ 172 | mn, mx = get_min_max_keycode() 173 | 174 | return conn.core.GetKeyboardMappingUnchecked(mn, mx - mn + 1) 175 | 176 | def get_keysym(keycode, col=0, kbmap=None): 177 | """ 178 | Get the keysym associated with a particular keycode in the current X 179 | environment. Although we get a list of keysyms from X in 180 | 'get_keyboard_mapping', this list is really a table with 181 | 'keysys_per_keycode' columns and ``mx - mn`` rows (where ``mx`` is the 182 | maximum keycode and ``mn`` is the minimum keycode). 183 | 184 | Thus, the index for a keysym given a keycode is: 185 | ``(keycode - mn) * keysyms_per_keycode + col``. 186 | 187 | In most cases, setting ``col`` to 0 will work. 188 | 189 | Witness the utter complexity: 190 | http://tronche.com/gui/x/xlib/input/keyboard-encoding.html 191 | 192 | You may also pass in your own keyboard mapping using the ``kbmap`` 193 | parameter, but xpybutil maintains an up-to-date version of this so you 194 | shouldn't have to. 195 | 196 | :param keycode: A physical key represented by an integer. 197 | :type keycode: int 198 | :param col: The column in the keysym table to use. 199 | Unless you know what you're doing, just use 0. 200 | :type col: int 201 | :param kbmap: The keyboard mapping to use. 202 | :type kbmap: xcb.xproto.GetKeyboardMapingReply 203 | """ 204 | if kbmap is None: 205 | kbmap = __kbmap 206 | 207 | mn, mx = get_min_max_keycode() 208 | per = kbmap.keysyms_per_keycode 209 | ind = (keycode - mn) * per + col 210 | 211 | return kbmap.keysyms[ind] 212 | 213 | def get_keysym_string(keysym): 214 | """ 215 | A simple wrapper to find the english string associated with a particular 216 | keysym. 217 | 218 | :param keysym: An X keysym. 219 | :rtype: str 220 | """ 221 | return keysym_strings.get(keysym, [None])[0] 222 | 223 | def get_keycode(keysym): 224 | """ 225 | Given a keysym, find the keycode mapped to it in the current X environment. 226 | It is necessary to search the keysym table in order to do this, including 227 | all columns. 228 | 229 | :param keysym: An X keysym. 230 | :return: A keycode or None if one could not be found. 231 | :rtype: int 232 | """ 233 | mn, mx = get_min_max_keycode() 234 | cols = __kbmap.keysyms_per_keycode 235 | for i in range(mn, mx + 1): 236 | for j in range(0, cols): 237 | ks = get_keysym(i, col=j) 238 | if ks == keysym: 239 | return i 240 | 241 | return None 242 | 243 | def get_mod_for_key(keycode): 244 | """ 245 | Finds the modifier that is mapped to the given keycode. 246 | This may be useful when analyzing key press events. 247 | 248 | :type keycode: int 249 | :return: A modifier identifier. 250 | :rtype: xcb.xproto.ModMask 251 | """ 252 | return __keysmods.get(keycode, 0) 253 | 254 | def get_keys_to_mods(): 255 | """ 256 | Fetches and creates the keycode -> modifier mask mapping. Typically, you 257 | shouldn't have to use this---xpybutil will keep this up to date if it 258 | changes. 259 | 260 | This function may be useful in that it should closely replicate the output 261 | of the ``xmodmap`` command. For example: 262 | 263 | :: 264 | 265 | keymods = get_keys_to_mods() 266 | for kc in sorted(keymods, key=lambda kc: keymods[kc]): 267 | print keymods[kc], hex(kc), get_keysym_string(get_keysym(kc)) 268 | 269 | Which will very closely replicate ``xmodmap``. I'm not getting precise 270 | results quite yet, but I do believe I'm getting at least most of what 271 | matters. (i.e., ``xmodmap`` returns valid keysym strings for some that 272 | I cannot.) 273 | 274 | :return: A dict mapping from keycode to modifier mask. 275 | :rtype: dict 276 | """ 277 | mm = xproto.ModMask 278 | modmasks = [mm.Shift, mm.Lock, mm.Control, 279 | mm._1, mm._2, mm._3, mm._4, mm._5] # order matters 280 | 281 | mods = conn.core.GetModifierMapping().reply() 282 | 283 | res = {} 284 | keyspermod = mods.keycodes_per_modifier 285 | for mmi in range(0, len(modmasks)): 286 | row = mmi * keyspermod 287 | for kc in mods.keycodes[row:row + keyspermod]: 288 | res[kc] = modmasks[mmi] 289 | 290 | return res 291 | 292 | def get_modifiers(state): 293 | """ 294 | Takes a ``state`` (typically found in key press or button press events) 295 | and returns a string list representation of the modifiers that were pressed 296 | when generating the event. 297 | 298 | :param state: Typically from ``some_event.state``. 299 | :return: List of modifier string representations. 300 | :rtype: [str] 301 | """ 302 | ret = [] 303 | 304 | if state & xproto.ModMask.Shift: 305 | ret.append('Shift') 306 | if state & xproto.ModMask.Lock: 307 | ret.append('Lock') 308 | if state & xproto.ModMask.Control: 309 | ret.append('Control') 310 | if state & xproto.ModMask._1: 311 | ret.append('Mod1') 312 | if state & xproto.ModMask._2: 313 | ret.append('Mod2') 314 | if state & xproto.ModMask._3: 315 | ret.append('Mod3') 316 | if state & xproto.ModMask._4: 317 | ret.append('Mod4') 318 | if state & xproto.ModMask._5: 319 | ret.append('Mod5') 320 | if state & xproto.KeyButMask.Button1: 321 | ret.append('Button1') 322 | if state & xproto.KeyButMask.Button2: 323 | ret.append('Button2') 324 | if state & xproto.KeyButMask.Button3: 325 | ret.append('Button3') 326 | if state & xproto.KeyButMask.Button4: 327 | ret.append('Button4') 328 | if state & xproto.KeyButMask.Button5: 329 | ret.append('Button5') 330 | 331 | return ret 332 | 333 | def grab_keyboard(grab_win): 334 | """ 335 | This will grab the keyboard. The effect is that further keyboard events 336 | will *only* be sent to the grabbing client. (i.e., ``grab_win``). 337 | 338 | N.B. There is an example usage of this in examples/window-marker. 339 | 340 | :param grab_win: A window identifier to report keyboard events to. 341 | :type grab_win: int 342 | :rtype: xcb.xproto.GrabStatus 343 | """ 344 | return conn.core.GrabKeyboard(False, grab_win, xproto.Time.CurrentTime, 345 | GM.Async, GM.Async).reply() 346 | 347 | def ungrab_keyboard(): 348 | """ 349 | This will release a grab initiated by ``grab_keyboard``. 350 | 351 | :rtype: void 352 | """ 353 | conn.core.UngrabKeyboardChecked(xproto.Time.CurrentTime).check() 354 | 355 | def grab_key(wid, modifiers, key): 356 | """ 357 | Grabs a key for a particular window and a modifiers/key value. 358 | If the grab was successful, return True. Otherwise, return False. 359 | If your client is grabbing keys, it is useful to notify the user if a 360 | key wasn't grabbed. Keyboard shortcuts not responding is disorienting! 361 | 362 | Also, this function will grab several keys based on varying modifiers. 363 | Namely, this accounts for all of the "trivial" modifiers that may have 364 | an effect on X events, but probably shouldn't effect key grabbing. (i.e., 365 | whether num lock or caps lock is on.) 366 | 367 | N.B. You should probably be using 'bind_key' or 'bind_global_key' instead. 368 | 369 | :param wid: A window identifier. 370 | :type wid: int 371 | :param modifiers: A modifier mask. 372 | :type modifiers: int 373 | :param key: A keycode. 374 | :type key: int 375 | :rtype: bool 376 | """ 377 | try: 378 | for mod in TRIVIAL_MODS: 379 | conn.core.GrabKeyChecked(True, wid, modifiers | mod, key, GM.Async, 380 | GM.Async).check() 381 | 382 | return True 383 | except xproto.BadAccess: 384 | return False 385 | 386 | def ungrab_key(wid, modifiers, key): 387 | """ 388 | Ungrabs a key that was grabbed by ``grab_key``. Similarly, it will return 389 | True on success and False on failure. 390 | 391 | When ungrabbing a key, the parameters to this function should be 392 | *precisely* the same as the parameters to ``grab_key``. 393 | 394 | :param wid: A window identifier. 395 | :type wid: int 396 | :param modifiers: A modifier mask. 397 | :type modifiers: int 398 | :param key: A keycode. 399 | :type key: int 400 | :rtype: bool 401 | """ 402 | try: 403 | for mod in TRIVIAL_MODS: 404 | conn.core.UngrabKeyChecked(key, wid, modifiers | mod).check() 405 | 406 | return True 407 | except xproto.BadAccess: 408 | return False 409 | 410 | def update_keyboard_mapping(e): 411 | """ 412 | Whenever the keyboard mapping is changed, this function needs to be called 413 | to update xpybutil's internal representing of the current keysym table. 414 | Indeed, xpybutil will do this for you automatically. 415 | 416 | Moreover, if something is changed that affects the current keygrabs, 417 | xpybutil will initiate a regrab with the changed keycode. 418 | 419 | :param e: The MappingNotify event. 420 | :type e: xcb.xproto.MappingNotifyEvent 421 | :rtype: void 422 | """ 423 | global __kbmap, __keysmods 424 | 425 | newmap = get_keyboard_mapping().reply() 426 | 427 | if e is None: 428 | __kbmap = newmap 429 | __keysmods = get_keys_to_mods() 430 | return 431 | 432 | if e.request == xproto.Mapping.Keyboard: 433 | changes = {} 434 | for kc in range(*get_min_max_keycode()): 435 | knew = get_keysym(kc, kbmap=newmap) 436 | oldkc = get_keycode(knew) 437 | if oldkc != kc: 438 | changes[oldkc] = kc 439 | 440 | __kbmap = newmap 441 | __regrab(changes) 442 | elif e.request == xproto.Mapping.Modifier: 443 | __keysmods = get_keys_to_mods() 444 | 445 | def __run_keybind_callbacks(e): 446 | """ 447 | A private function that intercepts all key press/release events, and runs 448 | their corresponding callback functions. Nothing much to see here, except 449 | that we must mask out the trivial modifiers from the state in order to 450 | find the right callback. 451 | 452 | Callbacks are called in the order that they have been added. (FIFO.) 453 | 454 | :param e: A Key{Press,Release} event. 455 | :type e: xcb.xproto.Key{Press,Release}Event 456 | :rtype: void 457 | """ 458 | kc, mods = e.detail, e.state 459 | for mod in TRIVIAL_MODS: 460 | mods &= ~mod 461 | 462 | key = (e.event, mods, kc) 463 | for cb in __keybinds.get(key, []): 464 | try: 465 | cb(e) 466 | except TypeError: 467 | cb() 468 | 469 | def __regrab(changes): 470 | """ 471 | Takes a dictionary of changes (mapping old keycode to new keycode) and 472 | regrabs any keys that have been changed with the updated keycode. 473 | 474 | :param changes: Mapping of changes from old keycode to new keycode. 475 | :type changes: dict 476 | :rtype: void 477 | """ 478 | for wid, mods, kc in __keybinds.keys(): 479 | if kc in changes: 480 | ungrab_key(wid, mods, kc) 481 | grab_key(wid, mods, changes[kc]) 482 | 483 | old = (wid, mods, kc) 484 | new = (wid, mods, changes[kc]) 485 | __keybinds[new] = __keybinds[old] 486 | del __keybinds[old] 487 | 488 | if conn is not None: 489 | update_keyboard_mapping(None) 490 | event.connect('MappingNotify', None, update_keyboard_mapping) 491 | 492 | -------------------------------------------------------------------------------- /examples/line_demo.py: -------------------------------------------------------------------------------- 1 | from draw import collide, simplify_transform 2 | from const import rounded, identity, get_matrix, transformed 3 | from const import get_translate, get_scale 4 | import numpy 5 | import persistent.document as pdocument 6 | from persistent.document import Expr, Ex 7 | from time import time as cur_time 8 | # from pyinstrument import Profiler 9 | from ast import literal_eval 10 | 11 | pdocument.scope = {"P": P, "cur_time": cur_time} 12 | 13 | #doc['drawing'].append(Node("text", value="hello world", p_botleft=P(100, 100))) 14 | #doc['drawing'][-1]["id"] = "console" 15 | #doc["console"]["value"] = "" 16 | 17 | def add_letter(node_id=None): 18 | node_id = node_id if node_id is not None else doc["editor.focus"] 19 | if doc["editor.key_name"] == "BackSpace": 20 | doc[node_id + ".value"] = doc[node_id + ".value"][:-1] 21 | else: 22 | doc[node_id + ".value"] += doc["editor.key_char"] 23 | 24 | def run_text(node_id, param="value"): 25 | try: 26 | co = compile(doc[node_id][param], "", "single") 27 | exec co in globals() 28 | except: 29 | traceback.print_exc() 30 | 31 | def clear(node_id): 32 | doc[node_id]["value"] = "" 33 | 34 | def key_press(event, key_name=None): 35 | return event.type == Event.key_press and\ 36 | (key_name is None or event.key_name == key_name) 37 | 38 | def run_button(): 39 | root = doc[doc["selection.root"]] 40 | txy = doc["editor.mouse_txy"] 41 | for child in reversed(root): 42 | if collide(child, txy): 43 | print "clicked on", child["id"] 44 | if "on_click" in child: 45 | run_text(child["id"], "on_click") 46 | return True 47 | return False 48 | 49 | def mouse_press(event, button=None): 50 | return event.type == Event.mouse_press and\ 51 | (button is None or event.button == int(button)) 52 | 53 | def mouse_release(event, button=None): 54 | return event.type == Event.mouse_release and\ 55 | (button is None or event.button == int(button)) 56 | 57 | 58 | def create_text(): 59 | doc["drawing"].append(Node("text", value="", 60 | p_botleft=doc["editor.mouse_txy"])) 61 | doc["editor.focus"] = doc["drawing"][-1]["id"] 62 | 63 | def edit_text(): 64 | root = doc[doc["selection.root"]] 65 | for child, transform in root.dfs(): 66 | # Problem: dfs includes child's transform and so does bbox. 67 | if child.name == "text" and\ 68 | collide(child, doc["editor.mouse_xy"], transform=transform, tolerance=8): 69 | doc["editor.focus"] = child["id"] 70 | return True 71 | return False 72 | 73 | def finished_edit_text(): 74 | node = doc[doc["editor.focus"]] 75 | text = node["value"] 76 | if text.startswith("!"): 77 | node["on_click"] = text[1:] 78 | elif text.startswith("="): 79 | node["value"] = Ex(text[1:], calc="reeval") 80 | 81 | def grab_point(): 82 | root = doc[doc["selection"]["root"]] 83 | for child, transform in root.dfs(): 84 | if child.name == "point" and\ 85 | collide(child, doc["editor.mouse_xy"], transform=transform, tolerance=8): 86 | doc["editor.drag_start"] = doc["editor.mouse_txy"] 87 | doc["editor.grabbed"] = child["id"] 88 | child.transforms["editor"] = Ex("('translate', `editor.mouse_txy - `editor.drag_start)", calc='on first read') 89 | return True 90 | return False 91 | 92 | def drop_point(): 93 | node = doc[doc["editor.grabbed"]] 94 | simplify_transform(node) 95 | doc["editor.drag_start"] = None 96 | doc["editor.grabbed"] = None 97 | 98 | def add_line(): 99 | doc["drawing"].append(Node("path", fill_color=None, children=[ 100 | Node("line", p_start=doc["editor.mouse_txy"], 101 | p_end=doc["editor.mouse_txy"] + P(50, 50))])) 102 | 103 | def add_line_start(): 104 | line = Node("line", p_start=doc["editor.mouse_txy"], 105 | p_end=doc["editor.mouse_txy"]) 106 | doc["drawing"].append(Node("path", fill_color=None, children=[line])) 107 | doc["editor.drag_start"] = doc["editor.mouse_txy"] 108 | doc["editor.grabbed"] = line[1]["id"] 109 | line[1].transforms["editor"] = Ex("('translate', `editor.mouse_txy - `editor.drag_start)", calc='on first read') 110 | 111 | def rectangle1(topleft, botright): 112 | topleft = topleft["value"] 113 | bottomright = botright["value"] 114 | if (topleft >= bottomright).all(): 115 | topleft, bottomright = bottomright, topleft 116 | topright = P(bottomright[0], topleft[1]) 117 | bottomleft = P(topleft[0], bottomright[1]) 118 | newnode = Node("path", skip_points=True, p_topleft=topleft, p_botright=bottomright, 119 | p_topright=topright, p_botleft=bottomleft, children = [ 120 | Node("line", start=Ex("`self.parent.topleft", "reeval"), end=Ex("`self.parent.topright", "reeval")), 121 | Node("line", start=Ex("`self.parent.topright", "reeval"), end=Ex("`self.parent.botright", "reeval")), 122 | Node("line", start=Ex("`self.parent.botright", "reeval"), end=Ex("`self.parent.botleft", "reeval")), 123 | Node("line", start=Ex("`self.parent.botleft", "reeval"), end=Ex("`self.parent.topleft", "reeval")), 124 | ]) 125 | return newnode 126 | 127 | #doc['drawing'].append(Node("group", id="rect", p_topleft=P(300, 300), p_botright=P(500, 400), children=[rectangle2()])) 128 | 129 | # Can't transform corners this way! Need corners to be a pair of points 130 | def topright(corners, transform): 131 | if numpy.array_equal(corners, (None, None)): 132 | return None 133 | return Node("point", value=P(transform.dot(numpy.append(corners[1], 1))[:2][0], 134 | transform.dot(numpy.append(corners[0], 1))[:2][1])) 135 | 136 | def botleft(corners, transform): 137 | return Node("point", value=P(transform.dot(numpy.append(corners[0], 1))[:2][0], 138 | transform.dot(numpy.append(corners[1], 1))[:2][1])) 139 | return Node("point", value=P(transform.dot(corners[0])[0], 140 | transform.dot(corners[1])[1])) 141 | 142 | pdocument.scope["topright"] = topright 143 | pdocument.scope["botleft"] = botleft 144 | 145 | 146 | def rectangle2(**params): 147 | newnode = Node("path", 148 | corners=exr("(`self.topleft.value, `self.botright.value)"), 149 | topright=exr("topright(`self.corners, (`self.parent).transform)"), 150 | botleft=exr("botleft(`self.corners, (`self.parent).transform)"), children = [ 151 | Node("line", start=exr("`self.parent.topleft"), 152 | end=exr("`self.parent.topright")), 153 | Node("line", start=exr("`self.parent.topright"), 154 | end=exr("`self.parent.botright")), 155 | Node("line", start=exr("`self.parent.botright"), 156 | end=exr("`self.parent.botleft")), 157 | Node("line", start=exr("`self.parent.botleft"), 158 | end=exr("`self.parent.topleft")), 159 | ], **params) 160 | return newnode 161 | 162 | #pdocument.scope["rectangle2"] = rectangle2 163 | #pdocument.scope["rectangle3"] = rectangle3 164 | 165 | def selection_bbox(node_id): 166 | newnode = Node("group", 167 | id="%s_bbox" % node_id, stroke_color=(0, 0.5, 0), 168 | ref=exr("`%s" % node_id), 169 | fill_color=None, skip_points=True, 170 | dash=([5,5],0), children=[ 171 | rectangle4(corners=exc("(`%s).bbox()" % node_id))]) 172 | return newnode 173 | 174 | # Problem with this approach above: Nodes are created all the time. id references are unstable... 175 | #def selection_bboxes(selected): 176 | # newnode = Node("group", children=[selection_bbox(node_id) 177 | # for node_id in selected]) 178 | # return newnode 179 | 180 | # pdocument.scope["selection_bboxes"] = selection_bboxes 181 | 182 | def toggle_selection(): 183 | root = doc[doc["selection"]["root"]] 184 | txy = doc["editor.mouse_txy"] 185 | for child in reversed(root): 186 | if collide(child, txy): 187 | print "clicked on", child["id"] 188 | if child["id"] in doc["editor.selected"]: 189 | selection_del(doc, child) 190 | else: 191 | selection_add(doc, child) 192 | break 193 | 194 | def selection_add(doc, node): 195 | node_id = node["id"] 196 | doc["selection"].append(selection_bbox(node_id)) 197 | doc["editor.selected.%s" % node_id] = True 198 | 199 | def selection_del(doc, node): 200 | node_id = node["id"] 201 | doc["selection"].remove(doc["%s_bbox" % node_id]) 202 | del doc["editor.selected.%s" % node_id] 203 | 204 | def least_common_ancestor(nodes): 205 | node = nodes[0] 206 | path_to_root = [node['id']] 207 | while node.parent: 208 | node = node.parent 209 | path_to_root.append(node['id']) 210 | for node in nodes: 211 | parent = node 212 | while parent['id'] not in path_to_root: 213 | parent = parent.parent 214 | path_to_root = path_to_root[path_to_root.index(parent['id']):] 215 | return doc[path_to_root[0]] 216 | 217 | def group_selection(): 218 | nodes = [doc[id] for id in doc["editor.selected"]] 219 | ancestor = least_common_ancestor(nodes) 220 | for node in nodes: 221 | node.deparent() 222 | parent = Node("group", children=[node.L for node in reversed(nodes)]) 223 | ancestor.L.append(parent) 224 | 225 | doc["editor.selected"] = pdocument.pmap() 226 | doc["selection"].clear() 227 | selection_add(doc, parent) 228 | 229 | def ungroup_selection(): 230 | for selected_node in list(doc["selection"]): 231 | node = selected_node["ref"] 232 | if node.name == "group": 233 | index = node.parent.index(node) 234 | parent = node.parent 235 | node.parent.remove(node) 236 | selection_del(doc, node) 237 | parent = parent.L 238 | parent.multi_insert(index, node) 239 | for child in reversed(node): 240 | selection_add(doc, child) 241 | 242 | def bboxes(nodes, transform=identity): 243 | boxes = [child.bbox(child.transform.dot(transform)) 244 | for child in nodes] 245 | boxes = zip(*[box for box in boxes 246 | if not numpy.array_equal(box, (None, None))]) 247 | if not boxes: 248 | return (None, None) 249 | return (numpy.min(numpy.vstack(boxes[0]), 0), 250 | numpy.max(numpy.vstack(boxes[1]), 0)) 251 | 252 | def align(nodes, side=0, axis=0, all_bbox=None): 253 | for node in nodes: 254 | if "align" in node.transforms: 255 | del node["transforms.align"] 256 | nodes = [node.L for node in nodes] 257 | all_bbox = bboxes(nodes) if all_bbox is None else all_bbox 258 | for node in nodes: 259 | diff = all_bbox[side][axis] - node.bbox(node.transform)[side][axis] 260 | if diff and axis == 0: 261 | node.transforms["align"] = ('translate', P(diff, 0)) 262 | elif diff and axis == 1: 263 | node.transforms["align"] = ('translate', P(0, diff)) 264 | 265 | def distribute(nodes, side=0, axis=0, spacing=10, all_bbox=None): 266 | for node in nodes: 267 | if "distribute" in node.transforms: 268 | del node["transforms.distribute"] 269 | nodes = [node.L for node in nodes] 270 | all_bbox = bboxes(nodes) if all_bbox is None else all_bbox 271 | val = all_bbox[side][axis] 272 | for node in nodes: 273 | bbox = node.bbox(node.transform) 274 | diff = val - bbox[side][axis] 275 | node.transforms["distribute"] = ('translate', 276 | P(diff, 0) if axis == 0 else P(0, diff)) 277 | val += abs(bbox[1-side][axis] - bbox[side][axis]) 278 | val += spacing 279 | 280 | def set_auto_layout(): 281 | for ref in doc["selection"]: 282 | node = ref["ref"] 283 | print "Layout", node, node.name 284 | # Maybe not needed 285 | if node.name not in ["group", "path"]: 286 | continue 287 | node.L["auto_layout"] = True 288 | node.L["side"] = 0 289 | node.L["axis"] = 0 290 | auto_layout_update(ref["ref"]) 291 | 292 | def update_layout(): 293 | for ref in doc["selection"]: 294 | auto_layout_update(ref["ref"]) 295 | 296 | def auto_layout_update(source): 297 | if source.get('auto_layout'): 298 | self = source if type(source) == Node else doc[source.node] 299 | nodes = self 300 | # Should make recursive auto_layout_update call here. 301 | for node in nodes: 302 | if "distribute" in node.transforms: 303 | del node["transforms.distribute"] 304 | node = node.L 305 | if "align" in node.transforms: 306 | del node["transforms.align"] 307 | all_bbox = bboxes(nodes) 308 | align(nodes, side=self["side"], axis=1-self["axis"], 309 | all_bbox=all_bbox) 310 | distribute(nodes, self["side"], self["axis"], 311 | all_bbox=all_bbox) 312 | 313 | def move_selection(): 314 | doc["editor.drag_start"] = doc["editor.mouse_xy"] 315 | for ref in doc["selection"]: 316 | # / get_scale(editor.zoom) 317 | ref["ref"].transforms["editor"] = exc("('translate', `editor.mouse_xy - `editor.drag_start)") 318 | 319 | def drop_selection(): 320 | for ref in doc["selection"]: 321 | node = ref["ref"] 322 | if "move" not in node.transforms: 323 | node.transforms["move"] = ("translate", P(0, 0)) 324 | node = node.L 325 | new = node["transforms.move"][1] + node["transforms.editor"][1] 326 | node.transforms["move"] = ("translate", new) 327 | del node.L.transforms["editor"] 328 | doc["editor.drag_start"] = None 329 | 330 | def add_rectangle(): 331 | doc["drawing"].append(rectangle2(p_topleft=doc["editor.mouse_xy"], 332 | p_botright=doc["editor.mouse_xy"] + P(50, 50))) 333 | 334 | def visualize_cb(node): 335 | from visualize import visualize 336 | doc['drawing'].append(Node("group", id="visualization", 337 | children=list(visualize(node)))) 338 | 339 | def add_visualize(): 340 | assert(len(doc["selection"]) == 1) 341 | node = doc["selection.0.ref"] 342 | pdocument.scope['visualize_cb'] = visualize_cb 343 | doc['editor.callbacks.visualize'] = Ex('visualize_cb(`%s)' % node['id']) 344 | 345 | profiler_state = "ended" 346 | def profile_start(): 347 | global profiler, profiler_state 348 | profiler_state = "started" 349 | print "Started profiler" 350 | profiler = Profiler() # or Profiler(use_signal=False), see below 351 | profiler.start() 352 | 353 | def profile_end(): 354 | global profiler, profiler_state 355 | profiler.stop() 356 | profiler_state = "ended" 357 | 358 | print(profiler.output_text(unicode=True, color=True)) 359 | 360 | def ended_profiler(): 361 | return profiler_state == "ended" 362 | 363 | def delete(): 364 | for ref in list(doc["selection"]): 365 | selection_del(doc, ref["ref"]) 366 | ref["ref"].deparent() 367 | 368 | def scroll(axis_side): 369 | axis, side = literal_eval(axis_side) 370 | #print "axis, side", axis, side, 371 | side = -1 if side == 0 else 1 372 | diff = P(50 * side, 0) if axis == 0 else P(0, 50 * side) 373 | #print diff 374 | scale = get_scale(doc['drawing'], "zoom") 375 | xy = get_translate(doc['drawing'], "scroll_xy") + diff / scale 376 | doc['drawing.transforms.scroll_xy'] = ("translate", xy) 377 | 378 | def shift_mouse_press(event, button=None): 379 | #if event.type == Event.mouse_press: 380 | # print "Shift" in event.mods, event.button 381 | return event.type == Event.mouse_press and\ 382 | "Shift" in event.mods and\ 383 | (button is None or event.button == int(button)) 384 | 385 | def control_mouse_press(event, button=None): 386 | return event.type == Event.mouse_press and\ 387 | "Control" in event.mods and\ 388 | (button is None or event.button == int(button)) 389 | 390 | def zoom(out): 391 | # Should conjugate by current mouse position 392 | out = literal_eval(out) 393 | zoom = get_scale(doc["drawing"], "zoom") / 1.25 if out else\ 394 | get_scale(doc["drawing"], "zoom") * 1.25 395 | doc["drawing"].transforms["zoom"] = ("scale", zoom) 396 | 397 | def add_child(): 398 | # Need multi-color selection? 399 | parent = doc["selection.0.ref"] 400 | child = doc["selection.1.ref"] 401 | #child.deparent() 402 | parent.append(child) 403 | selection_del(doc, child) 404 | update_layout() 405 | 406 | def add_sibling(): 407 | # Need multi-color selection? 408 | sibling = doc["editor.gui_selected"] 409 | parent = sibling.parent 410 | #child = doc["selection.0.ref"] 411 | index = parent.index(sibling) 412 | selection = [node["ref"] for node in doc['selection']] 413 | doc["editor.selected"] = pdocument.pmap() 414 | doc["selection"].clear() 415 | parent.multi_insert(index+1, selection) 416 | auto_layout_update(parent.L) 417 | 418 | # Need clipping and scrolling before this can be put into overlay 419 | # Maybe using only add_visualize would be enough? 420 | # Maybe want selecting a new item deselects the previous one? 421 | def update_gui_tree(): 422 | from visualize import visualize 423 | if 'gui_tree' in doc: 424 | doc['gui_tree'].deparent() 425 | # Should instead find the right index 426 | doc['overlay'].append(Node("group", 427 | children=list(visualize(doc['drawing'])))) 428 | 429 | def gui_select(): 430 | root = doc[doc["selection"]["root"]] 431 | xy = doc["editor.mouse_xy"] 432 | for child, transform in root.dfs(): 433 | if child.name not in ["group", "path"] and\ 434 | collide(child, xy, transform=transform, tolerance=8): 435 | print "gui elem selected", child["id"] 436 | doc["editor.gui_selected"] = exr("`%s" % child["id"]) 437 | break 438 | 439 | def doc_undo(): 440 | doc.undo_index -= 1 441 | doc.log("undo", doc.saved[doc.undo_index]) 442 | doc.dirty.clear() 443 | 444 | def doc_redo(): 445 | if doc.undo_index < -1: 446 | doc.undo_index += 1 447 | doc.log("redo", doc.saved[doc.undo_index]) 448 | doc.dirty.clear() 449 | 450 | def save_undo(): 451 | if doc.undo_index != -1: 452 | del doc.saved[doc.undo_index+1:] 453 | doc.saved.append(doc.m) 454 | doc.undo_index = -1 455 | 456 | def fail(): 457 | return False 458 | 459 | if __init__: 460 | doc.saved = [doc.m] 461 | doc.undo_index = -1 462 | doc.save("coverage_test.py") 463 | 464 | input_callbacks = """ 465 | exec = key_press(return) 466 | (~key_press(return) (key_press !add_letter(console) | @anything))* 467 | key_press(return) !run_text(console) !clear(console) 468 | button = mouse_press(1) ?run_button mouse_release(1) 469 | text = key_press(t) (?edit_text | !create_text) 470 | (~key_press(return) (key_press !add_letter | @anything))* 471 | key_press(return) !finished_edit_text 472 | move_point = key_press(e) ?grab_point (~key_press(e) @anything)* key_press(e) !drop_point 473 | new_line = key_press(l) !add_line 474 | new_line = key_press(l) (mouse_press(1) !add_line | ~key_press @anything)* 475 | new_line = key_press(l) 476 | (mouse_press(1) !add_line_start 477 | (~mouse_press(1) @anything)* mouse_press(1) !drop_point 478 | | ~key_press @anything)* 479 | new_rect = key_press(r) !add_rectangle 480 | select = key_press(s) !toggle_selection 481 | group = key_press(g) !group_selection 482 | ungroup = key_press(u) !ungroup_selection 483 | move_selection = key_press(m) !move_selection 484 | (~key_press(m) @anything)* key_press(m) !drop_selection 485 | layout = key_press(f) !set_auto_layout 486 | update_layout = key_press(q) !update_layout 487 | visualize = key_press(v) !add_visualize 488 | profile_start = key_press(p) ?ended_profiler !profile_start 489 | profile_end = key_press(p) !profile_end 490 | delete = key_press(x) !delete 491 | zoom = control_mouse_press(4) !zoom(False) 492 | | control_mouse_press(5) !zoom(True) 493 | scroll = shift_mouse_press(4) !scroll(0, 1) 494 | | shift_mouse_press(5) !scroll(0, 0) 495 | | mouse_press(4) !scroll(1, 1) 496 | | mouse_press(5) !scroll(1, 0) 497 | gui_add_child = key_press(c) !add_child 498 | gui_add_sibling = key_press(a) !add_sibling 499 | update_gui_tree = key_press(h) update_gui_tree 500 | gui_select = mouse_press(3) !gui_select mouse_release(3) 501 | undo = key_press(z) !doc_undo ?fail 502 | redo = key_press(y) !doc_redo ?fail 503 | 504 | command = @exec | @button | @text | @move_point | @new_line | @new_rect 505 | | @select | @group | @ungroup | @layout | @move_selection 506 | | @update_layout | @visualize | @delete 507 | | @profile_start | @profile_end 508 | | @zoom | @scroll 509 | | @gui_add_child | @gui_add_sibling | @update_gui_tree | @gui_select 510 | | @undo | @redo 511 | grammar = (@command !save_undo | @anything)* 512 | """ 513 | -------------------------------------------------------------------------------- /examples/functions.py: -------------------------------------------------------------------------------- 1 | from draw import collide, simplify_transform 2 | from const import rounded, identity, get_matrix, transformed, default_get 3 | from const import get_translate, get_scale 4 | import numpy 5 | import persistent_doc.document as pdocument 6 | from persistent_doc.document import Expr, Ex 7 | from time import time as cur_time 8 | # from pyinstrument import Profiler 9 | from ast import literal_eval 10 | 11 | pdocument.scope = {"P": P, "cur_time": cur_time, "transformed": transformed} 12 | 13 | #doc['drawing'].append(Node("text", value="hello world", p_botleft=P(100, 100))) 14 | #doc['drawing'][-1]["id"] = "console" 15 | #doc["console"]["value"] = "" 16 | 17 | def add_letter(node_id=None): 18 | node_id = node_id if node_id is not None else doc["editor.focus"] 19 | if doc["editor.key_name"] == "backspace": 20 | doc[node_id + ".value"] = doc[node_id + ".value"][:-1] 21 | else: 22 | doc[node_id + ".value"] += doc["editor.key_char"] 23 | 24 | def run_text(node_id, param="value"): 25 | try: 26 | co = compile(doc[node_id][param], "", "single") 27 | exec co in globals() 28 | except: 29 | traceback.print_exc() 30 | 31 | def clear(node_id): 32 | doc[node_id]["value"] = "" 33 | 34 | def key_press(event, key_name=None): 35 | return event.type == Event.key_press and\ 36 | (key_name is None or (event.key_name == key_name and (not key_name.isalpha or "shift" not in event.mods))) 37 | 38 | def shift_key_press(event, key_name=None): 39 | return event.type == Event.key_press and "shift" in event.mods and\ 40 | (key_name is None or event.key_name.lower() == key_name) 41 | 42 | def run_button(): 43 | root = doc[doc["selection.root"]] 44 | txy = doc["editor.mouse_txy"] 45 | for child in reversed(root): 46 | if collide(child, txy): 47 | print "clicked on", child["id"] 48 | if "on_click" in child: 49 | run_text(child["id"], "on_click") 50 | return True 51 | return False 52 | 53 | def mouse_press(event, button=None): 54 | return event.type == Event.mouse_press and\ 55 | (button is None or event.button == int(button)) 56 | 57 | def mouse_release(event, button=None): 58 | return event.type == Event.mouse_release and\ 59 | (button is None or event.button == int(button)) 60 | 61 | 62 | def create_text(): 63 | doc[doc['selection.root']].append(Node("text", value="", 64 | p_botleft=doc["editor.mouse_txy"])) 65 | doc["editor.focus"] = doc[doc['selection.root']][-1]["id"] 66 | 67 | def edit_text(): 68 | root = doc[doc["selection.root"]] 69 | mxy = doc["editor.mouse_xy"] if root["id"] == "drawing" else \ 70 | doc["editor.mouse_txy"] 71 | for child, transform in root.dfs(): 72 | # Problem: dfs includes child's transform and so does bbox. 73 | if child.name == "text" and\ 74 | collide(child, mxy, transform=transform, tolerance=8): 75 | doc["editor.focus"] = child["id"] 76 | return True 77 | return False 78 | 79 | def finished_edit_text(): 80 | node = doc[doc["editor.focus"]] 81 | text = node["value"] 82 | if text.startswith("!"): 83 | node["on_click"] = text[1:] 84 | elif text.startswith("="): 85 | node["value"] = Ex(text[1:], calc="reeval") 86 | 87 | def grab_point(): 88 | root = doc[doc["selection.root"]] 89 | mxy = doc["editor.mouse_xy"] if root["id"] == "drawing" else \ 90 | doc["editor.mouse_txy"] 91 | for child, transform in root.dfs(): 92 | if child.name == "point" and\ 93 | collide(child, mxy, transform=transform, tolerance=8): 94 | doc["editor.drag_start"] = doc["editor.mouse_txy"] 95 | doc["editor.grabbed"] = child["id"] 96 | child.transforms["editor"] = Ex("('translate', `editor.mouse_txy - `editor.drag_start)", calc='on first read') 97 | return True 98 | return False 99 | 100 | def drop_point(): 101 | node = doc[doc["editor.grabbed"]] 102 | simplify_transform(node) 103 | doc["editor.drag_start"] = None 104 | doc["editor.grabbed"] = None 105 | 106 | def add_line(): 107 | doc[doc['selection.root']].append(Node("path", fill_color=None, children=[ 108 | Node("line", p_start=doc["editor.mouse_txy"], 109 | p_end=doc["editor.mouse_txy"] + P(50, 50))])) 110 | 111 | def rectangle1(topleft, botright): 112 | topleft = topleft["value"] 113 | bottomright = botright["value"] 114 | if (topleft >= bottomright).all(): 115 | topleft, bottomright = bottomright, topleft 116 | topright = P(bottomright[0], topleft[1]) 117 | bottomleft = P(topleft[0], bottomright[1]) 118 | newnode = Node("path", skip_points=True, p_topleft=topleft, p_botright=bottomright, 119 | p_topright=topright, p_botleft=bottomleft, children = [ 120 | Node("line", start=Ex("`self.parent.topleft", "reeval"), end=Ex("`self.parent.topright", "reeval")), 121 | Node("line", start=Ex("`self.parent.topright", "reeval"), end=Ex("`self.parent.botright", "reeval")), 122 | Node("line", start=Ex("`self.parent.botright", "reeval"), end=Ex("`self.parent.botleft", "reeval")), 123 | Node("line", start=Ex("`self.parent.botleft", "reeval"), end=Ex("`self.parent.topleft", "reeval")), 124 | ]) 125 | return newnode 126 | 127 | #doc['drawing'].append(Node("group", id="rect", p_topleft=P(300, 300), p_botright=P(500, 400), children=[rectangle2()])) 128 | 129 | # Can't transform corners this way! Need corners to be a pair of points 130 | def topright(corners): 131 | if numpy.array_equal(corners, (None, None)): 132 | return None 133 | return Node("point", value=P(corners[1][0], corners[0][1])) 134 | 135 | def botleft(corners): 136 | return Node("point", value=P(corners[0][0], corners[1][1])) 137 | 138 | pdocument.scope["topright"] = topright 139 | pdocument.scope["botleft"] = botleft 140 | 141 | 142 | def rectangle2(**params): 143 | params_str = " ".join("%s=%s" % (key, _repr(value)) 144 | for key, value in params.items()) 145 | inp = """ 146 | group: 147 | corners=exc('(transformed(`self.topleft), transformed(`self.botright))') 148 | topright=exc('topright(`self.corners)') 149 | botleft=exc('botleft(`self.corners)') 150 | %s 151 | path: skip_points=True 152 | line: start=exc('`self.parent.parent.topleft') 153 | end=exc('`self.parent.parent.topright') 154 | line: start=exc('`self.parent.parent.topright') 155 | end=exc('`self.parent.parent.botright') 156 | line: start=exc('`self.parent.parent.botright') 157 | end=exc('`self.parent.parent.botleft') 158 | line: start=exc('`self.parent.parent.botleft') 159 | end=exc('`self.parent.parent.topleft') 160 | """ % params_str 161 | return tree_lang.parse(inp, locals=globals()) 162 | 163 | #pdocument.scope["rectangle2"] = rectangle2 164 | #pdocument.scope["rectangle3"] = rectangle3 165 | 166 | def selection_bbox(node_id): 167 | newnode = Node("group", 168 | id="%s_bbox" % node_id, stroke_color=(0, 0.5, 0), 169 | ref=exr("`%s" % node_id), 170 | fill_color=None, skip_points=True, 171 | dash=([5,5],0), children=[ 172 | rectangle4(corners=exc("(`%s).bbox()" % node_id))]) 173 | return newnode 174 | 175 | # Problem with this approach above: Nodes are created all the time. id references are unstable... 176 | #def selection_bboxes(selected): 177 | # newnode = Node("group", children=[selection_bbox(node_id) 178 | # for node_id in selected]) 179 | # return newnode 180 | 181 | # pdocument.scope["selection_bboxes"] = selection_bboxes 182 | 183 | def toggle_selection(): 184 | root = doc[doc["selection"]["root"]] 185 | # Assumes root.combined_transform().dot(root.transform) is the identity. 186 | txy = doc["editor.mouse_txy"] 187 | for child in reversed(root): 188 | if collide(child, txy): 189 | print "clicked on", child["id"] 190 | if child["id"] in doc["editor.selected"]: 191 | selection_del(doc, child) 192 | else: 193 | selection_add(doc, child) 194 | break 195 | 196 | def selection_add(doc, node): 197 | node_id = node["id"] 198 | doc["selection"].append(selection_bbox(node_id)) 199 | doc["editor.selected.%s" % node_id] = True 200 | 201 | def selection_del(doc, node): 202 | node_id = node["id"] 203 | doc["selection"].remove(doc["%s_bbox" % node_id]) 204 | del doc["editor.selected.%s" % node_id] 205 | 206 | def least_common_ancestor(nodes): 207 | node = nodes[0] 208 | path_to_root = [node['id']] 209 | while node.parent: 210 | node = node.parent 211 | path_to_root.append(node['id']) 212 | for node in nodes: 213 | parent = node 214 | while parent['id'] not in path_to_root: 215 | parent = parent.parent 216 | path_to_root = path_to_root[path_to_root.index(parent['id']):] 217 | return doc[path_to_root[0]] 218 | 219 | def group_selection(): 220 | nodes = [ref['ref'] for ref in doc["selection"]] 221 | ancestor = least_common_ancestor(nodes) 222 | for node in nodes: 223 | node.deparent() 224 | parent = Node("group", children=[node.L for node in reversed(nodes)]) 225 | ancestor.L.append(parent) 226 | 227 | doc["editor.selected"] = pdocument.pmap() 228 | doc["selection"].clear() 229 | selection_add(doc, parent) 230 | 231 | def ungroup_selection(): 232 | for selected_node in list(doc["selection"]): 233 | node = selected_node["ref"] 234 | if node.name == "group": 235 | index = node.parent.index(node) 236 | parent = node.parent 237 | node.parent.remove(node) 238 | selection_del(doc, node) 239 | parent = parent.L 240 | parent.multi_insert(index, node) 241 | for child in reversed(node): 242 | selection_add(doc, child) 243 | 244 | def bboxes(nodes, transform=identity): 245 | boxes = [child.bbox(child.transform.dot(transform)) 246 | for child in nodes] 247 | boxes = zip(*[box for box in boxes 248 | if not numpy.array_equal(box, (None, None))]) 249 | if not boxes: 250 | return (None, None) 251 | return (numpy.min(numpy.vstack(boxes[0]), 0), 252 | numpy.max(numpy.vstack(boxes[1]), 0)) 253 | 254 | def align(nodes, side=0, axis=0, all_bbox=None): 255 | for node in nodes: 256 | if "align" in node.transforms: 257 | del node["transforms.align"] 258 | nodes = [node.L for node in nodes] 259 | all_bbox = bboxes(nodes) if all_bbox is None else all_bbox 260 | for node in nodes: 261 | diff = all_bbox[side][axis] - node.bbox(node.transform)[side][axis] 262 | if diff and axis == 0: 263 | node.transforms["align"] = ('translate', P(diff, 0)) 264 | elif diff and axis == 1: 265 | node.transforms["align"] = ('translate', P(0, diff)) 266 | 267 | def distribute(nodes, side=0, axis=0, spacing=10, all_bbox=None): 268 | for node in nodes: 269 | if "distribute" in node.transforms: 270 | del node["transforms.distribute"] 271 | nodes = [node.L for node in nodes] 272 | all_bbox = bboxes(nodes) if all_bbox is None else all_bbox 273 | val = all_bbox[side][axis] 274 | for node in nodes: 275 | bbox = node.bbox(node.transform) 276 | diff = val - bbox[side][axis] 277 | node.transforms["distribute"] = ('translate', 278 | P(diff, 0) if axis == 0 else P(0, diff)) 279 | val += abs(bbox[1-side][axis] - bbox[side][axis]) 280 | val += spacing 281 | 282 | def set_auto_layout(): 283 | for ref in doc["selection"]: 284 | node = ref["ref"] 285 | print "Layout", node, node.name 286 | # Maybe not needed 287 | if node.name not in ["group", "path"]: 288 | continue 289 | node.L["auto_layout"] = True 290 | node.L["side"] = 0 291 | node.L["axis"] = 0 292 | auto_layout_update(ref["ref"]) 293 | 294 | def update_layout(): 295 | for ref in doc["selection"]: 296 | auto_layout_update(ref["ref"]) 297 | 298 | def auto_layout_update(source): 299 | if source.get('auto_layout'): 300 | self = source if type(source) == Node else doc[source.node] 301 | nodes = self 302 | # Should make recursive auto_layout_update call here. 303 | for node in nodes: 304 | if "distribute" in node.transforms: 305 | del node["transforms.distribute"] 306 | node = node.L 307 | if "align" in node.transforms: 308 | del node["transforms.align"] 309 | all_bbox = bboxes(nodes) 310 | align(nodes, side=self["side"], axis=1-self["axis"], 311 | all_bbox=all_bbox) 312 | distribute(nodes, self["side"], self["axis"], 313 | all_bbox=all_bbox) 314 | 315 | def move_selection(): 316 | doc["editor.drag_start"] = doc["editor.mouse_xy"] 317 | for ref in doc["selection"]: 318 | # / get_scale(editor.zoom) 319 | ref["ref"].transforms["editor"] = exc("('translate', `editor.mouse_xy - `editor.drag_start)") 320 | 321 | def drop_selection(): 322 | for ref in doc["selection"]: 323 | node = ref["ref"] 324 | if "move" not in node.transforms: 325 | node.transforms["move"] = ("translate", P(0, 0)) 326 | node = node.L 327 | new = node["transforms.move"][1] + node["transforms.editor"][1] 328 | node.transforms["move"] = ("translate", new) 329 | del node.L.transforms["editor"] 330 | doc["editor.drag_start"] = None 331 | 332 | def add_rectangle(): 333 | doc[doc['selection.root']].append(rectangle2(px_topleft=doc["editor.mouse_xy"], 334 | px_botright=doc["editor.mouse_xy"] + P(50, 50))) 335 | 336 | def visualize_cb(node): 337 | from visualize import visualize 338 | if "visualization" in doc.m: 339 | doc["visualization"].deparent() 340 | doc[doc['selection.root']].append(Node("group", id="visualization", 341 | children=list(visualize(node)))) 342 | 343 | def add_visualize(): 344 | assert(len(doc["selection"]) == 1) 345 | node = doc["selection.0.ref"] 346 | #visualize_cb(node) 347 | pdocument.scope['visualize_cb'] = visualize_cb 348 | doc['editor.callbacks.visualize'] = Ex('visualize_cb(`%s)' % node['id']) 349 | 350 | profiler_state = "ended" 351 | def profile_start(): 352 | global profiler, profiler_state 353 | profiler_state = "started" 354 | print "Started profiler" 355 | profiler = Profiler() # or Profiler(use_signal=False), see below 356 | profiler.start() 357 | 358 | def profile_end(): 359 | global profiler, profiler_state 360 | profiler.stop() 361 | profiler_state = "ended" 362 | 363 | print(profiler.output_text(unicode=True, color=True)) 364 | 365 | def ended_profiler(): 366 | return profiler_state == "ended" 367 | 368 | def delete(): 369 | for ref in list(doc["selection"]): 370 | selection_del(doc, ref["ref"]) 371 | ref["ref"].deparent() 372 | 373 | def scroll(axis_side): 374 | axis, side = literal_eval(axis_side) 375 | #print "axis, side", axis, side, 376 | side = -1 if side == 0 else 1 377 | diff = P(50 * side, 0) if axis == 0 else P(0, 50 * side) 378 | #print diff 379 | scale = get_scale(doc['drawing'], "zoom") 380 | xy = get_translate(doc['drawing'], "scroll_xy") + diff / scale 381 | doc['drawing.transforms.scroll_xy'] = ("translate", xy) 382 | 383 | def shift_mouse_press(event, button=None): 384 | return event.type == Event.mouse_press and\ 385 | "shift" in event.mods and\ 386 | (button is None or event.button == int(button)) 387 | 388 | def control_mouse_press(event, button=None): 389 | return event.type == Event.mouse_press and\ 390 | "control" in event.mods and\ 391 | (button is None or event.button == int(button)) 392 | 393 | def zoom(out): 394 | # Should conjugate by current mouse position 395 | out = literal_eval(out) 396 | zoom = get_scale(doc["drawing"], "zoom") / 1.25 if out else\ 397 | get_scale(doc["drawing"], "zoom") * 1.25 398 | doc["drawing"].transforms["zoom"] = ("scale", zoom) 399 | 400 | def add_child(): 401 | # Need multi-color selection? 402 | parent = doc["selection.0.ref"] 403 | child = doc["selection.1.ref"] 404 | #child.deparent() 405 | parent.append(child) 406 | selection_del(doc, child) 407 | update_layout() 408 | 409 | def add_sibling(): 410 | # Need multi-color selection? 411 | sibling = doc["editor.gui_selected"] 412 | parent = sibling.parent 413 | #child = doc["selection.0.ref"] 414 | index = parent.index(sibling) 415 | selection = [node["ref"] for node in doc['selection']] 416 | doc["editor.selected"] = pdocument.pmap() 417 | doc["selection"].clear() 418 | parent.multi_insert(index+1, selection) 419 | auto_layout_update(parent.L) 420 | 421 | # Need clipping and scrolling before this can be put into overlay 422 | # Maybe using only add_visualize would be enough? 423 | # Maybe want selecting a new item deselects the previous one? 424 | def update_gui_tree(): 425 | from visualize import visualize 426 | if 'gui_tree' in doc: 427 | doc['gui_tree'].deparent() 428 | # Should instead find the right index 429 | doc['overlay'].append(Node("group", 430 | children=list(visualize(doc[doc['selection.root']])))) 431 | 432 | def gui_select(): 433 | root = doc[doc["selection"]["root"]] 434 | mxy = doc["editor.mouse_xy"] if root["id"] == "drawing" else \ 435 | doc["editor.mouse_txy"] 436 | for child, transform in root.dfs(): 437 | if child.name not in ["group", "path"] and\ 438 | collide(child, mxy, transform=transform, tolerance=8): 439 | print "gui elem selected", child["id"] 440 | doc["editor.gui_selected"] = exr("`%s" % child["id"]) 441 | break 442 | 443 | def doc_undo(): 444 | if doc.undo_index == -1: 445 | save_undo() 446 | doc.undo_index -= 1 447 | doc.undo_index -= 1 448 | doc.log("undo", doc.saved[doc.undo_index]) 449 | doc.dirty.clear() 450 | 451 | def doc_redo(): 452 | if doc.undo_index < -1: 453 | doc.undo_index += 1 454 | doc.log("redo", doc.saved[doc.undo_index]) 455 | doc.dirty.clear() 456 | 457 | def save_undo(): 458 | if doc.undo_index != -1: 459 | del doc.saved[doc.undo_index+1:] 460 | doc.saved.append(doc.m) 461 | doc.undo_index = -1 462 | 463 | def fail(): 464 | # Do not write undo/redo events into history. 465 | return False 466 | 467 | def duplicate(): 468 | id_ = doc['editor.selected'].keys()[0] 469 | node = doc[id_] 470 | doc[doc['selection.root']].append(Node("group", render=exr('[`%s]' % id_), 471 | skip_points=True, id=id_ + "_copy", 472 | transforms={"clone": ('translate', P(10, 10))})) 473 | 474 | def paste_selection(): 475 | for id_ in doc['editor.selected']: 476 | doc['drawing'].append(doc[id_].deepcopy()) 477 | 478 | def selected_nodes(): 479 | return [ref['ref'] for ref in doc['selection']] 480 | 481 | def clear_selection(): 482 | for node in selected_nodes(): 483 | selection_del(doc, node) 484 | 485 | if __init__: 486 | doc.saved = [doc.m] 487 | doc.undo_index = -1 488 | doc['overlay.transforms'] = exr('`drawing.transforms') 489 | doc.sync() 490 | 491 | input_callbacks = """ 492 | exec = key_press(return) 493 | (~key_press(return) (key_press !add_letter(console) | @anything))* 494 | key_press(return) !run_text(console) !clear(console) 495 | button = mouse_press(1) ?run_button mouse_release(1) 496 | text = key_press(t) (?edit_text | !create_text) 497 | (~key_press(return) (key_press !add_letter | @anything))* 498 | key_press(return) !finished_edit_text 499 | move_point = key_press(e) ?grab_point (~key_press(e) @anything)* key_press(e) !drop_point 500 | new_line = key_press(l) !add_line 501 | new_rect = key_press(r) !add_rectangle 502 | select = key_press(s) !toggle_selection 503 | group = key_press(g) !group_selection 504 | ungroup = key_press(u) !ungroup_selection 505 | move_selection = key_press(m) !move_selection 506 | (~key_press(m) @anything)* key_press(m) !drop_selection 507 | layout = key_press(f) !set_auto_layout 508 | update_layout = key_press(q) !update_layout 509 | visualize = key_press(v) !add_visualize 510 | profile_start = key_press(p) ?ended_profiler !profile_start 511 | profile_end = key_press(p) !profile_end 512 | delete = key_press(x) !delete 513 | zoom = control_mouse_press(4) !zoom(False) 514 | | control_mouse_press(5) !zoom(True) 515 | scroll = shift_mouse_press(4) !scroll(0, 1) 516 | | shift_mouse_press(5) !scroll(0, 0) 517 | | mouse_press(4) !scroll(1, 1) 518 | | mouse_press(5) !scroll(1, 0) 519 | gui_add_child = key_press(c) !add_child 520 | gui_add_sibling = key_press(a) !add_sibling 521 | update_gui_tree = key_press(h) update_gui_tree 522 | gui_select = mouse_press(3) !gui_select mouse_release(3) 523 | undo = key_press(z) !doc_undo ?fail 524 | redo = shift_key_press(z) !doc_redo ?fail 525 | paste = key_press(c) !paste_selection 526 | clear_selection = key_press(escape) !clear_selection 527 | 528 | command = @exec | @button | @text | @move_point | @new_line | @new_rect 529 | | @select | @group | @ungroup | @layout | @move_selection 530 | | @update_layout | @visualize | @delete 531 | | @profile_start | @profile_end 532 | | @zoom | @scroll 533 | | @paste | @clear_selection 534 | | @gui_add_sibling | @update_gui_tree | @gui_select 535 | | @undo | @redo 536 | grammar = (@command !save_undo | @anything)* 537 | """ 538 | # | @gui_add_child 539 | -------------------------------------------------------------------------------- /flow_editor.py: -------------------------------------------------------------------------------- 1 | import persistent_doc.document as pdocument 2 | from node import Node, _repr 3 | import node 4 | 5 | from const import P, exr, exc, Ex 6 | import compgeo 7 | from uielem import uidict, UI 8 | try: 9 | from tkui import TkTerp, Entry, ScrolledText 10 | import tkui 11 | except: 12 | from tkui import tkui 13 | from tkui.tkui import TkTerp, Entry, ScrolledText 14 | from Tkinter import Tk, Label, Frame, Button, Canvas, Checkbutton, Toplevel 15 | from config import BACKEND, LOG_EVENTS, MAINLOOP, DRAW_FREQUENCY, POLL_FREQUENCY 16 | import tree_lang 17 | import gui_lang 18 | import pdb 19 | from pdb import set_trace as bp 20 | import traceback, sys 21 | import os 22 | 23 | import time 24 | import logging 25 | 26 | logformat = "[%(name)8s:%(levelno)3s] --- %(message)s" # (%(filename)s:%(lineno)s)" 27 | logging.basicConfig(level=logging.DEBUG, format=logformat) 28 | for name in ["draw", "poll", "proc", "event", "callback", "tk_draw", "ogl_draw", "sdl_draw", "tree", "transform", "eval", "key", "document", "recalc"]: 29 | logging.getLogger(name).propagate = False 30 | for name in ["tree", "eval", "key"]: 31 | logging.getLogger(name).propagate = True 32 | event_logger = logging.getLogger("poll") 33 | tree_logger = logging.getLogger("tree") 34 | eval_logger = logging.getLogger("eval") 35 | key_logger = logging.getLogger("key") 36 | draw_logger = logging.getLogger("draw") 37 | cb_logger = logging.getLogger("callback") 38 | recalc_logger = logging.getLogger("recalc") 39 | 40 | if BACKEND == "xcb": 41 | from xcb_doc import Surface 42 | elif BACKEND == "tkinter": 43 | from tkinter_doc import TkCanvas 44 | from wrap_event import tk_bind 45 | elif BACKEND == "opengl": 46 | from OpenGL import GLUT 47 | GLUT.glutInit() 48 | GLUT.glutInitDisplayMode(GLUT.GLUT_RGB|GLUT.GLUT_DEPTH) 49 | from ogl_doc import OGLCanvas 50 | elif BACKEND == "sdl": 51 | from sdl_doc import SDLCanvas, pygame 52 | pygame.init() 53 | pygame.font.init() 54 | 55 | class Document(pdocument.Document): 56 | def __init__(self, surface, root): 57 | pdocument.Document.__init__(self, root) 58 | self.surface = surface 59 | self.tree_ui = None 60 | self.tree_doc = None 61 | 62 | def draw_loop(self, timer, delay): 63 | starttime = time.time() 64 | try: 65 | drawn = self.surface.update_all() 66 | except: 67 | print "Error drawing" 68 | self.last_tb = sys.exc_traceback 69 | traceback.print_exc() 70 | draw_logger.debug("Drawtime %.5f" % (time.time() - starttime)) 71 | if delay is not None: 72 | timer.after(delay, self.draw_loop, timer, delay) 73 | 74 | def layer_root(self, node): 75 | if hasattr(node, "node"): 76 | node = node.node 77 | # Assuming all root children are layers (which might not be true!) 78 | while "id" not in node or node not in self.tree_root: 79 | node = node.parent 80 | return node 81 | 82 | def save(self, filename=None): 83 | filename = doc['editor'].get("filename", "saved_doc.py")\ 84 | if filename is None else filename 85 | import node 86 | from shutil import copyfile 87 | timestr = time.strftime("%Y%m%d-%H%M%S") 88 | f = open(filename, "w") 89 | f.write("from node import Node\n") 90 | f.write("from numpy import array\n") 91 | f.write("from const import P, exr, exc\n") 92 | f.write("from persistent_doc.document import pmap\n") 93 | f.write("import node\n") 94 | f.write("node.node_num = %s\n" % node.node_num) 95 | f.write("root = %s" % self.tree_root.L.code()) 96 | f.close() 97 | if not os.path.exists("saves"): 98 | os.makedirs("saves") 99 | copyfile(filename, os.path.join("saves", "%s-%s" % (filename, timestr))) 100 | 101 | def load(self, filename=None): 102 | filename = doc['editor'].get("filename", "saved_doc.py")\ 103 | if filename is None else filename 104 | # A bit dangerous because of eval at the moment. 105 | if filename.endswith(".py"): 106 | filename = filename[:-3] 107 | import importlib 108 | # Clear doc dict 109 | doc.log("load_doc", pdocument.pmap()) 110 | doc.set_root(Node("group")) 111 | module = importlib.import_module(filename) 112 | reload(module) 113 | doc.set_root(module.root) 114 | 115 | def update_text(self, *args, **kwargs): 116 | if not self.tree_ui: 117 | return 118 | id_ = doc.m.get('editor.text_root') 119 | if id_ is None: 120 | newtext = "" 121 | else: 122 | newtext = "\n".join(self[id_].pprint_string()) 123 | #newtext = "\n".join(self.tree_root.pprint_string()) 124 | #newtext = "" 125 | #newtext = "\n".join(self.root.pprint_string()) 126 | #tree_logger.debug("Text changed %s" % (uidict["tree"].get(1.0, 'end') != newtext+"\n")) 127 | if self.tree_ui.text != newtext+"\n": 128 | if self.tree_doc: 129 | self.tree_doc["drawing"].replace(list(visualize.visualize(self.tree_root))) 130 | self.tree_ui.text = newtext 131 | 132 | def icons(): 133 | inp = """ 134 | group: 135 | arc: id='point_icon' radius=5 fill_color=(0, 0, 0.5) p_center=P(0, 0) 136 | arc: id='text_icon' radius=10 fill_color=(0, 0, 1) p_center=P(0, 0) 137 | arc: id='arc_icon' radius=10 fill_color=(0, 0, 0.8) p_center=P(0, 0) 138 | rectangle: id='rectangle_icon' 139 | p_bottomright=P(10, 8) p_topleft=P(-10, -8) 140 | rectangle: id='square_icon' p_bottomright=P(8, 8) p_topleft=P(-8, -8) 141 | group: id='line_icon' skip_points=True 142 | rectangle: stroke_color=(0, 0, 0) 143 | p_bottomright=P(10, 10) p_topleft=P(-10, -10) 144 | line: stroke_color=(0, 0, 0) p_start=(-5, 0) p_end=(5, 0) 145 | group: id='path_icon' skip_points=True 146 | rectangle: stroke_color=(0, 0, 0) 147 | p_bottomright=P(10, 10) p_topleft=P(-10, -10) 148 | path: stroke_color=(0, 0, 0) 149 | line: p_start=P(-8, 2) p_end=P(-4, 6) 150 | line: p_start=P(-4, 6) p_end=P(4, -6) 151 | line: p_start=P(4, -6) p_end=P(8, -2) 152 | group: id='group_icon' 153 | arc: stroke_color=(0, 0, 0) radius=12 p_center=P(0, 0) 154 | arc: radius=4 fill_color=(0, 0, 0.8) p_center=P(0, -5) 155 | arc: radius=4 fill_color=(0, 0.8, 0) p_center=P(5, 4) 156 | arc: radius=4 fill_color=(0.8, 0, 0) p_center=P(-5, 4) 157 | group: id='ref_icon' 158 | path: stroke_color=(0, 0, 0) 159 | line: child_id='0' p_start=P(-10, 0) p_end=P(10, 0) 160 | path: stroke_color=(0, 0, 0) fill_color=(0, 0, 0) 161 | line: p_start=P(10, 0) p_end=P(0, -5) 162 | line: start=exr('`self.parent.parent.0.end') p_end=P(5, 0) 163 | line: start=exr('`self.parent.parent.1.end') p_end=P(0, 5) 164 | line: start=exr('`self.parent.parent.2.end') 165 | end=exr('`self.parent.parent.0.start') 166 | """ 167 | return tree_lang.parse(inp, locals=globals()) 168 | 169 | def test_drawing(): 170 | return [ 171 | Node("group", id="edit node"), 172 | Node("line", id="aline", dash=([10,10], 0), stroke_color=(0, 0, 0), 173 | transform = ("linear", numpy.array([[1,0,30],[0,1,0],[0,0,1]])), 174 | children = [Node("point", child_id="start", value=P(0, 0)), 175 | Node("point", child_id="end", value=P(100, 100))]), 176 | Node("path", id="square", stroke_color=(0.5, 0, 0), fill_color=(0, 1, 0), children=[ 177 | Node("line", child_id="1", p_start=P(200, 200), p_end=P(250, 200)), 178 | Node("line", child_id="2", r_start="square.1.end", r_end="square.3.start"), 179 | Node("line", child_id="3", p_start=P(250, 250), p_end=P(200, 250)), 180 | Node("line", child_id="4", r_start="square.3.end", r_end="square.1.start") ]), 181 | Node("arc", id="aarc", fill_color=(0.5, 0, 0), p_center=P(200, 50), radius=50, 182 | angle=(0, 5*math.pi/8)), ] 183 | 184 | 185 | def debug(*args, **kwargs): 186 | pdb.pm() 187 | 188 | def doc_debug(): 189 | doc.pm() 190 | 191 | def terp_debug(): 192 | terp.pm() 193 | 194 | def quit(): 195 | logging.info("Shutting down...") 196 | if "terp" in globals(): 197 | try: 198 | terp.save() 199 | except: 200 | logging.error("Error saving.") 201 | uidict["root"].quit() 202 | 203 | class EditorDocument(Document): 204 | def __init__(self, set_compgeo_surface=True): 205 | root = Node("group", id="root") 206 | if BACKEND == "xcb": 207 | Document.__init__(self, Surface(800, 600), root) 208 | elif BACKEND == "tkinter": 209 | Document.__init__(self, TkCanvas(800, 600, Toplevel()), root) 210 | elif BACKEND == "opengl": 211 | Document.__init__(self, OGLCanvas(800, 600), root) 212 | elif BACKEND == "sdl": 213 | Document.__init__(self, SDLCanvas(800, 600), root) 214 | pdocument.default_doc = self 215 | self.set_root(empty_doc()) 216 | self.tree_ui = None 217 | self.surface.addlayer(self, "drawing") 218 | self.surface.addlayer(self, "ui") 219 | #self['grid'].parent.remove(self['grid']) 220 | # Hack! 221 | if set_compgeo_surface: 222 | compgeo.surface = self.surface 223 | self.surface.show() 224 | self.STOP_POLLING = False 225 | self.event_log = [] 226 | self.events = [] 227 | 228 | def reload(self, grammar): 229 | if not grammar: 230 | self.interp = None 231 | return 232 | self.interp = gui_lang.interpreter(grammar) 233 | self.new = True 234 | self.interp.match(self.interp.rules['grammar'][-1], self.events, 235 | pos=len(self.events), scope=globals()) 236 | 237 | def poll(self): 238 | starttime = time.time() 239 | last_mouse = None 240 | last_event = None 241 | num_events = 0 242 | while True: 243 | if last_event: 244 | event, last_event = last_event, None 245 | else: 246 | event = backend_poll(self) 247 | # Combine contiguous region of mouse move events. 248 | if event and event.type == Event.motion: 249 | last_mouse = event 250 | continue 251 | elif last_mouse: 252 | last_event, event, last_mouse = event, last_mouse, None 253 | elif event is None: 254 | break 255 | if LOG_EVENTS: 256 | self.events.append(event) 257 | self.event_log.append((event, time.time())) 258 | out = None 259 | if event.type == Event.key_press: 260 | self["editor.key_name"] = event.key_name 261 | self["editor.key_char"] = event.char 262 | self["editor.key_mods"] = event.mods 263 | elif event.type == Event.motion: 264 | recalc_logger.debug("New mouse position") 265 | self["editor.mouse_xy"] = event.xy 266 | self["editor.mouse_txy"] = event.txy 267 | # Callbacks 268 | #if event.type == Event.key_press: 269 | # self.add_letter("echo") 270 | if event.type == Event.key_press: 271 | if "control" in event.mods and event.key_name.lower() == "r": 272 | if "save_undo" in globals(): 273 | save_undo() 274 | print "Reloading" 275 | try: 276 | execfile(script_name, globals()) 277 | self.reload(input_callbacks) 278 | doc.script_versions.log("load", open(script_name).read()) 279 | except: 280 | self.last_tb = sys.exc_traceback 281 | traceback.print_exc() 282 | elif "control" in event.mods and event.key_name.lower() == "z": 283 | self.undo_reload() 284 | #print event.mods, event.key_name 285 | if self.interp: 286 | try: 287 | inp = self.interp.input[:] 288 | finished, self.new = self.interp.match_loop(self.new) 289 | assert(not finished) 290 | except: 291 | traceback.print_exc() 292 | self.interp.input = inp[:] 293 | self.last_tb = sys.exc_traceback 294 | # Not sure about this one. Might just want to reset the 295 | # state to the root grammar rule? 296 | self.reload(input_callbacks) 297 | if time.time() - starttime > 0.2: 298 | print "Over time!", time.time() - starttime 299 | if event.type != "no_exposure": 300 | num_events += 1 301 | if num_events: 302 | self.sync() 303 | event_logger.debug("Eventtime %s %.5f" % (num_events, time.time() - starttime)) 304 | self.update_text() 305 | if DRAW_FREQUENCY is None: 306 | self.draw_loop(uidict["root"], None) 307 | parents_assert = True 308 | if parents_assert: 309 | for node, _ in self.tree_root.L.dfs(): 310 | if node["id"] != "root" and not node._parent: 311 | bp() 312 | if not self.STOP_POLLING: 313 | uidict["root"].after(POLL_FREQUENCY, self.poll) 314 | 315 | def undo_reload(self): 316 | self.script_versions.undo() 317 | try: 318 | exec self.script_versions.current() in globals() 319 | self.reload(input_callbacks) 320 | # Should really be tracked with script_versions 321 | # but indices are off by one. 322 | doc_undo() 323 | except: 324 | self.last_tb = sys.exc_traceback 325 | traceback.print_exc() 326 | 327 | def pm(self): 328 | pdb.post_mortem(self.last_tb) 329 | 330 | def add_letter(self, node_id): 331 | if self["editor.key_name"] == "BackSpace": 332 | self[node_id]["value"] = self[node_id]["value"][:-1] 333 | else: 334 | self[node_id]["value"] += self["editor.key_char"] 335 | 336 | uiroot = UI(Toplevel, packanchor = 'n', title = 'XCB Cairo', name = 'root', children = [ 337 | UI(Frame, packside = 'top', children = [ 338 | UI(ScrolledText, name = 'tree', width=50, height=30, font=('Arial',12)), 339 | UI(Frame, packside = 'left', children = [ 340 | UI(Button, text = 'Debug', command=debug), 341 | UI(Button, text = 'DocDebug', command=doc_debug), 342 | UI(Button, text = 'TerpDebug', command=terp_debug)]), 343 | UI(Frame, packside = 'left', children = [ 344 | UI(Label, text = 'Text: '), 345 | UI(Entry, defaulttext = 'test', name = 'text')]), 346 | UI(Frame, packside = 'left', children = [ 347 | UI(Label, text = 'Id: '), 348 | UI(Entry, defaulttext = '', name = 'id')]), 349 | UI(Frame, packside = 'left', children = [ 350 | UI(Label, text = 'Exec: '), 351 | UI(Entry, defaulttext = '', name = 'exec')]), 352 | UI(ScrolledText, name = 'node edit', 353 | width=50, height=3, font=('Arial',12)), 354 | ])]) 355 | 356 | def demo_empty_doc(): 357 | inp = """ 358 | group: id="root" 359 | group: id="references" 360 | arc: id="point_icon" radius=5 fill_color=(0, 0, 0.5) p_center=P(0, 0) 361 | group: id="drawing" 362 | transforms=dict(zoom=("scale", P(1.0, 1.0)), 363 | scroll_xy=("translate", P(0, 0))) 364 | text: value='Press control-r to reload' p_botleft=P(0, 30) 365 | group: id="ui" 366 | group: id="editor" stroke_color=(0, 0.5, 0) 367 | mouse_xy=P(0, 0) key_name=None key_char=None key_mods=None 368 | mode="edit" selected=pdocument.pmap() 369 | callbacks=pdocument.pmap() gui_selected=None p_lastxy=P(0, 0) 370 | text_root="drawing" 371 | .path=path: stroke_color=(0, 0.5, 0) 372 | .text=text: value="" botleft=Ex("`self.parent.lastxy", calc="reeval") 373 | group: id="overlay" 374 | group: id="selection" root="drawing" 375 | group: id="selection_bbox" stroke_color=(0.5, 0, 0) 376 | dash=([5, 5], 0) skip_points=True 377 | children=[rectangle4(corners=exc("(`selection).bbox()"), 378 | visible=exc("len(`selection) > 1"))] 379 | """ 380 | return tree_lang.parse(inp, locals=globals()) 381 | 382 | def rectangle4(**params): 383 | """ Expects to receive corners as params """ 384 | # Parsing is very slow for now for some reason. 385 | params_str = " ".join("%s=%s" % (key, _repr(value)) 386 | for key, value in params.items()) 387 | inp = """ 388 | path: 389 | %s 390 | topright=exr('topright(`self.corners)') 391 | botleft=exr('botleft(`self.corners)') 392 | p_botright=exr('`self.parent.corners[1]') 393 | p_topleft=exr('`self.parent.corners[0]') 394 | line: start=exr('`self.parent.topleft') end=exr('`self.parent.topright') 395 | line: start=exr('`self.parent.topright') end=exr('`self.parent.botright') 396 | line: start=exr('`self.parent.botright') end=exr('`self.parent.botleft') 397 | line: start=exr('`self.parent.botleft') end=exr('`self.parent.topleft') 398 | """ % params_str 399 | out = tree_lang.parse(inp, locals=globals()) 400 | return out 401 | 402 | def full_empty_doc(): 403 | inp = """ 404 | group: id="root" 405 | group: id="references" children=icons() 406 | group: id="drawing" 407 | transforms=dict(zoom=("scale", P(1.0, 1.0)), 408 | scroll_xy=("translate", P(0, 0))) 409 | group: id="ui" 410 | group: id="editor" stroke_color=(0, 0.5, 0) 411 | mouse_xy=P(0, 0) key_name=None key_char=None key_mods=None 412 | mode="edit" selected=pdocument.pmap() 413 | callbacks=pdocument.pmap() gui_selected=None p_lastxy=P(0, 0) 414 | text_root="drawing" 415 | .path=path: stroke_color=(0, 0.5, 0) 416 | .text=text: value="" botleft=Ex("`self.parent.lastxy", calc="reeval") 417 | group: id="overlay" 418 | group: id="selection" root="drawing" 419 | group: id="selection_bbox" stroke_color=(0.5, 0, 0) 420 | dash=([5, 5], 0) skip_points=True 421 | children=[rectangle4(corners=exc("(`selection).bbox()"), 422 | visible=exc("len(`selection) > 1"))] 423 | group: id="clipboard" visible=False 424 | group: id="mouseover" root="drawing" 425 | group: id="status" root="drawing" 426 | text: id="foo" value="testing" p_botleft=P(0, 600) 427 | text: id="echo" value="" p_botleft=P(0, 400) 428 | text: ref_id="editor" ref_param="mode" 429 | p_botleft=P(100, 600) 430 | group: id="grid" line_width=1 stroke_color=(0, 0, 1) 431 | skip_points=True 432 | """ 433 | return tree_lang.parse(inp, locals=globals()) 434 | 435 | def tkui_sendexec(event): 436 | if not hasattr(doc, "saved"): 437 | terp.sendexec(event) 438 | return 439 | save_undo() 440 | is_error = terp.sendexec(event) 441 | if is_error: 442 | doc_undo() 443 | 444 | if __name__ == "__main__": 445 | #empty_doc = full_empty_doc 446 | empty_doc = demo_empty_doc 447 | tkroot = Tk() 448 | tkroot.withdraw() 449 | doc = EditorDocument() 450 | from wrap_event import Event, backend_poll 451 | #doc.tree_doc = EditorDocument() 452 | uiroot.makeelem() 453 | doc.tree_ui = uidict["tree"] 454 | doc.update_text() 455 | tkui.setfonts() 456 | 457 | histfile = "flow_editor_history" 458 | terp = TkTerp(histfile, globals()) 459 | uidict['exec'].bind('', tkui_sendexec) #terp.sendexec 460 | uidict['exec'].bind('', tkui_sendexec) 461 | uidict['exec'].bind('', terp.hist) 462 | uidict['exec'].bind('', terp.hist) 463 | uidict["root"].protocol("WM_DELETE_WINDOW", quit) 464 | 465 | script_name = "final-demo.py" 466 | script_name = "full-demo.py" 467 | script_name = os.path.join("examples", "functions.py") 468 | if len(sys.argv) > 1: 469 | script_name = sys.argv[1] 470 | doc.script_versions = pdocument.UndoLog() 471 | doc.script_versions.log("load", open(script_name).read()) 472 | __init__ = True 473 | execfile(script_name, globals()) 474 | doc.reload(input_callbacks) 475 | __init__ = False 476 | 477 | import wrap_event 478 | if MAINLOOP == "tkinter": 479 | uidict["root"].after(10, doc.draw_loop, uidict["root"], DRAW_FREQUENCY) 480 | #uidict["root"].after(10, doc.tree_doc.draw_loop, uidict["root"], DRAW_FREQUENCY) 481 | uidict["root"].after(10, doc.poll) 482 | #uidict["root"].after(10, doc.tree_doc.poll) 483 | if BACKEND == "tkinter": 484 | tk_bind(doc.surface.canvas) 485 | elif BACKEND == "opengl": 486 | wrap_event.set_callbacks(doc.surface.win_id) 487 | #wrap_event.set_callbacks(doc.tree_doc.surface.win_id) 488 | logging.debug("Testing") 489 | 490 | uidict["root"].mainloop() 491 | elif MAINLOOP == "glut": 492 | def draw(self): 493 | self.draw_loop(uidict["root"], None) 494 | Document.ogl_draw = draw 495 | from OpenGL.GLUT import (glutIdleFunc, glutMainLoop, glutDisplayFunc, 496 | glutPostRedisplay, glutSetWindow) 497 | wrap_event.event_doc = {doc.surface.win_id: doc,} 498 | #doc.tree_doc.surface.win_id: doc.tree_doc} 499 | wrap_event.event_poll = doc.poll 500 | wrap_event.event_draw = draw 501 | wrap_event.set_callbacks(doc.surface.win_id) 502 | #wrap_event.set_callbacks(doc.tree_doc.surface.win_id) 503 | glutMainLoop() 504 | else: 505 | raise Exception("Unknown main loop %s." % MAINLOOP) 506 | --------------------------------------------------------------------------------