Fixes #1584 (hello @w3af looking for the patch of this one ;)

This commit is contained in:
Miroslav Stampar 2015-12-07 16:17:28 +01:00
parent b5b3411f16
commit af60f11319
2 changed files with 374 additions and 105 deletions

View File

@ -87,5 +87,4 @@ def profile(profileOutputFile=None, dotOutputFile=None, imageOutputFile=None):
win.connect('destroy', gtk.main_quit) win.connect('destroy', gtk.main_quit)
win.set_filter("dot") win.set_filter("dot")
win.open_file(dotOutputFile) win.open_file(dotOutputFile)
gobject.timeout_add(1000, win.update, dotOutputFile)
gtk.main() gtk.main()

View File

@ -16,11 +16,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
'''Visualize dot graphs via the xdot Format.''' '''Visualize dot graphs via the xdot format.'''
__author__ = "Jose Fonseca" __author__ = "Jose Fonseca et al"
__version__ = "0.4"
import os import os
@ -30,6 +28,7 @@ import math
import colorsys import colorsys
import time import time
import re import re
import optparse
import gobject import gobject
import gtk import gtk
@ -90,13 +89,12 @@ class Shape:
else: else:
return self.pen return self.pen
def search_text(self, regexp):
return False
class TextShape(Shape): class TextShape(Shape):
#fontmap = pangocairo.CairoFontMap()
#fontmap.set_resolution(72)
#context = fontmap.create_context()
LEFT, CENTER, RIGHT = -1, 0, 1 LEFT, CENTER, RIGHT = -1, 0, 1
def __init__(self, pen, x, y, j, w, t): def __init__(self, pen, x, y, j, w, t):
@ -191,6 +189,33 @@ class TextShape(Shape):
cr.line_to(x+self.w, self.y) cr.line_to(x+self.w, self.y)
cr.stroke() cr.stroke()
def search_text(self, regexp):
return regexp.search(self.t) is not None
class ImageShape(Shape):
def __init__(self, pen, x0, y0, w, h, path):
Shape.__init__(self)
self.pen = pen.copy()
self.x0 = x0
self.y0 = y0
self.w = w
self.h = h
self.path = path
def draw(self, cr, highlight=False):
cr2 = gtk.gdk.CairoContext(cr)
pixbuf = gtk.gdk.pixbuf_new_from_file(self.path)
sx = float(self.w)/float(pixbuf.get_width())
sy = float(self.h)/float(pixbuf.get_height())
cr.save()
cr.translate(self.x0, self.y0 - self.h)
cr.scale(sx, sy)
cr2.set_source_pixbuf(pixbuf, 0, 0)
cr2.paint()
cr.restore()
class EllipseShape(Shape): class EllipseShape(Shape):
@ -304,6 +329,12 @@ class CompoundShape(Shape):
for shape in self.shapes: for shape in self.shapes:
shape.draw(cr, highlight=highlight) shape.draw(cr, highlight=highlight)
def search_text(self, regexp):
for shape in self.shapes:
if shape.search_text(regexp):
return True
return False
class Url(object): class Url(object):
@ -332,6 +363,9 @@ class Element(CompoundShape):
def __init__(self, shapes): def __init__(self, shapes):
CompoundShape.__init__(self, shapes) CompoundShape.__init__(self, shapes)
def is_inside(self, x, y):
return False
def get_url(self, x, y): def get_url(self, x, y):
return None return None
@ -341,9 +375,10 @@ class Element(CompoundShape):
class Node(Element): class Node(Element):
def __init__(self, x, y, w, h, shapes, url): def __init__(self, id, x, y, w, h, shapes, url):
Element.__init__(self, shapes) Element.__init__(self, shapes)
self.id = id
self.x = x self.x = x
self.y = y self.y = y
@ -360,7 +395,6 @@ class Node(Element):
def get_url(self, x, y): def get_url(self, x, y):
if self.url is None: if self.url is None:
return None return None
#print (x, y), (self.x1, self.y1), "-", (self.x2, self.y2)
if self.is_inside(x, y): if self.is_inside(x, y):
return Url(self, self.url) return Url(self, self.url)
return None return None
@ -370,6 +404,9 @@ class Node(Element):
return Jump(self, self.x, self.y) return Jump(self, self.x, self.y)
return None return None
def __repr__(self):
return "<Node %s>" % self.id
def square_distance(x1, y1, x2, y2): def square_distance(x1, y1, x2, y2):
deltax = x2 - x1 deltax = x2 - x1
@ -387,13 +424,29 @@ class Edge(Element):
RADIUS = 10 RADIUS = 10
def is_inside_begin(self, x, y):
return square_distance(x, y, *self.points[0]) <= self.RADIUS*self.RADIUS
def is_inside_end(self, x, y):
return square_distance(x, y, *self.points[-1]) <= self.RADIUS*self.RADIUS
def is_inside(self, x, y):
if self.is_inside_begin(x, y):
return True
if self.is_inside_end(x, y):
return True
return False
def get_jump(self, x, y): def get_jump(self, x, y):
if square_distance(x, y, *self.points[0]) <= self.RADIUS*self.RADIUS: 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, highlight=set([self, self.dst]))
if square_distance(x, y, *self.points[-1]) <= self.RADIUS*self.RADIUS: 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, highlight=set([self, self.src]))
return None return None
def __repr__(self):
return "<Edge %s -> %s>" % (self.src, self.dst)
class Graph(Shape): class Graph(Shape):
@ -424,6 +477,14 @@ class Graph(Shape):
for node in self.nodes: for node in self.nodes:
node.draw(cr, highlight=(node in highlight_items)) node.draw(cr, highlight=(node in highlight_items))
def get_element(self, x, y):
for node in self.nodes:
if node.is_inside(x, y):
return node
for edge in self.edges:
if edge.is_inside(x, y):
return edge
def get_url(self, x, y): def get_url(self, x, y):
for node in self.nodes: for node in self.nodes:
url = node.get_url(x, y) url = node.get_url(x, y)
@ -443,6 +504,14 @@ class Graph(Shape):
return None return None
BOLD = 1
ITALIC = 2
UNDERLINE = 4
SUPERSCRIPT = 8
SUBSCRIPT = 16
STRIKE_THROUGH = 32
class XDotAttrParser: class XDotAttrParser:
"""Parser for xdot drawing attributes. """Parser for xdot drawing attributes.
See also: See also:
@ -451,7 +520,7 @@ class XDotAttrParser:
def __init__(self, parser, buf): def __init__(self, parser, buf):
self.parser = parser self.parser = parser
self.buf = self.unescape(buf) self.buf = buf
self.pos = 0 self.pos = 0
self.pen = Pen() self.pen = Pen()
@ -460,11 +529,6 @@ class XDotAttrParser:
def __nonzero__(self): def __nonzero__(self):
return self.pos < len(self.buf) return self.pos < len(self.buf)
def unescape(self, buf):
buf = buf.replace('\\"', '"')
buf = buf.replace('\\n', '\n')
return buf
def read_code(self): def read_code(self):
pos = self.buf.find(" ", self.pos) pos = self.buf.find(" ", self.pos)
res = self.buf[self.pos:pos] res = self.buf[self.pos:pos]
@ -473,19 +537,19 @@ class XDotAttrParser:
self.pos += 1 self.pos += 1
return res return res
def read_number(self): def read_int(self):
return int(self.read_code()) return int(self.read_code())
def read_float(self): def read_float(self):
return float(self.read_code()) return float(self.read_code())
def read_point(self): def read_point(self):
x = self.read_number() x = self.read_float()
y = self.read_number() y = self.read_float()
return self.transform(x, y) return self.transform(x, y)
def read_text(self): def read_text(self):
num = self.read_number() num = self.read_int()
pos = self.buf.find("-", self.pos) + 1 pos = self.buf.find("-", self.pos) + 1
self.pos = pos + num self.pos = pos + num
res = self.buf[pos:self.pos] res = self.buf[pos:self.pos]
@ -494,7 +558,7 @@ class XDotAttrParser:
return res return res
def read_polygon(self): def read_polygon(self):
n = self.read_number() n = self.read_int()
p = [] p = []
for i in range(n): for i in range(n):
x, y = self.read_point() x, y = self.read_point()
@ -521,6 +585,9 @@ class XDotAttrParser:
r, g, b = colorsys.hsv_to_rgb(h, s, v) r, g, b = colorsys.hsv_to_rgb(h, s, v)
a = 1.0 a = 1.0
return r, g, b, a return r, g, b, a
elif c1 == "[":
sys.stderr.write('warning: color gradients not supported yet\n')
return None
else: else:
return self.lookup_color(c) return self.lookup_color(c)
@ -550,7 +617,7 @@ class XDotAttrParser:
a = 1.0 a = 1.0
return r, g, b, a return r, g, b, a
sys.stderr.write("unknown color '%s'\n" % c) sys.stderr.write("warning: unknown color '%s'\n" % c)
return None return None
def parse(self): def parse(self):
@ -573,7 +640,7 @@ class XDotAttrParser:
lw = style.split("(")[1].split(")")[0] lw = style.split("(")[1].split(")")[0]
lw = float(lw) lw = float(lw)
self.handle_linewidth(lw) self.handle_linewidth(lw)
elif style in ("solid", "dashed"): elif style in ("solid", "dashed", "dotted"):
self.handle_linestyle(style) self.handle_linestyle(style)
elif op == "F": elif op == "F":
size = s.read_float() size = s.read_float()
@ -581,19 +648,22 @@ class XDotAttrParser:
self.handle_font(size, name) self.handle_font(size, name)
elif op == "T": elif op == "T":
x, y = s.read_point() x, y = s.read_point()
j = s.read_number() j = s.read_int()
w = s.read_number() w = s.read_float()
t = s.read_text() t = s.read_text()
self.handle_text(x, y, j, w, t) self.handle_text(x, y, j, w, t)
elif op == "t":
f = s.read_int()
self.handle_font_characteristics(f)
elif op == "E": elif op == "E":
x0, y0 = s.read_point() x0, y0 = s.read_point()
w = s.read_number() w = s.read_float()
h = s.read_number() h = s.read_float()
self.handle_ellipse(x0, y0, w, h, filled=True) self.handle_ellipse(x0, y0, w, h, filled=True)
elif op == "e": elif op == "e":
x0, y0 = s.read_point() x0, y0 = s.read_point()
w = s.read_number() w = s.read_float()
h = s.read_number() h = s.read_float()
self.handle_ellipse(x0, y0, w, h, filled=False) self.handle_ellipse(x0, y0, w, h, filled=False)
elif op == "L": elif op == "L":
points = self.read_polygon() points = self.read_polygon()
@ -610,9 +680,15 @@ class XDotAttrParser:
elif op == "p": elif op == "p":
points = self.read_polygon() points = self.read_polygon()
self.handle_polygon(points, filled=False) self.handle_polygon(points, filled=False)
elif op == "I":
x0, y0 = s.read_point()
w = s.read_float()
h = s.read_float()
path = s.read_text()
self.handle_image(x0, y0, w, h, path)
else: else:
sys.stderr.write("unknown xdot opcode '%s'\n" % op) sys.stderr.write("error: unknown xdot opcode '%s'\n" % op)
break sys.exit(1)
return self.shapes return self.shapes
@ -633,11 +709,18 @@ class XDotAttrParser:
self.pen.dash = () self.pen.dash = ()
elif style == "dashed": elif style == "dashed":
self.pen.dash = (6, ) # 6pt on, 6pt off self.pen.dash = (6, ) # 6pt on, 6pt off
elif style == "dotted":
self.pen.dash = (2, 4) # 2pt on, 4pt off
def handle_font(self, size, name): def handle_font(self, size, name):
self.pen.fontsize = size self.pen.fontsize = size
self.pen.fontname = name self.pen.fontname = name
def handle_font_characteristics(self, flags):
# TODO
if flags != 0:
sys.stderr.write("warning: font characteristics not supported yet\n" % op)
def handle_text(self, x, y, j, w, t): def handle_text(self, x, y, j, w, t):
self.shapes.append(TextShape(self.pen, x, y, j, w, t)) self.shapes.append(TextShape(self.pen, x, y, j, w, t))
@ -647,6 +730,9 @@ class XDotAttrParser:
self.shapes.append(EllipseShape(self.pen, x0, y0, w, h, filled=True)) self.shapes.append(EllipseShape(self.pen, x0, y0, w, h, filled=True))
self.shapes.append(EllipseShape(self.pen, x0, y0, w, h)) self.shapes.append(EllipseShape(self.pen, x0, y0, w, h))
def handle_image(self, x0, y0, w, h, path):
self.shapes.append(ImageShape(self.pen, x0, y0, w, h, path))
def handle_line(self, points): def handle_line(self, points):
self.shapes.append(LineShape(self.pen, points)) self.shapes.append(LineShape(self.pen, points))
@ -922,10 +1008,11 @@ class DotLexer(Lexer):
text = text.replace('\\\r', '') text = text.replace('\\\r', '')
text = text.replace('\\\n', '') text = text.replace('\\\n', '')
text = text.replace('\\r', '\r') # quotes
text = text.replace('\\n', '\n') text = text.replace('\\"', '"')
text = text.replace('\\t', '\t')
text = text.replace('\\', '') # layout engines recognize other escape codes (many non-standard)
# but we don't translate them here
type = ID type = ID
@ -1059,6 +1146,8 @@ class DotParser(Parser):
class XDotParser(DotParser): class XDotParser(DotParser):
XDOTVERSION = '1.6'
def __init__(self, xdotcode): def __init__(self, xdotcode):
lexer = DotLexer(buf = xdotcode) lexer = DotLexer(buf = xdotcode)
DotParser.__init__(self, lexer) DotParser.__init__(self, lexer)
@ -1071,26 +1160,34 @@ class XDotParser(DotParser):
def handle_graph(self, attrs): def handle_graph(self, attrs):
if self.top_graph: if self.top_graph:
# Check xdot version
try:
xdotversion = attrs['xdotversion']
except KeyError:
pass
else:
if float(xdotversion) > float(self.XDOTVERSION):
sys.stderr.write('warning: xdot version %s, but supported is %s\n' % (xdotversion, self.XDOTVERSION))
# Parse bounding box
try: try:
bb = attrs['bb'] bb = attrs['bb']
except KeyError: except KeyError:
return return
if not bb: if bb:
return xmin, ymin, xmax, ymax = map(float, bb.split(","))
xmin, ymin, xmax, ymax = map(float, bb.split(",")) self.xoffset = -xmin
self.yoffset = -ymax
self.xscale = 1.0
self.yscale = -1.0
# FIXME: scale from points to pixels
self.xoffset = -xmin self.width = max(xmax - xmin, 1)
self.yoffset = -ymax self.height = max(ymax - ymin, 1)
self.xscale = 1.0
self.yscale = -1.0
# FIXME: scale from points to pixels
self.width = xmax - xmin self.top_graph = False
self.height = ymax - ymin
self.top_graph = False
for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"): for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"):
if attr in attrs: if attr in attrs:
@ -1104,15 +1201,15 @@ class XDotParser(DotParser):
return return
x, y = self.parse_node_pos(pos) x, y = self.parse_node_pos(pos)
w = float(attrs['width'])*72 w = float(attrs.get('width', 0))*72
h = float(attrs['height'])*72 h = float(attrs.get('height', 0))*72
shapes = [] shapes = []
for attr in ("_draw_", "_ldraw_"): for attr in ("_draw_", "_ldraw_"):
if attr in attrs: if attr in attrs:
parser = XDotAttrParser(self, attrs[attr]) parser = XDotAttrParser(self, attrs[attr])
shapes.extend(parser.parse()) shapes.extend(parser.parse())
url = attrs.get('URL', None) url = attrs.get('URL', None)
node = Node(x, y, w, h, shapes, url) node = Node(id, x, y, w, h, shapes, url)
self.node_by_name[id] = node self.node_by_name[id] = node
if shapes: if shapes:
self.nodes.append(node) self.nodes.append(node)
@ -1399,6 +1496,9 @@ class DotWidget(gtk.DrawingArea):
self.connect("size-allocate", self.on_area_size_allocate) self.connect("size-allocate", self.on_area_size_allocate)
self.connect('key-press-event', self.on_key_press_event) self.connect('key-press-event', self.on_key_press_event)
self.last_mtime = None
gobject.timeout_add(1000, self.update)
self.x, self.y = 0.0, 0.0 self.x, self.y = 0.0, 0.0
self.zoom_ratio = 1.0 self.zoom_ratio = 1.0
@ -1411,9 +1511,9 @@ class DotWidget(gtk.DrawingArea):
def set_filter(self, filter): def set_filter(self, filter):
self.filter = filter self.filter = filter
def set_dotcode(self, dotcode, filename='<stdin>'): def run_filter(self, dotcode):
if isinstance(dotcode, unicode): if not self.filter:
dotcode = dotcode.encode('utf8') return dotcode
p = subprocess.Popen( p = subprocess.Popen(
[self.filter, '-Txdot'], [self.filter, '-Txdot'],
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
@ -1423,6 +1523,7 @@ class DotWidget(gtk.DrawingArea):
universal_newlines=True universal_newlines=True
) )
xdotcode, error = p.communicate(dotcode) xdotcode, error = p.communicate(dotcode)
sys.stderr.write(error)
if p.returncode != 0: if p.returncode != 0:
dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
message_format=error, message_format=error,
@ -1430,10 +1531,19 @@ class DotWidget(gtk.DrawingArea):
dialog.set_title('Dot Viewer') dialog.set_title('Dot Viewer')
dialog.run() dialog.run()
dialog.destroy() dialog.destroy()
return None
return xdotcode
def set_dotcode(self, dotcode, filename=None):
self.openfilename = None
if isinstance(dotcode, unicode):
dotcode = dotcode.encode('utf8')
xdotcode = self.run_filter(dotcode)
if xdotcode is None:
return False return False
try: try:
self.set_xdotcode(xdotcode) self.set_xdotcode(xdotcode)
except ParseError, ex: except ParseError as ex:
dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
message_format=str(ex), message_format=str(ex),
buttons=gtk.BUTTONS_OK) buttons=gtk.BUTTONS_OK)
@ -1442,11 +1552,14 @@ class DotWidget(gtk.DrawingArea):
dialog.destroy() dialog.destroy()
return False return False
else: else:
if filename is None:
self.last_mtime = None
else:
self.last_mtime = os.stat(filename).st_mtime
self.openfilename = filename self.openfilename = filename
return True return True
def set_xdotcode(self, xdotcode): def set_xdotcode(self, xdotcode):
#print xdotcode
parser = XDotParser(xdotcode) parser = XDotParser(xdotcode)
self.graph = parser.parse() self.graph = parser.parse()
self.zoom_image(self.zoom_ratio, center=True) self.zoom_image(self.zoom_ratio, center=True)
@ -1460,6 +1573,14 @@ class DotWidget(gtk.DrawingArea):
except IOError: except IOError:
pass pass
def update(self):
if self.openfilename is not None:
current_mtime = os.stat(self.openfilename).st_mtime
if current_mtime != self.last_mtime:
self.last_mtime = current_mtime
self.reload()
return True
def do_expose_event(self, event): def do_expose_event(self, event):
cr = self.window.cairo_create() cr = self.window.cairo_create()
@ -1500,6 +1621,10 @@ class DotWidget(gtk.DrawingArea):
self.queue_draw() self.queue_draw()
def zoom_image(self, zoom_ratio, center=False, pos=None): 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)
zoom_ratio = max(zoom_ratio, 1E-6)
if center: if center:
self.x = self.graph.width/2 self.x = self.graph.width/2
self.y = self.graph.height/2 self.y = self.graph.height/2
@ -1518,10 +1643,13 @@ class DotWidget(gtk.DrawingArea):
rect = self.get_allocation() rect = self.get_allocation()
width = abs(x1 - x2) width = abs(x1 - x2)
height = abs(y1 - y2) height = abs(y1 - y2)
self.zoom_ratio = min( if width == 0 and height == 0:
float(rect.width)/float(width), self.zoom_ratio *= self.ZOOM_INCREMENT
float(rect.height)/float(height) else:
) self.zoom_ratio = min(
float(rect.width)/float(width),
float(rect.height)/float(height)
)
self.zoom_to_fit_on_resize = False self.zoom_to_fit_on_resize = False
self.x = (x1 + x2) / 2 self.x = (x1 + x2) / 2
self.y = (y1 + y2) / 2 self.y = (y1 + y2) / 2
@ -1574,11 +1702,16 @@ class DotWidget(gtk.DrawingArea):
self.y += self.POS_INCREMENT/self.zoom_ratio self.y += self.POS_INCREMENT/self.zoom_ratio
self.queue_draw() self.queue_draw()
return True return True
if event.keyval == gtk.keysyms.Page_Up: if event.keyval in (gtk.keysyms.Page_Up,
gtk.keysyms.plus,
gtk.keysyms.equal,
gtk.keysyms.KP_Add):
self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT) self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
self.queue_draw() self.queue_draw()
return True return True
if event.keyval == gtk.keysyms.Page_Down: if event.keyval in (gtk.keysyms.Page_Down,
gtk.keysyms.minus,
gtk.keysyms.KP_Subtract):
self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT) self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
self.queue_draw() self.queue_draw()
return True return True
@ -1589,11 +1722,49 @@ class DotWidget(gtk.DrawingArea):
if event.keyval == gtk.keysyms.r: if event.keyval == gtk.keysyms.r:
self.reload() self.reload()
return True return True
if event.keyval == gtk.keysyms.f:
win = widget.get_toplevel()
find_toolitem = win.uimanager.get_widget('/ToolBar/Find')
textentry = find_toolitem.get_children()
win.set_focus(textentry[0])
return True
if event.keyval == gtk.keysyms.q: if event.keyval == gtk.keysyms.q:
gtk.main_quit() gtk.main_quit()
return True return True
if event.keyval == gtk.keysyms.p:
self.on_print()
return True
return False return False
print_settings = None
def on_print(self, action=None):
print_op = gtk.PrintOperation()
if self.print_settings != None:
print_op.set_print_settings(self.print_settings)
print_op.connect("begin_print", self.begin_print)
print_op.connect("draw_page", self.draw_page)
res = print_op.run(gtk.PRINT_OPERATION_ACTION_PRINT_DIALOG, self.parent.parent)
if res == gtk.PRINT_OPERATION_RESULT_APPLY:
print_settings = print_op.get_print_settings()
def begin_print(self, operation, context):
operation.set_n_pages(1)
return True
def draw_page(self, operation, context, page_nr):
cr = context.get_cairo_context()
rect = self.get_allocation()
cr.translate(0.5*rect.width, 0.5*rect.height)
cr.scale(self.zoom_ratio, self.zoom_ratio)
cr.translate(-self.x, -self.y)
self.graph.draw(cr, highlight_items=self.highlight)
def get_drag_action(self, event): def get_drag_action(self, event):
state = event.state state = event.state
if event.button in (1, 2): # left or middle button if event.button in (1, 2): # left or middle button
@ -1628,20 +1799,32 @@ class DotWidget(gtk.DrawingArea):
return (time.time() < self.presstime + click_timeout return (time.time() < self.presstime + click_timeout
and math.hypot(deltax, deltay) < click_fuzz) and math.hypot(deltax, deltay) < click_fuzz)
def on_click(self, element, event):
"""Override this method in subclass to process
click events. Note that element can be None
(click on empty space)."""
return False
def on_area_button_release(self, area, event): def on_area_button_release(self, area, event):
self.drag_action.on_button_release(event) self.drag_action.on_button_release(event)
self.drag_action = NullAction(self) self.drag_action = NullAction(self)
if event.button == 1 and self.is_click(event): x, y = int(event.x), int(event.y)
x, y = int(event.x), int(event.y) if self.is_click(event):
url = self.get_url(x, y) el = self.get_element(x, y)
if url is not None: if self.on_click(el, event):
self.emit('clicked', unicode(url.url), event) return True
else:
jump = self.get_jump(x, y) if event.button == 1:
if jump is not None: url = self.get_url(x, y)
self.animate_to(jump.x, jump.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)
return True
return True
if event.button == 1 or event.button == 2: if event.button == 1 or event.button == 2:
return True return True
return False return False
@ -1679,6 +1862,10 @@ class DotWidget(gtk.DrawingArea):
y += self.y y += self.y
return x, y return x, y
def get_element(self, x, y):
x, y = self.window2graph(x, y)
return self.graph.get_element(x, y)
def get_url(self, x, y): def get_url(self, x, y):
x, y = self.window2graph(x, y) x, y = self.window2graph(x, y)
return self.graph.get_url(x, y) return self.graph.get_url(x, y)
@ -1688,6 +1875,14 @@ class DotWidget(gtk.DrawingArea):
return self.graph.get_jump(x, y) return self.graph.get_jump(x, y)
class FindMenuToolAction(gtk.Action):
__gtype_name__ = "FindMenuToolAction"
def __init__(self, *args, **kw):
gtk.Action.__init__(self, *args, **kw)
self.set_tool_item_type(gtk.ToolItem)
class DotWindow(gtk.Window): class DotWindow(gtk.Window):
ui = ''' ui = '''
@ -1695,28 +1890,33 @@ class DotWindow(gtk.Window):
<toolbar name="ToolBar"> <toolbar name="ToolBar">
<toolitem action="Open"/> <toolitem action="Open"/>
<toolitem action="Reload"/> <toolitem action="Reload"/>
<toolitem action="Print"/>
<separator/> <separator/>
<toolitem action="ZoomIn"/> <toolitem action="ZoomIn"/>
<toolitem action="ZoomOut"/> <toolitem action="ZoomOut"/>
<toolitem action="ZoomFit"/> <toolitem action="ZoomFit"/>
<toolitem action="Zoom100"/> <toolitem action="Zoom100"/>
<separator/>
<toolitem name="Find" action="Find"/>
</toolbar> </toolbar>
</ui> </ui>
''' '''
def __init__(self): base_title = 'Dot Viewer'
def __init__(self, widget=None):
gtk.Window.__init__(self) gtk.Window.__init__(self)
self.graph = Graph() self.graph = Graph()
window = self window = self
window.set_title('Dot Viewer') window.set_title(self.base_title)
window.set_default_size(512, 512) window.set_default_size(512, 512)
vbox = gtk.VBox() vbox = gtk.VBox()
window.add(vbox) window.add(vbox)
self.widget = DotWidget() self.widget = widget or DotWidget()
# Create a UIManager instance # Create a UIManager instance
uimanager = self.uimanager = gtk.UIManager() uimanager = self.uimanager = gtk.UIManager()
@ -1733,12 +1933,17 @@ class DotWindow(gtk.Window):
actiongroup.add_actions(( actiongroup.add_actions((
('Open', gtk.STOCK_OPEN, None, None, None, self.on_open), ('Open', gtk.STOCK_OPEN, None, None, None, self.on_open),
('Reload', gtk.STOCK_REFRESH, None, None, None, self.on_reload), ('Reload', gtk.STOCK_REFRESH, None, None, None, self.on_reload),
('Print', gtk.STOCK_PRINT, None, None, "Prints the currently visible part of the graph", self.widget.on_print),
('ZoomIn', gtk.STOCK_ZOOM_IN, None, None, None, self.widget.on_zoom_in), ('ZoomIn', gtk.STOCK_ZOOM_IN, None, None, None, self.widget.on_zoom_in),
('ZoomOut', gtk.STOCK_ZOOM_OUT, None, None, None, self.widget.on_zoom_out), ('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), ('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), ('Zoom100', gtk.STOCK_ZOOM_100, None, None, None, self.widget.on_zoom_100),
)) ))
find_action = FindMenuToolAction("Find", None,
"Find a node by name", None)
actiongroup.add_action(find_action)
# Add the actiongroup to the uimanager # Add the actiongroup to the uimanager
uimanager.insert_action_group(actiongroup, 0) uimanager.insert_action_group(actiongroup, 0)
@ -1751,45 +1956,82 @@ class DotWindow(gtk.Window):
vbox.pack_start(self.widget) vbox.pack_start(self.widget)
self.last_open_dir = "."
self.set_focus(self.widget) self.set_focus(self.widget)
# Add Find text search
find_toolitem = uimanager.get_widget('/ToolBar/Find')
self.textentry = gtk.Entry(max=20)
self.textentry.set_icon_from_stock(0, gtk.STOCK_FIND)
find_toolitem.add(self.textentry)
self.textentry.set_activates_default(True)
self.textentry.connect ("activate", self.textentry_activate, self.textentry);
self.textentry.connect ("changed", self.textentry_changed, self.textentry);
self.show_all() self.show_all()
def update(self, filename): def find_text(self, entry_text):
import os found_items = []
if not hasattr(self, "last_mtime"): dot_widget = self.widget
self.last_mtime = None regexp = re.compile(entry_text)
for node in dot_widget.graph.nodes:
if node.search_text(regexp):
found_items.append(node)
return found_items
current_mtime = os.stat(filename).st_mtime def textentry_changed(self, widget, entry):
if current_mtime != self.last_mtime: entry_text = entry.get_text()
self.last_mtime = current_mtime dot_widget = self.widget
self.open_file(filename) if not entry_text:
dot_widget.set_highlight(None)
return
return True found_items = self.find_text(entry_text)
dot_widget.set_highlight(found_items)
def textentry_activate(self, widget, entry):
entry_text = entry.get_text()
dot_widget = self.widget
if not entry_text:
dot_widget.set_highlight(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)
def set_filter(self, filter): def set_filter(self, filter):
self.widget.set_filter(filter) self.widget.set_filter(filter)
def set_dotcode(self, dotcode, filename='<stdin>'): def set_dotcode(self, dotcode, filename=None):
if self.widget.set_dotcode(dotcode, filename): if self.widget.set_dotcode(dotcode, filename):
self.set_title(os.path.basename(filename) + ' - Dot Viewer') self.update_title(filename)
self.widget.zoom_to_fit() self.widget.zoom_to_fit()
def set_xdotcode(self, xdotcode, filename='<stdin>'): def set_xdotcode(self, xdotcode, filename=None):
if self.widget.set_xdotcode(xdotcode): if self.widget.set_xdotcode(xdotcode):
self.set_title(os.path.basename(filename) + ' - Dot Viewer') self.update_title(filename)
self.widget.zoom_to_fit() self.widget.zoom_to_fit()
def update_title(self, filename=None):
if filename is None:
self.set_title(self.base_title)
else:
self.set_title(os.path.basename(filename) + ' - ' + self.base_title)
def open_file(self, filename): def open_file(self, filename):
try: try:
fp = file(filename, 'rt') fp = file(filename, 'rt')
self.set_dotcode(fp.read(), filename) self.set_dotcode(fp.read(), filename)
fp.close() fp.close()
except IOError, ex: except IOError as ex:
dlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, dlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
message_format=str(ex), message_format=str(ex),
buttons=gtk.BUTTONS_OK) buttons=gtk.BUTTONS_OK)
dlg.set_title('Dot Viewer') dlg.set_title(self.base_title)
dlg.run() dlg.run()
dlg.destroy() dlg.destroy()
@ -1801,6 +2043,7 @@ class DotWindow(gtk.Window):
gtk.STOCK_OPEN, gtk.STOCK_OPEN,
gtk.RESPONSE_OK)) gtk.RESPONSE_OK))
chooser.set_default_response(gtk.RESPONSE_OK) chooser.set_default_response(gtk.RESPONSE_OK)
chooser.set_current_folder(self.last_open_dir)
filter = gtk.FileFilter() filter = gtk.FileFilter()
filter.set_name("Graphviz dot files") filter.set_name("Graphviz dot files")
filter.add_pattern("*.dot") filter.add_pattern("*.dot")
@ -1811,6 +2054,7 @@ class DotWindow(gtk.Window):
chooser.add_filter(filter) chooser.add_filter(filter)
if chooser.run() == gtk.RESPONSE_OK: if chooser.run() == gtk.RESPONSE_OK:
filename = chooser.get_filename() filename = chooser.get_filename()
self.last_open_dir = chooser.get_current_folder()
chooser.destroy() chooser.destroy()
self.open_file(filename) self.open_file(filename)
else: else:
@ -1820,17 +2064,41 @@ class DotWindow(gtk.Window):
self.widget.reload() self.widget.reload()
def main(): class OptionParser(optparse.OptionParser):
import optparse
parser = optparse.OptionParser( def format_epilog(self, formatter):
# Prevent stripping the newlines in epilog message
# http://stackoverflow.com/questions/1857346/python-optparse-how-to-include-additional-info-in-usage-output
return self.epilog
def main():
parser = OptionParser(
usage='\n\t%prog [file]', usage='\n\t%prog [file]',
version='%%prog %s' % __version__) epilog='''
Shortcuts:
Up, Down, Left, Right scroll
PageUp, +, = zoom in
PageDown, - zoom out
R reload dot file
F find
Q quit
P print
Escape halt animation
Ctrl-drag zoom in/out
Shift-drag zooms an area
'''
)
parser.add_option( parser.add_option(
'-f', '--filter', '-f', '--filter',
type='choice', choices=('dot', 'neato', 'twopi', 'circo', 'fdp'), type='choice', choices=('dot', 'neato', 'twopi', 'circo', 'fdp'),
dest='filter', default='dot', dest='filter', default='dot',
help='graphviz filter: dot, neato, twopi, circo, or fdp [default: %default]') help='graphviz filter: dot, neato, twopi, circo, or fdp [default: %default]')
parser.add_option(
'-n', '--no-filter',
action='store_const', const=None, dest='filter',
help='assume input is already filtered into xdot format (use e.g. dot -Txdot)')
(options, args) = parser.parse_args(sys.argv[1:]) (options, args) = parser.parse_args(sys.argv[1:])
if len(args) > 1: if len(args) > 1:
@ -1839,12 +2107,14 @@ def main():
win = DotWindow() win = DotWindow()
win.connect('destroy', gtk.main_quit) win.connect('destroy', gtk.main_quit)
win.set_filter(options.filter) win.set_filter(options.filter)
if len(args) >= 1: if len(args) == 0:
if not sys.stdin.isatty():
win.set_dotcode(sys.stdin.read())
else:
if args[0] == '-': if args[0] == '-':
win.set_dotcode(sys.stdin.read()) win.set_dotcode(sys.stdin.read())
else: else:
win.open_file(args[0]) win.open_file(args[0])
gobject.timeout_add(1000, win.update, args[0])
gtk.main() gtk.main()