#!/usr/bin/python """ TODO: - support pair: (0 . 1) - have and set "modified" flag - comment out: put something into (complicated) hashsemicolon node. - refactoring via drag&drop - colorized IDs when on it, maybe. - debugging mark ? Store of previous values when evaluated? - "immediate" window ? Note: When deleting, it can enter a technically-invalid state. I think verification should only be passively done at first, but it's actively done on insert... bad... """ import pygtk pygtk.require("2.0") import pango import gtk import gtk.keysyms import gobject from StringIO import StringIO in_ = "let" semicolon = ";" colonequal = ":=" begin = "begin" quote = "'" quasiquote = "`" unquote = "," unquote_splicing = ",@" hashsemicolon = "#;" # not really a name, but whatever. prog = [in_, [colonequal, "x", "2"], "x"] def list_P(obj): return isinstance(obj, list) C_NAME = 0 C_DISPLAYSTRING = 1 C_TYPE = 2 C_TOOLTIP = C_TYPE C_EXPANDED = 3 store = gtk.TreeStore(str, str, str, bool) def parse(inputfile, ainputpos = 0, ainputline = 1): """ parse S-expression """ class L: input = None inputpos = ainputpos inputline = ainputline quasiquotinglevel = 0 comments = StringIO() def raise_error(expectation, reality): raise Exception("parse error on line %d: expected %r, got %r" % (L.inputline, expectation, reality)) # FIXME nicer exception def consume(): oldinput = L.input #print oldinput, L.input = inputfile.read(1) or None L.inputpos += 1 if L.input == "\n": L.inputline += 1 return oldinput def collect_comment(): assert(L.input == ";") consume() while L.input and L.input != "\n": comments.write(consume()) assert(L.input == '\n') #assert(L.input) comments.write(consume()) def collect_multiline_comment(): # these aren't really supported assert(False) # FIXME support nesting # FIXME what if a '|' without '#' is valid? while L.input and L.input != '|': comments.write(consume()) assert(L.input == '|') consume() assert(L.input == '#') consume() def skip_whitespace(): while L.input and (L.input in " ;" or L.input < ' '): # FIXME too wide. Narrow down. if L.input == ";": collect_comment() else: consume() def parse_string_literal(): result = StringIO() assert(L.input == '"') consume() escaped = True while L.input and (L.input != "\"" or escaped): # FIXME escaping escaped = not escaped and L.input == "\\" result.write(consume()) assert(not escaped) assert(L.input == "\"") consume() return "\"%s\"" % (result.getvalue(), ) # FIXME escaping def parse_name_with_prefix(result): while L.input and L.input not in ["(", ")"] and ord(L.input) > 32: result.write(consume()) return result.getvalue() # FIXME escaping def parse_name(): result = StringIO() return parse_name_with_prefix(result) def parse_form(): assert(L.input == "(") consume() items = [] skip_whitespace() while L.input and L.input != ")": t = parse_token() items.append(t) skip_whitespace() assert(L.input == ")") consume() return items def parse_quote_form(): assert(L.input == "'") consume() skip_whitespace() return [quote, parse_token()] def parse_quasiquote_form(): # FIXME make this recursive in the sense of "how many unquoted do we need" assert(L.input == "`") consume() skip_whitespace() L.quasiquotinglevel += 1 try: content = parse_token() return [quasiquote, content] finally: L.quasiquotinglevel -= 1 def parse_unquote_form(): skip_whitespace() return [unquote, parse_token()] def parse_unquote_splicing_form(): return [unquote_splicing, parse_token()] def parse_numeral(base = 10): digits = "0123456789ABCDEF" result = StringIO() while L.input and (L.input.upper() in digits[:base] or L.input == '.'): result.write(consume().upper()) return result.getvalue() def parse_special_coding(): result = StringIO() assert(L.input == '#') result.write(consume()) if L.input == ";": consume() return [hashsemicolon, parse_token()] elif L.input == "|": consume() return collect_multiline_comment() else: # FIXME #;..., #x, #o, ... return parse_name_with_prefix(result) def parse_token(): """ Note: it's not good to call parse_token() without skip_whitespace() first """ assert(L.input) commentvalue = comments.getvalue().rstrip("\n") if len(commentvalue) > 0: # prefix the comment comments.truncate(0) return [semicolon, commentvalue, parse_token()] # FIXME commentvalue as "string" if L.input == "\"": return parse_string_literal() elif L.input in "0123456789": return parse_numeral(10) elif L.input == "(": return parse_form() elif L.input == "'": return parse_quote_form() elif L.input == "`": return parse_quasiquote_form() elif L.input == ",": if L.quasiquotinglevel == 0: return raise_error("", L.input) else: consume() if L.input == "@": consume() return parse_unquote_splicing_form() else: return parse_unquote_form() elif L.input == '#': return parse_special_coding() elif L.input >= '*' or L.input == '%' or L.input == '&' or L.input == '$' or L.input == '!': # input in ascii_letters + "*?": # FIXME much more, use this as fallback! # FIXME "-9" is technically a numeral. Doesn't matter much. return parse_name() else: return raise_error("", L.input) consume() items = [begin] while L.input: skip_whitespace() if not L.input: break t = parse_token() items.append(t) # FIXME check for trailing comments and put them somewhere... return items[1] if len(items) == 2 else items # if just one item, return it. def newrow(name, repr): return (name, repr, None, False) def for_tree(model, iter, callback): if not iter: return callback(iter) iter = model.iter_children(iter) while iter: for_tree(model, iter, callback) iter = model.iter_next(iter) def summarize_treeview(treeview, iter, path): dest = StringIO() model = treeview.props.model # store expanded state of children in model for_tree(model, iter, lambda iter: model.set_value(iter, C_EXPANDED, treeview.row_expanded(model.get_path(iter)))) summarize(model, iter, dest) rep = dest.getvalue().replace("\n", "") model.set_value(iter, C_DISPLAYSTRING, rep) model.set_value(iter, C_EXPANDED, False) def unsummarize_treeview(treeview, iter, path): model = treeview.props.model name = model.get_value(iter, C_NAME) model.set_value(iter, C_DISPLAYSTRING, name) model.set_value(iter, C_EXPANDED, True) # restore expanded state of children from model for_tree(model, iter, lambda iter: treeview.expand_row(model.get_path(iter), False) if model.get_value(iter, C_EXPANDED) else None) # TODO technically the latter is collapse def load_sentinel(parent): model = store return model.append(parent, newrow("", "")) def load_part(parent, obj): if list_P(obj): if len(obj) == 0: return load_sentinel(parent) p = obj[0] if list_P(p): iter = load_sentinel(parent) load_part(iter, p) else: iter = store.append(parent, newrow(p, p)) for item in obj[1:]: load_part(iter, item) if len(obj) == 1: # missing args is OK in Scheme. FIXME otherwise don't do that load_sentinel(iter) dest = StringIO() #summarize(model, iter, dest) ##summarize_treeview(treeview, iter, None) # FIXME split that from test-collapse-row else: store.append(parent, newrow(str(obj), str(obj))) # FIXME clean up def load_prog(prog): load_part(None, prog) def summarize(model, parent, dest): """ returns (pretty-printed?) S-expression """ name = model.get_value(parent, C_NAME) iter = model.iter_children(parent) aquote = name in [quote, quasiquote, unquote, unquote_splicing, semicolon] p = iter and not aquote # FIXME make sure that for quotes, there is only 1 argument (!!!) if p: dest.write("(") dest.write(name) first = True while iter: if not first or not aquote: dest.write(" ") if first and name == semicolon: # this is just so that after a comment is exactly one \n (especially more than 0) temp = StringIO() summarize(model, iter, temp) v = temp.getvalue() dest.write(v) if len(v) == 0 or v[-1] != "\n": dest.write(v) else: summarize(model, iter, dest) iter = model.iter_next(iter) first = False if p: dest.write(")") contextmenu = gtk.Menu() def show_popup_menu(widget, event = None): # Shift-F10 # event is optional contextmenu.show_all() contextmenu.popup(None, None, None, event.button if event else 0, event.time if event else 0) def handle_tree_button_press(treeview, event): if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3: selection = treeview.get_selection() res = treeview.get_path_at_pos(int(event.x), int(event.y)) if res: path, column, x, y = res treeview.set_cursor(path, column) # select node. if selection.count_selected_rows() <= 1: treeview.get_selection().select_path(path) show_popup_menu(treeview, event) return True # handled return False # unhandled class TreeView(gtk.TreeView): __gsignals__ = { "move-cursor-to-next-sibling": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), "move-cursor-to-prev-sibling": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), "move-cursor-to-parent": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), "move-cursor-to-child": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), } def __init__(self, *args, **kwargs): gtk.TreeView.__init__(self, *args, **kwargs) # TODO make nicer. Is extremely HIG hostile. Better add signals and bind these and your own class... gtk.binding_entry_remove(TreeView, gtk.keysyms.Left, 0) gtk.binding_entry_remove(TreeView, gtk.keysyms.Right, 0) gtk.binding_entry_remove(TreeView, gtk.keysyms.Up, 0) gtk.binding_entry_remove(TreeView, gtk.keysyms.Down, 0) gtk.binding_entry_add_signal(TreeView, gtk.keysyms.Left, 0, "move-cursor-to-parent") gtk.binding_entry_add_signal(TreeView, gtk.keysyms.Right, 0, "move-cursor-to-child") gtk.binding_entry_add_signal(TreeView, gtk.keysyms.Up, 0, "move-cursor-to-prev-sibling") gtk.binding_entry_add_signal(TreeView, gtk.keysyms.Down, 0, "move-cursor-to-next-sibling") def do_move_cursor_to_next_sibling(self): print("NEXT") def do_move_cursor_to_prev_sibling(self): print("PREV") def do_move_cursor_to_parent(self): print("PARENT") def do_move_cursor_to_first_child(self): print("CHILD") gobject.type_register(TreeView) treeview = TreeView() treeview.connect("button-press-event", handle_tree_button_press) treeview.connect("popup-menu", show_popup_menu) treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE) treeview.connect("test-collapse-row", summarize_treeview) treeview.connect("row-expanded", unsummarize_treeview) cell0 = gtk.CellRendererText() cell0.props.ellipsize = pango.ELLIPSIZE_MIDDLE cell0.props.editable = True def update_cells_from_text(cell, pathstr, newtext): model = treeview.props.model #path = tuple(map(int, pathstr.split(":"))) # FIXME gtk.TreePath(pathstr) # model.get_path_from_string(pathstr) iter = model.get_iter_from_string(pathstr) path = model.get_path(iter) # FIXME check & update expanded = treeview.row_expanded(path) if expanded or not model.iter_children(iter): model.set_value(iter, C_NAME, newtext) else: oldtext = model.get_value(iter, C_DISPLAYSTRING) if oldtext == newtext: # unchanged return model.set_value(iter, C_DISPLAYSTRING, newtext) def check_row_path(model, iter_to_copy, target_iter): path_of_iter_to_copy = model.get_path(iter_to_copy) path_of_target_iter = model.get_path(target_iter) return path_of_target_iter[0:len(path_of_iter_to_copy)] != path_of_iter_to_copy # no loop... DND_TYPE_TEXT = 1 def dnd_copy(model, iter_to_copy, target_iter, pos): if pos in [gtk.TREE_VIEW_DROP_INTO_OR_BEFORE, gtk.TREE_VIEW_DROP_INTO_OR_AFTER]: newiter = model.prepend(target_iter, None) elif pos == gtk.TREE_VIEW_DROP_BEFORE: newiter = model.insert_before(None, target_iter) elif pos == gtk.TREE_VIEW_DROP_AFTER: newiter = model.insert_after(None, target_iter) # FIXME copy values over for n in range(model.iter_n_children(iter_to_copy)): next_iter_to_copy = model.iter_nth_child(iter_to_copy, n) dnd_copy(model, next_iter_to_copy, new_iter, gtk.TREE_VIEW_DROP_INTO_OR_BEFORE) # FIXME remove old row # Note: we can pretty much use the same stuff for both the clipboard and Drag&Drop def handle_received_dnd_data(treeview, dragcontext, x, y, selection, dnd_type, eventtime): if dnd_type == DND_TYPE_TEXT: delete_data = False text = selection.data suggested_action = dragcontext.props.suggested_action if suggested_action == gtk.gdk.ACTION_ASK: # FIXME ask pass elif suggested_action == gtk.gdk.ACTION_MOVE: delete_data = True # FIXME move pass print("OK", text) dragcontext.finish(True, delete_data, eventtime) else: print("ignored unknown dnd target type", dnd_type) dragcontext.finish(False, False, eventtime) def handle_dnd_data_preparation(): pass # FIXME # see def handle_dnd_drop(widget, context, x, y, eventtime): valid_drop_site = True # FIXME check whether valid drop site targets = context.list_targets() if not targets: valid_drop_site = False else: print("TARGETS", targets) target_type = targets # FIXME isn't that the wrong widget? Try context.source_window widget.drag_get_data(context, target_type, eventtime) return valid_drop_site cell0.connect("edited", update_cells_from_text) col0 = gtk.TreeViewColumn("Expression") col0.pack_start(cell0, True) col0.add_attribute(cell0, "text", C_DISPLAYSTRING) treeview.append_column(col0) treeview.props.model = store #treeview.props.rules_hint = True treeview.props.headers_visible = False #treeview.props.level_indentation = 30 treeview.props.rubber_banding = True #treeview.props.hover_expand = True treeview.props.enable_tree_lines = True treeview.props.enable_search = True # TODO style properties: row-ending-details, tree-line-pattern, tree-line-width, vertical-separator, indent-expanders # treeview.set_reorderable(True) # incompatible with drag&drop targets = [ ("STRING", 0, DND_TYPE_TEXT), ("text/plain", 0, DND_TYPE_TEXT), # mime type, flags, user-internal id ] dactions = gtk.gdk.ACTION_COPY | gtk.gdk.ACTION_MOVE | gtk.gdk.ACTION_LINK | gtk.gdk.ACTION_ASK treeview.enable_model_drag_source(gtk.gdk.BUTTON1_MASK | gtk.gdk.BUTTON2_MASK | gtk.gdk.BUTTON3_MASK | gtk.gdk.MOD1_MASK | gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK, targets, dactions) treeview.enable_model_drag_dest(targets, dactions) # gtk.drag_source_set(widget, gtk.BUTTON1_MASK, targets, gtk.gdk.ACTION_COPY) # gtk.drag_dest_set(widget, gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_HIGHLIGHT, targets, gtk.ACTION_COPY) # TODO drag_leave, drag_motion, drag_data_delete, drag_begin, drag_end #treeview.connect("drag_data_get", handle_dnd_data_preparation) #treeview.connect("drag_data_received", handle_received_dnd_data) #treeview.connect("drag_drop", handle_dnd_drop) # column.pack_start, pack_end scroller = gtk.ScrolledWindow() scroller.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) scroller.add(treeview) textbuffer = gtk.TextBuffer() textview = gtk.TextView(textbuffer) textscroller = gtk.ScrolledWindow() textscroller.add(textview) def edit_node(action): path, col = treeview.get_cursor() cell = col.get_cell_renderers()[0] if not cell.props.editing: treeview.set_cursor(path, col, True) def delete_nodes(action): selection = treeview.get_selection() model, paths = selection.get_selected_rows() refs = map(lambda path: gtk.TreeRowReference(model, path), paths) for rref in refs: path = rref.get_path() iter = model.get_iter(path) model.remove(iter) def insertion_permitted_under_node(model, parent): # FIXME check operator kind etc. return True def insertion_permitted_above_node(model, parent): # FIXME check return True def append_child_node(action): path, column = treeview.get_cursor() treeview.expand_to_path(path) model = treeview.props.model iter = model.get_iter(path) if insertion_permitted_under_node(model, iter): iter = model.append(iter, newrow("", "")) # TODO nicer initializer? path = model.get_path(iter) treeview.expand_to_path(path) treeview.set_cursor(path, column) # TODO hard-code column edit_node(action) def insert_parent_node(action): path, column = treeview.get_cursor() treeview.expand_to_path(path) model = treeview.props.model iter = model.get_iter(path) if insertion_permitted_above_node(model, iter): # FIXME move iter node away so it's a child of the new node. iter = model.append(iter, newrow("", "")) # FIXME nicer initializer? path = model.get_path(iter) treeview.expand_to_path(path) treeview.set_cursor(path, column) # TODO hard-code column edit_node(action) def insert_sibling_before_this_node(action): path, column = treeview.get_cursor() treeview.expand_to_path(path) model = treeview.props.model iter = model.get_iter(path) parent = model.iter_parent(iter) if insertion_permitted_under_node(model, parent): iter = model.insert_before(parent, iter, newrow("", "")) # FIXME nicer initializer? path = model.get_path(iter) treeview.expand_to_path(path) treeview.set_cursor(path, column) # TODO hard-code column edit_node(action) def create_Action(name, label, tooltiptext, stockid, cb, key = None, keymods = 0): action = gtk.Action(name, label, tooltiptext, stockid) action.connect("activate", cb) menuitem = action.create_menu_item() path = "/Contextmenu/%s" % (name, ) menuitem.set_accel_path(path) if key and not gtk.accel_map_lookup_entry(path): gtk.accel_map_add_entry(path, key, keymods) contextmenu.append(menuitem) return action def cut_nodes(action): copy_nodes(action) delete_nodes(action) def copy_nodes(action): selection = treeview.get_selection() model, paths = selection.get_selected_rows() #refs = map(lambda path: gtk.TreeRowReference(model, path), paths) dest = StringIO() if len(paths) > 1: dest.write("(") for path in paths: iter = model.get_iter(path) summarize(model, iter, dest) if len(paths) > 1: dest.write(")") data = dest.getvalue() # FIXME put data into clipboard. def paste_nodes(action): # FIXME decide HOW to paste it in: move everthing down or what? pass # FIXME def go_to_prev_sibling_node(action): path, col = treeview.get_cursor() path = list(path) if path[-1] > 0: path[-1] -= 1 path = tuple(path) treeview.set_cursor(path, col) def go_to_next_sibling_node(action): path, col = treeview.get_cursor() apath = list(path) apath[-1] += 1 # can be out of bounds npath = tuple(apath) treeview.set_cursor(npath, col) if not treeview.get_cursor()[0]: # didn't work... treeview.set_cursor(path, col) def go_to_first_child_node(action): path, col = treeview.get_cursor() treeview.expand_to_path(path) model = treeview.props.model iter = model.get_iter(path) iter = model.iter_children(iter) if iter: path = model.get_path(iter) treeview.set_cursor(path, col) # TODO beep def go_to_parent_node(action): path, col = treeview.get_cursor() apath = list(path) if len(apath) > 0: npath = tuple(apath[:-1]) treeview.set_cursor(npath, col) if not treeview.get_cursor()[0]: # didn't work... treeview.set_cursor(path, col) else: treeview.collapse_row(npath) # FIXME add "FindText" action (Ctrl-F), maybe by widget.bindings_activate(keyval, modifiers) actions = { "RenameNode": create_Action("RenameNode", "Rename...", "Edit Node", gtk.STOCK_EDIT, edit_node, gtk.keysyms.F2, 0), "AppendChild": create_Action("AppendChild", "Append Child", "Insert Child Under This Node", gtk.STOCK_ADD, append_child_node, gtk.keysyms.Insert, 0), "InsertParent": create_Action("InsertParent", "Insert As Parent", "Insert As Parent of This Node", gtk.STOCK_ADD, insert_parent_node, gtk.keysyms.p, gtk.gdk.CONTROL_MASK|gtk.gdk.MOD1_MASK), # FIXME resolve conflict with "Print" "InsertSibling": create_Action("InsertSibling", "Insert Sibling Before", "Insert Sibling Before This Node", 0, insert_sibling_before_this_node, gtk.keysyms.Insert, gtk.gdk.SHIFT_MASK), "DeleteTree": create_Action("DeleteTree", "Delete Tree", "Delete Tree under this Node", gtk.STOCK_DELETE, delete_nodes, gtk.keysyms.Delete), "CommentOut": gtk.Action("CommentOut", "Comment Out", "Comment Tree Out", 0), # FIXME "#;" "DeleteComment": gtk.Action("DeleteComment", "Delete Comment", "Delete Comment", 0), # FIXME "CutNode": create_Action("CutNode", "Cut", "Cut into Clipboard", gtk.STOCK_CUT, cut_nodes, gtk.keysyms.x, gtk.gdk.CONTROL_MASK), "CopyNode": create_Action("CopyNode", "Copy", "Copy into Clipboard", gtk.STOCK_COPY, copy_nodes, gtk.keysyms.c, gtk.gdk.CONTROL_MASK), "PasteNode": create_Action("PasteNode", "Paste", "Paste from Clipboard", gtk.STOCK_PASTE, paste_nodes, gtk.keysyms.p, gtk.gdk.CONTROL_MASK), "GoToPrevSibling": create_Action("GoToPrevSibling", "Go To Prev Sibling", "Go To Prev Sibling", 0, go_to_prev_sibling_node, gtk.keysyms.Up, 0), "GoToNextSibling": create_Action("GoToNextSibling", "Go To Next Sibling", "Go To Next Sibling", 0, go_to_next_sibling_node, gtk.keysyms.Down, 0), "GoToFirstChild": create_Action("GoToFirstChild", "Go To First Child", "Go To First Child", 0, go_to_first_child_node, gtk.keysyms.Right, 0), "GoToParent": create_Action("GoToParent", "Go To Parent", "Go To Parent", 0, go_to_parent_node, gtk.keysyms.Left, 0), } #for name in ["EditNode", "AppendChild", "InsertSibling", "DeleteTree", "CommentOut", "DeleteComment", "CutNode", "CopyNode", "PasteNode"]: # contextmenu.append(actions[name].create_menu_item()) def update_textview(selection): model, paths = selection.get_selected_rows() if len(paths) == 1: path = paths[0] dest = StringIO() iter = store.get_iter(path) summarize(store, iter, dest) text = dest.getvalue() textbuffer.set_text(text) else: textbuffer.set_text("No selection") treeview.get_selection().connect("changed", update_textview) treeview.set_tooltip_column(C_TYPE) textscroller.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) box = gtk.HBox() box.pack_start(scroller, True, True) # unused for now box.pack_start(textscroller, True, True) window = gtk.Window() accelgroup = gtk.AccelGroup() contextmenu.set_accel_group(accelgroup) contextmenu.set_accel_path("/Contextmenu") window.add_accel_group(accelgroup) window.connect("delete-event", gtk.main_quit) # TODO ask whether to save. window.add(box) window.show_all() import sys if len(sys.argv) > 1: name = sys.argv[1] with open(name) as f: prog = parse(f) load_prog(prog) treeview.expand_all() def p(x): print(x) # doesn't work as expected: treeview.collapse_all(), so the workaround is: def collapse1(iter): path = store.get_path(iter) treeview.emit("test-collapse-row", iter, path) treeview.collapse_row(path) for_tree(store, store.get_iter_root(), collapse1) gtk.main()