00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022 from xdot import *
00023
00024 __all__ = ['WxDotWindow', 'WxDotFrame']
00025
00026
00027 import wxversion
00028 wxversion.select("2.8")
00029 import wx
00030 import wx.lib.wxcairo as wxcairo
00031
00032
00033 if 'wxMac' in wx.PlatformInfo:
00034 pass
00035 elif 'wxMSW' in wx.PlatformInfo:
00036 pass
00037 elif 'wxGTK' in wx.PlatformInfo:
00038 import ctypes
00039 gdkLib = wx.lib.wxcairo._findGDKLib()
00040 gdkLib.gdk_cairo_create.restype = ctypes.c_void_p
00041
00042 class WxDragAction(object):
00043 def __init__(self, dot_widget):
00044 self.dot_widget = dot_widget
00045
00046 def on_button_press(self, event):
00047 x,y = event.GetPositionTuple()
00048 self.startmousex = self.prevmousex = x
00049 self.startmousey = self.prevmousey = y
00050 self.start()
00051
00052 def on_motion_notify(self, event):
00053 x,y = event.GetPositionTuple()
00054 deltax = self.prevmousex - x
00055 deltay = self.prevmousey - y
00056 self.drag(deltax, deltay)
00057 self.prevmousex = x
00058 self.prevmousey = y
00059
00060 def on_button_release(self, event):
00061 x,y = event.GetPositionTuple()
00062 self.stopmousex = x
00063 self.stopmousey = y
00064 self.stop()
00065
00066 def draw(self, cr):
00067 pass
00068
00069 def start(self):
00070 pass
00071
00072 def drag(self, deltax, deltay):
00073 pass
00074
00075 def stop(self):
00076 pass
00077
00078 def abort(self):
00079 pass
00080
00081 class WxNullAction(WxDragAction):
00082 def on_motion_notify(self, event):
00083 pass
00084
00085 class WxPanAction(WxDragAction):
00086 def start(self):
00087 self.dot_widget.set_cursor(wx.CURSOR_SIZING)
00088
00089 def drag(self, deltax, deltay):
00090 self.dot_widget.x += deltax / self.dot_widget.zoom_ratio
00091 self.dot_widget.y += deltay / self.dot_widget.zoom_ratio
00092 self.dot_widget.Refresh()
00093
00094 def stop(self):
00095 self.dot_widget.set_cursor(wx.CURSOR_ARROW)
00096
00097 abort = stop
00098
00099 class WxZoomAction(WxDragAction):
00100 def drag(self, deltax, deltay):
00101 self.dot_widget.zoom_ratio *= 1.005 ** (deltax + deltay)
00102 self.dot_widget.zoom_to_fit_on_resize = False
00103 self.dot_widget.Refresh()
00104
00105 def stop(self):
00106 self.dot_widget.Refresh()
00107
00108 class WxZoomAreaAction(WxDragAction):
00109 def drag(self, deltax, deltay):
00110 self.dot_widget.Refresh()
00111
00112 def draw(self, cr):
00113 cr.save()
00114 cr.set_source_rgba(.5, .5, 1.0, 0.25)
00115 cr.rectangle(self.startmousex, self.startmousey,
00116 self.prevmousex - self.startmousex,
00117 self.prevmousey - self.startmousey)
00118 cr.fill()
00119 cr.set_source_rgba(.5, .5, 1.0, 1.0)
00120 cr.set_line_width(1)
00121 cr.rectangle(self.startmousex - .5, self.startmousey - .5,
00122 self.prevmousex - self.startmousex + 1,
00123 self.prevmousey - self.startmousey + 1)
00124 cr.stroke()
00125 cr.restore()
00126
00127 def stop(self):
00128 x1, y1 = self.dot_widget.window2graph(self.startmousex,
00129 self.startmousey)
00130 x2, y2 = self.dot_widget.window2graph(self.stopmousex,
00131 self.stopmousey)
00132 self.dot_widget.zoom_to_area(x1, y1, x2, y2)
00133
00134 def abort(self):
00135 self.dot_widget.Refresh()
00136
00137 class WxDotWindow(wx.Panel):
00138 """wxpython Frame that draws dot graphs."""
00139 filter = 'dot'
00140
00141 def __init__(self, parent, id):
00142 """constructor"""
00143 wx.Panel.__init__(self, parent, id)
00144
00145 self.graph = Graph()
00146 self.openfilename = None
00147
00148 self.x, self.y = 0.0, 0.0
00149 self.zoom_ratio = 1.0
00150 self.zoom_to_fit_on_resize = False
00151 self.animation = NoAnimation(self)
00152 self.drag_action = WxNullAction(self)
00153 self.presstime = None
00154 self.highlight = None
00155
00156
00157 self.Bind(wx.EVT_PAINT, self.OnPaint)
00158 self.Bind(wx.EVT_SIZE, self.OnResize)
00159
00160 self.Bind(wx.EVT_MOUSEWHEEL, self.OnScroll)
00161
00162 self.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse)
00163
00164 self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
00165
00166
00167 self.select_cbs = []
00168 self.dc = None
00169 self.ctx = None
00170 self.items_by_url = {}
00171
00172
00173 def register_select_callback(self, cb):
00174 self.select_cbs.append(cb)
00175
00176
00177 def OnResize(self, event):
00178 self.Refresh()
00179
00180 def OnPaint(self, event):
00181 """Redraw the graph."""
00182 dc = wx.PaintDC(self)
00183
00184
00185 ctx = wxcairo.ContextFromDC(dc)
00186 ctx = pangocairo.CairoContext(ctx)
00187
00188
00189
00190 width, height = self.GetSize()
00191
00192
00193 ctx.rectangle(0,0,width,height)
00194 ctx.clip()
00195
00196 ctx.set_source_rgba(1.0, 1.0, 1.0, 1.0)
00197 ctx.paint()
00198
00199 ctx.save()
00200 ctx.translate(0.5*width, 0.5*height)
00201
00202 ctx.scale(self.zoom_ratio, self.zoom_ratio)
00203 ctx.translate(-self.x, -self.y)
00204 self.graph.draw(ctx, highlight_items=self.highlight)
00205 ctx.restore()
00206
00207 self.drag_action.draw(ctx)
00208
00209 def OnScroll(self, event):
00210 """Zoom the view."""
00211 if event.GetWheelRotation() > 0:
00212 self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT,
00213 pos=(event.GetX(), event.GetY()))
00214 else:
00215 self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT,
00216 pos=(event.GetX(), event.GetY()))
00217
00218 def OnKeyDown(self, event):
00219 """Process key down event."""
00220 key = event.GetKeyCode()
00221 if key == wx.WXK_LEFT:
00222 self.x -= self.POS_INCREMENT/self.zoom_ratio
00223 self.Refresh()
00224 if key == wx.WXK_RIGHT:
00225 self.x += self.POS_INCREMENT/self.zoom_ratio
00226 self.Refresh()
00227 if key == wx.WXK_UP:
00228 self.y -= self.POS_INCREMENT/self.zoom_ratio
00229 self.Refresh()
00230 if key == wx.WXK_DOWN:
00231 self.y += self.POS_INCREMENT/self.zoom_ratio
00232 self.Refresh()
00233 if key == wx.WXK_PAGEUP:
00234 self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
00235 self.Refresh()
00236 if key == wx.WXK_PAGEDOWN:
00237 self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
00238 self.Refresh()
00239 if key == wx.WXK_ESCAPE:
00240 self.drag_action.abort()
00241 self.drag_action = WxNullAction(self)
00242 if key == ord('F'):
00243 self.zoom_to_fit()
00244 if key == ord('R'):
00245 self.reload()
00246 if key == ord('Q'):
00247 self.reload()
00248 exit(0)
00249
00250
00251 def get_current_pos(self):
00252 """Get the current graph position."""
00253 return self.x, self.y
00254
00255 def set_current_pos(self, x, y):
00256 """Set the current graph position."""
00257 self.x = x
00258 self.y = y
00259 self.Refresh()
00260
00261 def set_highlight(self, items):
00262 """Set a number of items to be hilighted."""
00263 if self.highlight != items:
00264 self.highlight = items
00265 self.Refresh()
00266
00267
00268 def set_cursor(self, cursor_type):
00269 self.cursor = wx.StockCursor(cursor_type)
00270 self.SetCursor(self.cursor)
00271
00272
00273 def zoom_image(self, zoom_ratio, center=False, pos=None):
00274 """Zoom the graph."""
00275 if center:
00276 self.x = self.graph.width/2
00277 self.y = self.graph.height/2
00278 elif pos is not None:
00279 width, height = self.GetSize()
00280 x, y = pos
00281 x -= 0.5*width
00282 y -= 0.5*height
00283 self.x += x / self.zoom_ratio - x / zoom_ratio
00284 self.y += y / self.zoom_ratio - y / zoom_ratio
00285 self.zoom_ratio = zoom_ratio
00286 self.zoom_to_fit_on_resize = False
00287 self.Refresh()
00288
00289 def zoom_to_area(self, x1, y1, x2, y2):
00290 """Zoom to an area of the graph."""
00291 width, height = self.GetSize()
00292 area_width = abs(x1 - x2)
00293 area_height = abs(y1 - y2)
00294 self.zoom_ratio = min(
00295 float(width)/float(area_width),
00296 float(height)/float(area_height)
00297 )
00298 self.zoom_to_fit_on_resize = False
00299 self.x = (x1 + x2) / 2
00300 self.y = (y1 + y2) / 2
00301 self.Refresh()
00302
00303 def zoom_to_fit(self):
00304 """Zoom to fit the size of the graph."""
00305 width,height = self.GetSize()
00306 x = self.ZOOM_TO_FIT_MARGIN
00307 y = self.ZOOM_TO_FIT_MARGIN
00308 width -= 2 * self.ZOOM_TO_FIT_MARGIN
00309 height -= 2 * self.ZOOM_TO_FIT_MARGIN
00310
00311 if float(self.graph.width) > 0 and float(self.graph.height) > 0 and width > 0 and height > 0:
00312 zoom_ratio = min(
00313 float(width)/float(self.graph.width),
00314 float(height)/float(self.graph.height)
00315 )
00316 self.zoom_image(zoom_ratio, center=True)
00317 self.zoom_to_fit_on_resize = True
00318
00319 ZOOM_INCREMENT = 1.25
00320 ZOOM_TO_FIT_MARGIN = 12
00321
00322 def on_zoom_in(self, action):
00323 self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
00324
00325 def on_zoom_out(self, action):
00326 self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
00327
00328 def on_zoom_fit(self, action):
00329 self.zoom_to_fit()
00330
00331 def on_zoom_100(self, action):
00332 self.zoom_image(1.0)
00333
00334 POS_INCREMENT = 100
00335
00336 def get_drag_action(self, event):
00337 """Get a drag action for this click."""
00338
00339 button = event.GetButton()
00340
00341 control_down = event.ControlDown()
00342 alt_down = event.AltDown()
00343 shift_down = event.ShiftDown()
00344
00345 drag = event.Dragging()
00346 motion = event.Moving()
00347
00348
00349 if button in (wx.MOUSE_BTN_LEFT, wx.MOUSE_BTN_MIDDLE):
00350 if control_down:
00351 if shift_down:
00352 return WxZoomAreaAction(self)
00353 else:
00354 return WxZoomAction(self)
00355 else:
00356 return WxPanAction(self)
00357
00358 return WxNullAction(self)
00359
00360 def OnMouse(self, event):
00361 x,y = event.GetPositionTuple()
00362
00363 item = None
00364
00365
00366 if not event.Dragging():
00367 item = self.get_url(x, y)
00368 if item is None:
00369 item = self.get_jump(x, y)
00370
00371 if item is not None:
00372 self.set_cursor(wx.CURSOR_HAND)
00373 self.set_highlight(item.highlight)
00374
00375 for cb in self.select_cbs:
00376 cb(item,event)
00377 else:
00378 self.set_cursor(wx.CURSOR_ARROW)
00379 self.set_highlight(None)
00380
00381 if item is None:
00382 if event.ButtonDown():
00383 self.animation.stop()
00384 self.drag_action.abort()
00385
00386
00387 self.drag_action = self.get_drag_action(event)
00388 self.drag_action.on_button_press(event)
00389
00390 self.pressx = x
00391 self.pressy = y
00392
00393 if event.Dragging() or event.Moving():
00394 self.drag_action.on_motion_notify(event)
00395
00396 if event.ButtonUp():
00397 self.drag_action.on_button_release(event)
00398 self.drag_action = WxNullAction(self)
00399
00400 event.Skip()
00401
00402
00403 def on_area_size_allocate(self, area, allocation):
00404 if self.zoom_to_fit_on_resize:
00405 self.zoom_to_fit()
00406
00407 def animate_to(self, x, y):
00408 self.animation = ZoomToAnimation(self, x, y)
00409 self.animation.start()
00410
00411 def window2graph(self, x, y):
00412 "Get the x,y coordinates in the graph from the x,y coordinates in the window."""
00413 width, height = self.GetSize()
00414 x -= 0.5*width
00415 y -= 0.5*height
00416 x /= self.zoom_ratio
00417 y /= self.zoom_ratio
00418 x += self.x
00419 y += self.y
00420 return x, y
00421
00422 def get_url(self, x, y):
00423 x, y = self.window2graph(x, y)
00424 return self.graph.get_url(x, y)
00425
00426 def get_jump(self, x, y):
00427 x, y = self.window2graph(x, y)
00428 return self.graph.get_jump(x, y)
00429
00430 def set_filter(self, filter):
00431 self.filter = filter
00432
00433 def set_dotcode(self, dotcode, filename='<stdin>'):
00434 if isinstance(dotcode, unicode):
00435 dotcode = dotcode.encode('utf8')
00436 p = subprocess.Popen(
00437 [self.filter, '-Txdot'],
00438 stdin=subprocess.PIPE,
00439 stdout=subprocess.PIPE,
00440 stderr=subprocess.PIPE,
00441 shell=False,
00442 universal_newlines=True
00443 )
00444 xdotcode, error = p.communicate(dotcode)
00445 if p.returncode != 0:
00446 print "ERROR PARSING DOT CODE", error
00447 dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
00448 message_format=error,
00449 buttons=gtk.BUTTONS_OK)
00450 dialog.set_title('Dot Viewer')
00451 dialog.run()
00452 dialog.destroy()
00453 return False
00454 try:
00455 self.set_xdotcode(xdotcode)
00456
00457 # Store references to all the items
00458 self.items_by_url = {}
00459 for item in self.graph.nodes + self.graph.edges:
00460 if item.url is not None:
00461 self.items_by_url[item.url] = item
00462
00463 # Store references to subgraph states
00464 self.subgraph_shapes = self.graph.subgraph_shapes
00465
00466 except ParseError, ex:
00467 print "ERROR PARSING XDOT CODE"
00468 dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
00469 message_format=str(ex),
00470 buttons=gtk.BUTTONS_OK)
00471 dialog.set_title('Dot Viewer')
00472 dialog.run()
00473 dialog.destroy()
00474 return False
00475 else:
00476 self.openfilename = filename
00477 return True
00478
00479 def set_xdotcode(self, xdotcode):
00480 """Set xdot code."""
00481 #print xdotcode
00482 parser = XDotParser(xdotcode)
00483 self.graph = parser.parse()
00484 self.highlight = None
00485 #self.zoom_image(self.zoom_ratio, center=True)
00486
00487 def reload(self):
00488 if self.openfilename is not None:
00489 try:
00490 fp = file(self.openfilename, 'rt')
00491 self.set_dotcode(fp.read(), self.openfilename)
00492 fp.close()
00493 except IOError:
00494 pass
00495
00496
00497 class WxDotFrame(wx.Frame):
00498 def __init__(self):
00499 wx.Frame.__init__(self, None, -1, "Dot Viewer", size=(512,512))
00500
00501 vbox = wx.BoxSizer(wx.VERTICAL)
00502
00503 # Construct toolbar
00504 toolbar = wx.ToolBar(self, -1)
00505 toolbar.AddLabelTool(wx.ID_OPEN, 'Open File',
00506 wx.ArtProvider.GetBitmap(wx.ART_FOLDER_OPEN,wx.ART_OTHER,(16,16)))
00507 toolbar.AddLabelTool(wx.ID_HELP, 'Help',
00508 wx.ArtProvider.GetBitmap(wx.ART_HELP,wx.ART_OTHER,(16,16)) )
00509 toolbar.Realize()
00510
00511 self.Bind(wx.EVT_TOOL, self.DoOpenFile, id=wx.ID_OPEN)
00512 self.Bind(wx.EVT_TOOL, self.ShowControlsDialog, id=wx.ID_HELP)
00513
00514 # Create dot widge
00515 self.widget = WxDotWindow(self, -1)
00516
00517 # Add elements to sizer
00518 vbox.Add(toolbar, 0, wx.EXPAND)
00519 vbox.Add(self.widget, 100, wx.EXPAND | wx.ALL)
00520
00521 self.SetSizer(vbox)
00522 self.Center()
00523
00524 def ShowControlsDialog(self,event):
00525 dial = wx.MessageDialog(None,
00526 "\
00527 Pan: Arrow Keys\n\
00528 Zoom: PageUp / PageDown\n\
00529 Zoom To Fit: F\n\
00530 Refresh: R",
00531 'Keyboard Controls', wx.OK)
00532 dial.ShowModal()
00533
00534 def DoOpenFile(self,event):
00535 wcd = 'All files (*)|*|GraphViz Dot Files(*.dot)|*.dot|'
00536 dir = os.getcwd()
00537 open_dlg = wx.FileDialog(self, message='Choose a file', defaultDir=dir, defaultFile='',
00538 wildcard=wcd, style=wx.OPEN|wx.CHANGE_DIR)
00539 if open_dlg.ShowModal() == wx.ID_OK:
00540 path = open_dlg.GetPath()
00541
00542 try:
00543 self.open_file(path)
00544
00545 except IOError, error:
00546 dlg = wx.MessageDialog(self, 'Error opening file\n' + str(error))
00547 dlg.ShowModal()
00548
00549 except UnicodeDecodeError, error:
00550 dlg = wx.MessageDialog(self, 'Error opening file\n' + str(error))
00551 dlg.ShowModal()
00552
00553 open_dlg.Destroy()
00554
00555 def OnExit(self, event):
00556 pass
00557
00558 def set_dotcode(self, dotcode, filename='<stdin>'):
00559 if self.widget.set_dotcode(dotcode, filename):
00560 self.SetTitle(os.path.basename(filename) + ' - Dot Viewer')
00561 self.widget.zoom_to_fit()
00562
00563 def set_xdotcode(self, xdotcode, filename='<stdin>'):
00564 if self.widget.set_xdotcode(xdotcode):
00565 self.SetTitle(os.path.basename(filename) + ' - Dot Viewer')
00566 self.widget.zoom_to_fit()
00567
00568 def open_file(self, filename):
00569 try:
00570 fp = file(filename, 'rt')
00571 self.set_dotcode(fp.read(), filename)
00572 fp.close()
00573 except IOError, ex:
00574 """
00575 dlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
00576 message_format=str(ex),
00577 buttons=gtk.BUTTONS_OK)
00578 dlg.set_title('Dot Viewer')
00579 dlg.run()
00580 dlg.destroy()
00581 """
00582
00583 def set_filter(self, filter):
00584 self.widget.set_filter(filter)
00585