#!/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 ? - test that '() writes out correctly again. 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... """ from StringIO import StringIO from sexps import parse, summarize, C_NAME import gtk32 as gtk from gtk32 import gobject, SelectionMode, pango, DragAction, ModifierType, PolicyType, keysyms, Gdk, window_set_geometry_hints, EventType #prog = [in_, [colonequal, "x", "2"], "x"] def list_P(obj): return isinstance(obj, list) assert(C_NAME == 0) C_DISPLAYSTRING = 1 C_TYPE = 2 C_TOOLTIP = C_TYPE C_EXPANDED = 3 store = gtk.TreeStore(str, str, str, bool) 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() # bad for line comments: .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 insert_node(store, position, parent, data): if position is not None: return store.insert_before(parent, position, data) else: return store.append(parent, data) def load_sentinel(parent, position = None): model = store return insert_node(model, position, parent, newrow("", "")) def load_part(parent, obj, position = None): if list_P(obj): if len(obj) == 0: return load_sentinel(parent, position) p = obj[0] if list_P(p): iter = load_sentinel(parent, position) load_part(iter, p) else: iter = insert_node(store, position, 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(store, iter, dest) v = dest.getvalue() store.set_value(iter, C_DISPLAYSTRING, v) # assumption: unexpanded by default ##summarize_treeview(treeview, iter, None) # FIXME split that from test-collapse-row else: insert_node(store, position, parent, newrow(str(obj), str(obj))) # FIXME clean up def load_prog(prog): load_part(None, prog) 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 collapse1(model, iter): path = model.get_path(iter) treeview.emit("test-collapse-row", iter, path) treeview.collapse_row(path) def handle_tree_button_press(treeview, event): if event.type == EventType.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.SIGNAL_ACTION, gobject.TYPE_NONE, ()), "move-cursor-to-prev-sibling": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, gobject.TYPE_NONE, ()), "move-cursor-to-parent": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, gobject.TYPE_NONE, ()), "close-and-move-cursor-to-parent": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, gobject.TYPE_NONE, ()), "move-cursor-to-first-child": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, gobject.TYPE_NONE, ()), } def __init__(self, *args, **kwargs): gtk.TreeView.__init__(self, *args, **kwargs) def do_move_cursor_to_next_sibling(self): path, col = treeview.get_cursor() if path: 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) # TODO beep def do_move_cursor_to_prev_sibling(self): path, col = treeview.get_cursor() if path: path = list(path) if path[-1] > 0: path[-1] -= 1 path = tuple(path) treeview.set_cursor(path, col) # TODO beep def do_move_cursor_to_parent(self): path, col = treeview.get_cursor() if path: apath = list(path) if len(apath) > 1: npath = tuple(apath[:-1]) treeview.set_cursor(npath, col) if not treeview.get_cursor()[0]: # didn't work... treeview.set_cursor(path, col) else: return True def do_close_and_move_cursor_to_parent(self): path, col = treeview.get_cursor() if path: collapse1(treeview.props.model, treeview.props.model.get_iter(path)) # treeview.collapse_row(npath) self.do_move_cursor_to_parent() def do_move_cursor_to_first_child(self): path, col = treeview.get_cursor() if path: 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 gobject.type_register(TreeView) # TODO make nicer. Is extremely HIG hostile. gtk.binding_entry_remove(TreeView, keysyms.Left, 0) gtk.binding_entry_remove(TreeView, keysyms.Left, ModifierType.SHIFT_MASK) gtk.binding_entry_remove(TreeView, keysyms.Right, 0) gtk.binding_entry_remove(TreeView, keysyms.Up, 0) gtk.binding_entry_remove(TreeView, keysyms.Down, 0) gtk.binding_entry_remove(TreeView, keysyms.Prior, 0) gtk.binding_entry_remove(TreeView, keysyms.Prior, ModifierType.SHIFT_MASK) # annoying gtk.binding_entry_remove(TreeView, keysyms.Next, 0) gtk.binding_entry_remove(TreeView, keysyms.Next, ModifierType.SHIFT_MASK) # annoying gtk.binding_entry_add_signal(TreeView, keysyms.Left, 0, "move-cursor-to-parent") gtk.binding_entry_add_signal(TreeView, keysyms.Left, ModifierType.SHIFT_MASK, "close-and-move-cursor-to-parent") gtk.binding_entry_add_signal(TreeView, keysyms.Right, 0, "move-cursor-to-first-child") gtk.binding_entry_add_signal(TreeView, keysyms.Up, 0, "move-cursor-to-prev-sibling") gtk.binding_entry_add_signal(TreeView, keysyms.Down, 0, "move-cursor-to-next-sibling") treeview = TreeView() treeview.connect("button-press-event", handle_tree_button_press) treeview.connect("popup-menu", show_popup_menu) treeview.get_selection().set_mode(SelectionMode.MULTIPLE) treeview.connect("test-collapse-row", summarize_treeview) treeview.connect("row-expanded", unsummarize_treeview) class CellRendererText(gtk.CellRendererText): def do_start_editing(self,event,widget,path,background_area,cell_area,flags): #(None, , '0:0', Gdk.Rectangle(0, 32, 640, 32), Gdk.Rectangle(43, 35, 594, 26), ) if event is None: event = Gdk.Event(EventType.NOTHING) editable = gtk.CellRendererText.do_start_editing(self,event,widget,path,background_area,cell_area,flags) def finish_editing(widget, event): editable.emit("editing-done") editable.remove_widget() editable.stop_emission("focus-out-event") return False import ctypes #c = ctypes.cdll.LoadLibrary("libgtk-x11-2.0.so.0") #c.g_object_disconnect() #print(dir(gobject)) #gobject.signal_handler_list_ids(editable) #gobject.signal_handler_disconnect(editable, id) editable.disconnect("focus-out-event::signal_name") editable.connect("focus-out-event", finish_editing) #TODO replace focus-out-event, just call editing-done afterwards, then gtk_cell_editable_remove_widget. return editable #__gproperties__ = { # 'editing-cancelled': (gobject.TYPE_BOOLEAN, 'Editing Cancelled?', 'Whether Editing Was Cancelled', False, gobject.PARAM_READWRITE) #} #def do_set_property(self, property, value): # print("SET") #pass gobject.type_register(CellRendererText) cell0 = CellRendererText() cell0.props.ellipsize = pango.EllipsizeMode.MIDDLE cell0.props.editable = True def maybe_restore_cursor(path): """ restore cursor, if possible. If deleted last sibling, restore to parent. """ npath, ncol = treeview.get_cursor() # if already there, stop fiddling with it while path is not None and len(path) > 0 and npath is None: treeview.set_cursor(path, None, start_editing=False) npath, ncol = treeview.get_cursor() if npath is None: if path[-1] > 0: # if the bottom sibling(s) were deleted but there are other siblings, restore to them. xpath = list(path) xpath[-1] -= 1 path = tuple(xpath) else: path = path[:-1] 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 newtext == "": # delete node. Is that allowed here? model.remove(iter) maybe_restore_cursor(path) return 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 progpart = parse(StringIO(newtext)) load_part(model.iter_parent(iter), progpart, iter) # FIXME doesn't work at top level model.remove(iter) 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 == Gdk.ACTION_ASK: # FIXME ask pass elif suggested_action == 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 = DragAction.COPY | DragAction.MOVE | DragAction.LINK | DragAction.ASK treeview.enable_model_drag_source(ModifierType.BUTTON1_MASK | ModifierType.BUTTON2_MASK | ModifierType.BUTTON3_MASK | ModifierType.MOD1_MASK | ModifierType.CONTROL_MASK | ModifierType.SHIFT_MASK, targets, dactions) treeview.enable_model_drag_dest(targets, dactions) # gtk.drag_source_set(widget, gtk.BUTTON1_MASK, targets, 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(PolicyType.NEVER, PolicyType.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() col = treeview.get_column(0) cell = col.get_cell_renderers()[0] if not cell.props.editing: treeview.set_cursor(path, col, start_editing=True) def get_selected_rows_for_deletion(): 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() yield model, path def delete_tree(action): cpath, ccol = treeview.get_cursor() for model, path in get_selected_rows_for_deletion(): iter = model.get_iter(path) model.remove(iter) maybe_restore_cursor(cpath) def delete_row(action): cpath, ccol = treeview.get_cursor() for model, path in get_selected_rows_for_deletion(): iter = model.get_iter(path) assert model.iter_children(iter) is None # not implemented yet. FIXME implement model.remove(iter) maybe_restore_cursor(cpath) def insertion_permitted_under_node(model, parent): # FIXME check after/before # 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() model = treeview.props.model if path: treeview.expand_to_path(path) iter = model.get_iter(path) else: iter = None 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() if path: 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) #TODO def insert_sibling_common(): def insert_sibling_before_this_node(action): path, column = treeview.get_cursor() if path: 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 insert_sibling_after_this_node(action): path, column = treeview.get_cursor() if path: 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): # FIXME different for "after" iter = model.insert_after(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 gtk.AccelMap_lookup_entry(path)[0]: gtk.AccelMap.add_entry(path, key, keymods) contextmenu.append(menuitem) return action def cut_nodes(action): copy_nodes(action) delete_tree(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): treeview.emit("move-cursor-to-prev-sibling") def go_to_next_sibling_node(action): treeview.emit("move-cursor-to-next-sibling") def go_to_first_child_node(action): treeview.emit("move-cursor-to-first-child") def go_to_parent_node(action): treeview.emit("move-cursor-to-parent") def close_and_go_to_parent_node(action): print("CLOS") treeview.emit("close-and-move-cursor-to-parent") # 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, keysyms.F2, 0), "AppendChild": create_Action("AppendChild", "Append Child", "Insert Child Under This Node", gtk.STOCK_ADD, append_child_node, keysyms.Insert, 0), "InsertParent": create_Action("InsertParent", "Insert As Parent", "Insert As Parent of This Node", gtk.STOCK_ADD, insert_parent_node, keysyms.p, ModifierType.CONTROL_MASK|ModifierType.MOD1_MASK), # FIXME resolve conflict with "Print" "InsertSiblingBefore": create_Action("InsertSiblingBefore", "Insert Before Sibling", "Insert Sibling Before This Node", 0, insert_sibling_before_this_node, keysyms.Prior, 0), "InsertSiblingAfter": create_Action("InsertSiblingAfter", "Insert After Sibling", "Insert Sibling After This Node", 0, insert_sibling_after_this_node, keysyms.Next, 0), "DeleteTree": create_Action("DeleteTree", "Delete Tree", "Delete Tree under this Node", gtk.STOCK_DELETE, delete_tree, keysyms.Delete, ModifierType.SHIFT_MASK), "DeleteRow": create_Action("DeleteRow", "Delete Row", "Delete just this row, nothing under this Node", gtk.STOCK_DELETE, delete_row, keysyms.Delete, 0), "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, keysyms.x, ModifierType.CONTROL_MASK), "CopyNode": create_Action("CopyNode", "Copy", "Copy into Clipboard", gtk.STOCK_COPY, copy_nodes, keysyms.c, ModifierType.CONTROL_MASK), "PasteNode": create_Action("PasteNode", "Paste", "Paste from Clipboard", gtk.STOCK_PASTE, paste_nodes, keysyms.p, ModifierType.CONTROL_MASK), "GoToPrevSibling": create_Action("GoToPrevSibling", "Go To Prev Sibling", "Go To Prev Sibling", 0, go_to_prev_sibling_node, keysyms.Up, 0), "GoToNextSibling": create_Action("GoToNextSibling", "Go To Next Sibling", "Go To Next Sibling", 0, go_to_next_sibling_node, keysyms.Down, 0), "GoToFirstChild": create_Action("GoToFirstChild", "Go To First Child", "Go To First Child", 0, go_to_first_child_node, keysyms.Right, 0), "GoToParent": create_Action("GoToParent", "Go To Parent", "Go To Parent", 0, go_to_parent_node, keysyms.Left, 0), "GoToParentAndClose": create_Action("GoToParentAndClose", "Close Node And Go To Parent", "Close This Node And Go To Parent", 0, close_and_go_to_parent_node, keysyms.Left, ModifierType.SHIFT_MASK), } #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, 0) # 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) def try_closing_window(*args, **kwargs): summarize(store, store.get_iter_first(), sys.stdout) sys.stdout.flush() # return True # nope. return gtk.main_quit() window.connect("delete-event", try_closing_window) g = Gdk.Geometry() g.min_width = 640 g.min_height = 480 hints = Gdk.WindowHints.MIN_SIZE | Gdk.WindowHints.MAX_SIZE window_set_geometry_hints(window, g, hints) window.add(box) window.set_name("GTreeEditor") 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) window.set_title("%s - Tree Editor" % (name, )) else: prog = parse(StringIO("(begin)")) load_prog(prog) treeview.expand_all() def p(x): print(x) # doesn't work as expected: treeview.collapse_all(), so the workaround is: for_tree(store, store.get_iter_first(), lambda iter: collapse1(store, iter)) treeview.set_cursor((0,),None) gtk.main()