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