diff --git a/.gitignore b/.gitignore index dd51734..09c8183 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .*.sw? *.pyc *.egg-info +.project build dist diff --git a/README.markdown b/README.markdown index 09ffad0..c604f59 100644 --- a/README.markdown +++ b/README.markdown @@ -103,13 +103,24 @@ Command Line Escape halt animation Ctrl-drag zoom in/out Shift-drag zooms an area + F2 prev highlighted item + F3 next highlighted item + , select prev focused node's edge + . select next focused node's edge + p,j prev file + n,k next file + o browse file + Enter follow selected edge + Ctrl-click display shortest path (retargetable) + Ctrl-shift-click display reverse shortest path (retargetable) + right click dot url open ([URL="dots://file1;file2;..."]) (Linux only) If no input file is given then it will read the dot graph from the standard input. Embedding --------- -See included `example.py` script for an example of how to embedded _xdot.py_ into another application. +See included `sample.py` script for an example of how to embedded _xdot.py_ into another application. [![Screenshot](https://raw.github.com/wiki/jrfonseca/xdot.py/xdot-sample_small.png)](https://raw.github.com/wiki/jrfonseca/xdot.py/xdot-sample.png) diff --git a/xdot.py b/xdot.py index 3153d2f..197a385 100755 --- a/xdot.py +++ b/xdot.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python2 # # Copyright 2008 Jose Fonseca # @@ -45,6 +45,9 @@ # - http://mirageiv.berlios.de/ # - http://comix.sourceforge.net/ +HOVER_PEN=1 +SELECTED_PEN=2 +PATH_PEN=3 class Pen: """Store pen attributes.""" @@ -64,12 +67,27 @@ def copy(self): pen.__dict__ = self.__dict__.copy() return pen - def highlighted(self): + # drnol: pen type extends to hover, selected, path + def hover(self): pen = self.copy() + pen.linewidth = 2.0 pen.color = (1, 0, 0, 1) pen.fillcolor = (1, .8, .8, 1) return pen + def selected(self): + pen = self.copy() + pen.linewidth = 2.0 + pen.color = (0, 1, 0, 1) + pen.fillcolor = (1, .8, .8, 1) + return pen + + def path(self): + pen = self.copy() + pen.linewidth = 3.0 + pen.color = (1, 0, 1, 1) + pen.fillcolor = (1, .8, .8, 1) + return pen class Shape: """Abstract base class for all the drawing shapes.""" @@ -77,15 +95,24 @@ class Shape: def __init__(self): pass - def draw(self, cr, highlight=False): + def draw(self, cr, pen_type=None): """Draw this shape with the given cairo context""" raise NotImplementedError - def select_pen(self, highlight): - if highlight: - if not hasattr(self, 'highlight_pen'): - self.highlight_pen = self.pen.highlighted() - return self.highlight_pen + # drnol: pen type extends to hover, selected, path + def select_pen(self, pen_type): + if pen_type == HOVER_PEN: + if not hasattr(self, 'hover_pen'): + self.hover_pen = self.pen.hover() + return self.hover_pen + elif pen_type == SELECTED_PEN: + if not hasattr(self, 'selected_pen'): + self.selected_pen = self.pen.selected() + return self.selected_pen + elif pen_type == PATH_PEN: + if not hasattr(self, 'path_pen'): + self.path_pen = self.pen.path() + return self.path_pen else: return self.pen @@ -106,7 +133,8 @@ def __init__(self, pen, x, y, j, w, t): self.w = w self.t = t - def draw(self, cr, highlight=False): + # drnol: change highlight flag to pen_type + def draw(self, cr, pen_type=None): try: layout = self.layout @@ -172,7 +200,8 @@ def draw(self, cr, highlight=False): cr.save() cr.scale(f, f) - cr.set_source_rgba(*self.select_pen(highlight).color) + # drnol: change highlight flag to pen_type + cr.set_source_rgba(*self.select_pen(pen_type).color) cr.show_layout(layout) cr.restore() @@ -204,7 +233,8 @@ def __init__(self, pen, x0, y0, w, h, path): self.h = h self.path = path - def draw(self, cr, highlight=False): + # drnol: change highlight flag to pen_type + def draw(self, cr, pen_type=None): cr2 = gtk.gdk.CairoContext(cr) pixbuf = gtk.gdk.pixbuf_new_from_file(self.path) sx = float(self.w)/float(pixbuf.get_width()) @@ -228,14 +258,16 @@ def __init__(self, pen, x0, y0, w, h, filled=False): self.h = h self.filled = filled - def draw(self, cr, highlight=False): + # drnol: change highlight flag to pen_type + def draw(self, cr, pen_type=None): cr.save() cr.translate(self.x0, self.y0) cr.scale(self.w, self.h) cr.move_to(1.0, 0.0) cr.arc(0.0, 0.0, 1.0, 0, 2.0*math.pi) cr.restore() - pen = self.select_pen(highlight) + # drnol: change highlight flag to pen_type + pen = self.select_pen(pen_type) if self.filled: cr.set_source_rgba(*pen.fillcolor) cr.fill() @@ -254,13 +286,15 @@ def __init__(self, pen, points, filled=False): self.points = points self.filled = filled - def draw(self, cr, highlight=False): + # drnol: change highlight flag to pen_type + def draw(self, cr, pen_type=None): x0, y0 = self.points[-1] cr.move_to(x0, y0) for x, y in self.points: cr.line_to(x, y) cr.close_path() - pen = self.select_pen(highlight) + # drnol: change highlight flag to pen_type + pen = self.select_pen(pen_type) if self.filled: cr.set_source_rgba(*pen.fillcolor) cr.fill_preserve() @@ -279,12 +313,14 @@ def __init__(self, pen, points): self.pen = pen.copy() self.points = points - def draw(self, cr, highlight=False): + # drnol: change highlight flag to pen_type + def draw(self, cr, pen_type=None): x0, y0 = self.points[0] cr.move_to(x0, y0) for x1, y1 in self.points[1:]: cr.line_to(x1, y1) - pen = self.select_pen(highlight) + # drnol: change highlight flag to pen_type + pen = self.select_pen(pen_type) cr.set_dash(pen.dash) cr.set_line_width(pen.linewidth) cr.set_source_rgba(*pen.color) @@ -299,7 +335,8 @@ def __init__(self, pen, points, filled=False): self.points = points self.filled = filled - def draw(self, cr, highlight=False): + # drnol: change highlight flag to pen_type + def draw(self, cr, pen_type=None): x0, y0 = self.points[0] cr.move_to(x0, y0) for i in xrange(1, len(self.points), 3): @@ -307,7 +344,8 @@ def draw(self, cr, highlight=False): x2, y2 = self.points[i + 1] x3, y3 = self.points[i + 2] cr.curve_to(x1, y1, x2, y2, x3, y3) - pen = self.select_pen(highlight) + # drnol: change highlight flag to pen_type + pen = self.select_pen(pen_type) if self.filled: cr.set_source_rgba(*pen.fillcolor) cr.fill_preserve() @@ -325,9 +363,10 @@ def __init__(self, shapes): Shape.__init__(self) self.shapes = shapes - def draw(self, cr, highlight=False): + # drnol: change highlight flag to pen + def draw(self, cr, pen_type=None): for shape in self.shapes: - shape.draw(cr, highlight=highlight) + shape.draw(cr, pen_type=pen_type) def search_text(self, regexp): for shape in self.shapes: @@ -338,23 +377,23 @@ def search_text(self, regexp): class Url(object): - def __init__(self, item, url, highlight=None): + def __init__(self, item, url, hover=None): self.item = item self.url = url - if highlight is None: - highlight = set([item]) - self.highlight = highlight + if hover is None: + hover = set([item]) + self.hover = hover class Jump(object): - def __init__(self, item, x, y, highlight=None): + def __init__(self, item, x, y, hover=None): self.item = item self.x = x self.y = y - if highlight is None: - highlight = set([item]) - self.highlight = highlight + if hover is None: + hover = set([item]) + self.hover = hover class Element(CompoundShape): @@ -416,10 +455,11 @@ def square_distance(x1, y1, x2, y2): class Edge(Element): - def __init__(self, src, dst, points, shapes): + def __init__(self, src, dst, points, shapes, dir): Element.__init__(self, shapes) self.src = src self.dst = dst + self.dir = dir self.points = points RADIUS = 10 @@ -439,9 +479,9 @@ def is_inside(self, x, y): def get_jump(self, x, y): if self.is_inside_begin(x, y): - return Jump(self, self.dst.x, self.dst.y, highlight=set([self, self.dst])) + return Jump(self, self.dst.x, self.dst.y, hover=set([self, self.dst])) if self.is_inside_end(x, y): - return Jump(self, self.src.x, self.src.y, highlight=set([self, self.src])) + return Jump(self, self.src.x, self.src.y, hover=set([self, self.src])) return None def __repr__(self): @@ -458,13 +498,35 @@ def __init__(self, width=1, height=1, shapes=(), nodes=(), edges=()): self.shapes = shapes self.nodes = nodes self.edges = edges + self.build_edge_map() # drnol: build edge lookup map + + # drnol: build edge lookup map + def build_edge_map(self): + self.edgemap = {} + for edge in self.edges: + # add src + if self.edgemap.has_key(edge.src): + self.edgemap[edge.src].append(edge) + else: + self.edgemap[edge.src] = [edge] + # add dst + if self.edgemap.has_key(edge.dst): + self.edgemap[edge.dst].append(edge) + else: + self.edgemap[edge.dst] = [edge] def get_size(self): return self.width, self.height - def draw(self, cr, highlight_items=None): - if highlight_items is None: - highlight_items = () + # drnol: highlight_items split to (hover...,selected...,path...) + def draw(self, cr, hover_items=None, selected_items=None, path_items=None): + if hover_items is None: + hover_items = () + if selected_items is None: + selected_items = () + if path_items is None: + path_items = () + cr.set_source_rgba(0.0, 0.0, 0.0, 1.0) cr.set_line_cap(cairo.LINE_CAP_BUTT) @@ -472,10 +534,26 @@ def draw(self, cr, highlight_items=None): for shape in self.shapes: shape.draw(cr) + for edge in self.edges: - edge.draw(cr, highlight=(edge in highlight_items)) + if edge in hover_items: + edge.draw(cr,HOVER_PEN) + elif edge in selected_items: + edge.draw(cr,SELECTED_PEN) + elif edge in path_items: + edge.draw(cr,PATH_PEN) + else: + edge.draw(cr) + for node in self.nodes: - node.draw(cr, highlight=(node in highlight_items)) + if node in hover_items: + node.draw(cr,HOVER_PEN) + elif node in selected_items: + node.draw(cr,SELECTED_PEN) + elif node in path_items: + node.draw(cr,PATH_PEN) + else: + node.draw(cr) def get_element(self, x, y): for node in self.nodes: @@ -503,6 +581,61 @@ def get_jump(self, x, y): return jump return None + # drnol: get ui shortest path + # use directed graph + def get_shortest_element_path(self, start, end): + node_path = self.find_shortest_node_path(start, end) + + if (node_path != None) and (len(node_path)>0): + path = [node_path[0]] + for i in range(0, (len(node_path)-1)): + src = node_path[i] + dst = node_path[i+1] + path.append(self.lookup_bidirection_edge(src,dst)) + path.append(dst) + + return path + else: + return None + + # drnol: edge lookup by src,dst + def lookup_edge(self,src,dst): + for edge in self.edgemap[src]: + if edge.dst == dst: + return edge + return None + + def lookup_bidirection_edge(self,src,dst): + edge = self.lookup_edge(src, dst) + if not edge: + edge = self.lookup_edge(dst, src) + return edge + + # drnol: simple shortest path calculator + # just get node path + # TODO: must be optimize + def find_shortest_node_path(self, start, end, path=[]): + path = path + [start] + + if start == end: + return path + + if not self.edgemap.has_key(start): + return None + + shortest = None + for edge in self.edgemap[start]: + if (edge.src == start) and (edge.dst not in path): + newpath = self.find_shortest_node_path(edge.dst, end, path) + if newpath: + if not shortest or len(newpath) < len(shortest): + shortest = newpath + elif (edge.dir=="none") and (edge.dst == start) and (edge.src not in path): # bi-direction support + newpath = self.find_shortest_node_path(edge.src, end, path) + if newpath: + if not shortest or len(newpath) < len(shortest): + shortest = newpath + return shortest BOLD = 1 ITALIC = 2 @@ -522,7 +655,7 @@ def __init__(self, parser, buf): self.parser = parser self.buf = buf self.pos = 0 - + self.pen = Pen() self.shapes = [] @@ -616,7 +749,7 @@ def lookup_color(self, c): b = b*s a = 1.0 return r, g, b, a - + sys.stderr.write("warning: unknown color '%s'\n" % c) return None @@ -691,7 +824,7 @@ def parse(self): sys.exit(1) return self.shapes - + def transform(self, x, y): return self.parser.transform(x, y) @@ -763,7 +896,7 @@ def __init__(self, msg=None, filename=None, line=None, col=None): def __str__(self): return ':'.join([str(part) for part in (self.filename, self.line, self.col, self.msg) if part != None]) - + class Scanner: """Stateless scanner.""" @@ -902,9 +1035,9 @@ def __init__(self, lexer): def match(self, type): if self.lookahead.type != type: raise ParseError( - msg = 'unexpected token %r' % self.lookahead.text, - filename = self.lexer.filename, - line = self.lookahead.line, + msg = 'unexpected token %r' % self.lookahead.text, + filename = self.lexer.filename, + line = self.lookahead.line, col = self.lookahead.col) def skip(self, type): @@ -1007,7 +1140,7 @@ def filter(self, type, text): text = text.replace('\\\r\n', '') text = text.replace('\\\r', '') text = text.replace('\\\n', '') - + # quotes text = text.replace('\\"', '"') @@ -1151,7 +1284,7 @@ class XDotParser(DotParser): def __init__(self, xdotcode): lexer = DotLexer(buf = xdotcode) DotParser.__init__(self, lexer) - + self.nodes = [] self.edges = [] self.shapes = [] @@ -1188,7 +1321,7 @@ def handle_graph(self, attrs): self.height = max(ymax - ymin, 1) self.top_graph = False - + for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"): if attr in attrs: parser = XDotAttrParser(self, attrs[attr]) @@ -1219,7 +1352,12 @@ def handle_edge(self, src_id, dst_id, attrs): pos = attrs['pos'] except KeyError: return - + + try: + dir = attrs['dir'] + except: + dir="" + points = self.parse_edge_pos(pos) shapes = [] for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"): @@ -1229,7 +1367,7 @@ def handle_edge(self, src_id, dst_id, attrs): if shapes: src = self.node_by_name[src_id] dst = self.node_by_name[dst_id] - self.edges.append(Edge(src, dst, points, shapes)) + self.edges.append(Edge(src, dst, points, shapes, dir)) def parse(self): DotParser.parse(self) @@ -1399,17 +1537,23 @@ def on_motion_notify(self, event): x, y, state = event.window.get_pointer() else: x, y, state = event.x, event.y, event.state + + # drnol: skip when shift is pressed + if state & gtk.gdk.CONTROL_MASK: + return + dot_widget = self.dot_widget item = dot_widget.get_url(x, y) if item is None: item = dot_widget.get_jump(x, y) if item is not None: dot_widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2)) - dot_widget.set_highlight(item.highlight) + # drnol: now hover instead highlight + dot_widget.set_hover(item.hover) else: dot_widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW)) - dot_widget.set_highlight(None) - + # drnol: now hover instead highlight + dot_widget.set_hover(None) class PanAction(DragAction): @@ -1474,7 +1618,9 @@ class DotWidget(gtk.DrawingArea): __gsignals__ = { 'expose-event': 'override', - 'clicked' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gtk.gdk.Event)) + 'clicked' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gtk.gdk.Event)), + # drnol: url right click action + 'url_right_clicked' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gtk.gdk.Event)) } filter = 'dot' @@ -1495,7 +1641,6 @@ def __init__(self): self.connect("scroll-event", self.on_area_scroll_event) self.connect("size-allocate", self.on_area_size_allocate) - self.connect('key-press-event', self.on_key_press_event) self.last_mtime = None gobject.timeout_add(1000, self.update) @@ -1506,7 +1651,21 @@ def __init__(self): self.animation = NoAnimation(self) self.drag_action = NullAction(self) self.presstime = None - self.highlight = None + + self.doc_init() + + + # drnol: doc init for multiple open + def doc_init(self): + # drnol: selections and path related variables + self.focused_index = None + self.pivot_node = None + self.selected_edge_index = None + + # drnol: 3 type highlights + self.hover = None + self.selected = None + self.path = None def set_filter(self, filter): self.filter = filter @@ -1549,6 +1708,8 @@ def set_dotcode(self, dotcode, filename=None): return False try: self.set_xdotcode(xdotcode) + # drnol: reset focus/selection state + self.doc_init() except ParseError as ex: dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, message_format=str(ex), @@ -1606,7 +1767,7 @@ def do_expose_event(self, event): cr.scale(self.zoom_ratio, self.zoom_ratio) cr.translate(-self.x, -self.y) - self.graph.draw(cr, highlight_items=self.highlight) + self.graph.draw(cr, hover_items=self.hover, selected_items=self.selected, path_items=self.path) cr.restore() self.drag_action.draw(cr) @@ -1621,11 +1782,56 @@ def set_current_pos(self, x, y): self.y = y self.queue_draw() - def set_highlight(self, items): - if self.highlight != items: - self.highlight = items + # drnol: highlight changed to hover + def set_hover(self, items): + if self.hover != items: + self.hover = items + self.queue_draw() + + # drnol: select node + def set_pivot_node(self, node): + self.pivot_node = node + + # reset focus/edge selection/path + if self.pivot_node != None: + self.focused_index = None + self.selected_edge_index = None + self.path = None + + # highlight + if node != None: + self.selected = [node] + else: + self.selected = None + + self.queue_draw() + + # drnol: select edge + def set_selected_edge(self, edge): + if self.selected_edge_index != edge: + if self.pivot_node == edge.src: + self.selected = [edge.src, edge, edge.dst] + elif self.pivot_node == edge.dst: + self.selected = [edge.dst, edge, edge.src] + else: + self.set_pivot_node(edge.src) + self.selected = [edge.src, edge, edge.dst] + self.queue_draw() + + # drnol: renew selected items + def set_selected(self, items): + self.focused_index = None + self.pivot_node = None + self.selected_edge_index = None + if self.selected != items: + self.selected = items self.queue_draw() + # drnol: set path + def set_path(self, path): + self.path = path + self.queue_draw() + def zoom_image(self, zoom_ratio, center=False, pos=None): # Constrain zoom ratio to a sane range to prevent numeric instability. zoom_ratio = min(zoom_ratio, 1E4) @@ -1740,8 +1946,136 @@ def on_key_press_event(self, widget, event): if event.keyval == gtk.keysyms.p: self.on_print() return True + if event.keyval == gtk.keysyms.comma: + # drnol: select prev edge of focused node + self.select_prev_edge_of_pivot_node() + return True + if event.keyval == gtk.keysyms.period: + # drnol: select next edge of focused node + self.select_next_edge_of_pivot_node() + return True + if event.keyval == gtk.keysyms.Return: + # drnol: follow selected edge + self.follow_selected_edge() + return True return False + # drnol: jump to prev selected item + def jump_to_prev_selected_node(self): + list = self.get_traversal_list() + if list: + if (self.focused_index == None) or (self.focused_index == 0): + self.focused_index = len(list)-1 + else: + self.focused_index = self.focused_index-1 + element = list[self.focused_index] + if isinstance(element,Node): + self.focus_node(element) + else: + self.jump_to_prev_selected_node() # skip edge + + # drnol: jump to next selected item + def jump_to_next_selected_node(self): + list = self.get_traversal_list() + if list != None: + if self.focused_index == None: + self.focused_index = 0 + else: + self.focused_index = (self.focused_index+1) % len(list) + element = list[self.focused_index] + if isinstance(element,Node): + self.focus_node(element) + else: + self.jump_to_next_selected_node() # skip edge + + # drnol: if path is displayed then traversal list is path otherwise selected nodes + def get_traversal_list(self): + if self.path != None: + for element in self.path: + if isinstance(element,Node): + return self.path + else: + for element in self.selected: + if isinstance(element,Node): + return self.selected + return None + + # drnol: focus node + def focus_node(self, node): + self.selected_edge = None + self.pivot_node = node + if self.path != None: + self.selected=[self.pivot_node] + self.queue_draw() + self.animate_to(node.x, node.y) + + # drnol: focus node by coordinate + def focus_node_at(self, x, y): + elt = self.get_element(x, y) + if (elt != None) and (isinstance(elt,Node)): + self.focused_index = 0 + self.pivot_node = elt + + # drnol: select prev edge of focused node + def select_prev_edge_of_pivot_node(self): + if self.pivot_node != None: + if self.graph.edgemap.has_key(self.pivot_node): + edges = self.graph.edgemap[self.pivot_node] + # if there is more than one edge in the selected node + if len(edges) > 0: + if (self.selected_edge_index == None) or (self.selected_edge_index <= 0): + self.selected_edge_index = len(edges)-1 + else: + self.selected_edge_index = self.selected_edge_index-1 + edge = edges[self.selected_edge_index] + self.set_selected_edge(edge) + + # drnol: select next edge of focused node + def select_next_edge_of_pivot_node(self): + if self.pivot_node != None: + if self.graph.edgemap.has_key(self.pivot_node): + edges = self.graph.edgemap[self.pivot_node] + # if there is more than one edge in the selected node + if len(edges) > 0: + if self.selected_edge_index == None: + self.selected_edge_index = 0 + else: + self.selected_edge_index = (self.selected_edge_index+1) % len(edges) + edge = edges[self.selected_edge_index] + self.set_selected_edge(edge) + + #drnol: follow selected edge + def follow_selected_edge(self): + if (self.pivot_node != None) and (self.selected_edge_index != None): + edges = self.graph.edgemap[self.pivot_node] + edge = edges[self.selected_edge_index] + + # follow to opposite node + if edge.src == self.pivot_node: + target = edge.dst + else: + target = edge.src + + # jump to selected node + self.focus_node(target) + + # automatically select one of other edges + self.selected_edge_index, new_edge = self.try_find_other_edge_index(edge) + self.set_selected_edge(new_edge) + + #drnol: try finding the other edge + def try_find_other_edge_index(self, edge): + edges = self.graph.edgemap[self.pivot_node] + + edge_index = 0 + new_edge = edges[0] + for i in range(1, len(edges)): + if edges[i] != edge: + edge_index = i + new_edge = edges[i] + + return edge_index, new_edge + print_settings = None def on_print(self, action=None): print_op = gtk.PrintOperation() @@ -1769,7 +2103,7 @@ def draw_page(self, operation, context, page_nr): cr.scale(self.zoom_ratio, self.zoom_ratio) cr.translate(-self.x, -self.y) - self.graph.draw(cr, highlight_items=self.highlight) + self.graph.draw(cr, hover_items=self.hover, selected_items=self.selected, path_items=self.path) def get_drag_action(self, event): state = event.state @@ -1811,6 +2145,57 @@ def on_click(self, element, event): (click on empty space).""" return False + # drnol: display shortest path + # if there is one selected node A -> path between A and just before ctrl-clicked node B + # if there are previously displayed path and pivot node A -> path between A and just before ctrl-clicked node B + def show_path(self, end_node, reverse=False): + # start node check + if self.pivot_node == None: + dlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, + message_format="You must select start node.", + buttons=gtk.BUTTONS_OK) + dlg.set_title("Path Selection Error!") + dlg.run() + dlg.destroy() + return + + self.focused_index = 0 + self.selected = [self.pivot_node] + + # build path + if reverse: + self.build_path(end_node, self.pivot_node) + else: + self.build_path(self.pivot_node, end_node) + + if not self.path: + dlg = gtk.MessageDialog(type=gtk.MESSAGE_INFO, + message_format="There is no path", + buttons=gtk.BUTTONS_OK) + dlg.set_title("Path Info") + dlg.run() + dlg.destroy() + else: + # display path + self.set_path(self.path) + self.queue_draw() + + def build_path(self, start, end): + self.path = self.graph.get_shortest_element_path(start, end) + + # drnol: url right click implementation + def do_url_right_clicked(self, url, event): + # drnol: open new dot files directives + # currently depend on operating system + if (url.index("dots://")==0) and (os.uname()[0]=="Linux"): + dots = url[7:].split(';') + dirname = os.path.dirname(self.openfilename) + if dirname!="": + dirname=dirname+"/" + for dot in dots: + subprocess.Popen([sys.argv[0], dirname+dot]) + return + def on_area_button_release(self, area, event): self.drag_action.on_button_release(event) self.drag_action = NullAction(self) @@ -1824,12 +2209,29 @@ def on_area_button_release(self, area, event): url = self.get_url(x, y) if url is not None: self.emit('clicked', unicode(url.url), event) - else: - jump = self.get_jump(x, y) - if jump is not None: - self.animate_to(jump.x, jump.y) - + # drnol: disable url node's skipping action because of node selection + # else: + jump = self.get_jump(x, y) + if jump is not None: + self.animate_to(jump.x, jump.y) + + # drnol: node selection + element = self.get_element(x, y) + if isinstance(element, Node): + if event.state & gtk.gdk.CONTROL_MASK: + if event.state & gtk.gdk.SHIFT_MASK: + self.show_path(element, True) # reverse path + else: + self.show_path(element) # normal path + else: # start node selection + self.set_pivot_node(element) + else: # edge selection + self.set_selected_edge(element) return True + elif event.button == 3: + url = self.get_url(x, y) + if url is not None: + self.emit('url_right_clicked', unicode(url.url), event) if event.button == 1 or event.button == 2: return True @@ -1903,6 +2305,10 @@ class DotWindow(gtk.Window): + + + + @@ -1924,6 +2330,8 @@ def __init__(self, widget=None): self.widget = widget or DotWidget() + self.connect('key-press-event', self.on_window_key_press_event) + # Create a UIManager instance uimanager = self.uimanager = gtk.UIManager() @@ -1944,6 +2352,9 @@ def __init__(self, widget=None): ('ZoomOut', gtk.STOCK_ZOOM_OUT, None, None, None, self.widget.on_zoom_out), ('ZoomFit', gtk.STOCK_ZOOM_FIT, None, None, None, self.widget.on_zoom_fit), ('Zoom100', gtk.STOCK_ZOOM_100, None, None, None, self.widget.on_zoom_100), + ('Previous', gtk.STOCK_GO_BACK, None, None, None, self.on_prev), + ('Next', gtk.STOCK_GO_FORWARD, None, None, None, self.on_next), + ('Quit', gtk.STOCK_QUIT, None, None, None, self.on_quit), )) find_action = FindMenuToolAction("Find", None, @@ -1960,6 +2371,14 @@ def __init__(self, widget=None): toolbar = uimanager.get_widget('/ToolBar') vbox.pack_start(toolbar, False) + # Create a text entry box for the filename + file_entry = gtk.Entry() + self.file_entry = file_entry + file_entry.set_has_frame(True) + file_entry.set_text('') + file_entry.connect("activate", self.on_text_open) + vbox.pack_start(file_entry, False) + vbox.pack_start(self.widget) self.last_open_dir = "." @@ -1989,26 +2408,31 @@ def find_text(self, entry_text): def textentry_changed(self, widget, entry): entry_text = entry.get_text() - dot_widget = self.widget + dot_widget = self.widget if not entry_text: - dot_widget.set_highlight(None) + dot_widget.set_selected(None) return - + found_items = self.find_text(entry_text) - dot_widget.set_highlight(found_items) + dot_widget.set_selected(found_items) def textentry_activate(self, widget, entry): entry_text = entry.get_text() - dot_widget = self.widget + dot_widget = self.widget if not entry_text: - dot_widget.set_highlight(None) + dot_widget.set_selected(None) return; - + found_items = self.find_text(entry_text) - dot_widget.set_highlight(found_items) if(len(found_items) == 1): - dot_widget.animate_to(found_items[0].x, found_items[0].y) + # drnol: select/focus node + dot_widget.set_pivot_node(found_items[0]) + dot_widget.focus_node(found_items[0]) + else: + dot_widget.set_selected(found_items) + win = widget.get_toplevel() + win.set_focus(win.uimanager.get_widget('/')) def set_filter(self, filter): self.widget.set_filter(filter) @@ -2021,7 +2445,7 @@ def set_xdotcode(self, xdotcode, filename=None): if self.widget.set_xdotcode(xdotcode): self.update_title(filename) self.widget.zoom_to_fit() - + def update_title(self, filename=None): if filename is None: self.set_title(self.base_title) @@ -2030,6 +2454,7 @@ def update_title(self, filename=None): def open_file(self, filename): try: + self.file_entry.set_text(filename) fp = file(filename, 'rt') self.set_dotcode(fp.read(), filename) fp.close() @@ -2041,6 +2466,30 @@ def open_file(self, filename): dlg.run() dlg.destroy() + def build_file_list(self, filepath): + try: + filepath = os.path.abspath(filepath) + if os.path.isdir(filepath): + directory = filepath + else: + directory = os.path.dirname(filepath) + sori = lambda x: (int(x) if x.isdigit() else x) # returns s(tring) or i(nteger) + natkey = lambda x: [sori(y) for y in re.split(r'(\d+)', x)] + self.files_in_dir = sorted([ os.path.join(directory, f) for f in os.listdir(directory) if f.endswith('.dot')], + key=natkey) + if os.path.isdir(filepath): + self.file_index = 0 + else: + self.file_index = self.files_in_dir.index(filepath) + self.file_dir = directory + except Exception as ex: + dlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, + message_format=str(ex), + buttons=gtk.BUTTONS_OK) + dlg.set_title(self.base_title) + dlg.run() + dlg.destroy() + def on_open(self, action): chooser = gtk.FileChooserDialog(title="Open dot File", action=gtk.FILE_CHOOSER_ACTION_OPEN, @@ -2058,17 +2507,89 @@ def on_open(self, action): filter.set_name("All files") filter.add_pattern("*") chooser.add_filter(filter) + try: + chooser.set_current_folder(self.file_dir) + except AttributeError: + pass # Will happen on the first call, because self.file_dir would not exist + except: + raise if chooser.run() == gtk.RESPONSE_OK: filename = chooser.get_filename() self.last_open_dir = chooser.get_current_folder() chooser.destroy() + self.build_file_list(filename) self.open_file(filename) else: chooser.destroy() + def on_text_open(self, action): + fileentry = self.file_entry.get_text() + if os.path.exists(fileentry): + fileentry = os.path.abspath(fileentry) + self.file_entry.set_text(fileentry) + self.file_entry.set_position(1000) # some large number to push the cursor to the end + + if os.path.isfile(fileentry): + self.build_file_list(fileentry) + self.open_file(fileentry) + self.child_focus(gtk.DIR_TAB_FORWARD) + elif os.path.isdir(fileentry): + self.build_file_list(fileentry) + if self.files_in_dir: # found .dot files + self.open_file(self.files_in_dir[0]) + self.child_focus(gtk.DIR_TAB_FORWARD) + def on_reload(self, action): self.widget.reload() + def on_quit(self, action): + gtk.main_quit() + + def on_next(self, action): + try: + self.file_index += 1 + if self.file_index >= len(self.files_in_dir): + self.file_index = 0 + self.open_file(self.files_in_dir[self.file_index]) + except: + # can happen when the button is pushed with no file loaded + pass + + def on_prev(self, action): + try: + self.file_index -= 1 + if self.file_index < 0: + self.file_index = len(self.files_in_dir)-1 + self.open_file(self.files_in_dir[self.file_index]) + except: + # can happen when the button is pushed with no file loaded + pass + + def on_window_key_press_event(self, widget, event): + if event.keyval == gtk.keysyms.F2: + # drnol: jump to prev highlighted item + self.widget.jump_to_prev_selected_node() + return True + elif event.keyval == gtk.keysyms.F3: + # drnol: jump to next highlighted item + self.widget.jump_to_next_selected_node() + return True + + if self.file_entry.is_focus() or self.textentry.is_focus(): + return False + + if event.keyval == gtk.keysyms.p or event.keyval == gtk.keysyms.j: + self.on_prev(None) + return True + elif event.keyval == gtk.keysyms.n or event.keyval == gtk.keysyms.k: + self.on_next(None) + return True + elif event.keyval == gtk.keysyms.o: + self.on_open(None) + return True + else: + return self.widget.on_key_press_event(widget, event) + class OptionParser(optparse.OptionParser): @@ -2079,7 +2600,6 @@ def format_epilog(self, formatter): def main(): - parser = OptionParser( usage='\n\t%prog [file]', epilog=''' @@ -2094,6 +2614,17 @@ def main(): Escape halt animation Ctrl-drag zoom in/out Shift-drag zooms an area + F2 prev highlighted item + F3 next highlighted item + , select prev focused node's edge + . select next focused node's edge + Enter follow selected edge + p,j prev file + n,k next file + o browse file + Ctrl-click display shortest path (retargetable) + Ctrl-shift-click display reverse shortest path (retargetable) + right click dot url open ([URL="dots://file1;file2;..."]) (Linux only) ''' ) parser.add_option( @@ -2120,30 +2651,31 @@ def main(): if args[0] == '-': win.set_dotcode(sys.stdin.read()) else: + win.build_file_list(args[0]) win.open_file(args[0]) gtk.main() # Apache-Style Software License for ColorBrewer software and ColorBrewer Color # Schemes, Version 1.1 -# +# # Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and The Pennsylvania State # University. All rights reserved. -# +# # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: -# +# # 1. Redistributions as source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. +# this list of conditions and the following disclaimer. # # 2. The end-user documentation included with the redistribution, if any, # must include the following acknowledgment: -# +# # This product includes color specifications and designs developed by # Cynthia Brewer (http://colorbrewer.org/). -# +# # Alternately, this acknowledgment may appear in the software itself, if and -# wherever such third-party acknowledgments normally appear. +# wherever such third-party acknowledgments normally appear. # # 3. The name "ColorBrewer" must not be used to endorse or promote products # derived from this software without prior written permission. For written @@ -2151,8 +2683,8 @@ def main(): # # 4. Products derived from this software may not be called "ColorBrewer", # nor may "ColorBrewer" appear in their name, without prior written -# permission of Cynthia Brewer. -# +# permission of Cynthia Brewer. +# # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES, # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND # FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CYNTHIA @@ -2162,7 +2694,7 @@ def main(): # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. brewer_colors = { 'accent3': [(127, 201, 127), (190, 174, 212), (253, 192, 134)], 'accent4': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153)],