$search
00001 #!/usr/bin/env python 00002 # 00003 # wxpython widgets for using Jose Fonseca's cairo graphviz visualizer 00004 # Copyright (c) 2010, Willow Garage, Inc. 00005 # 00006 # Source modified from Jose Fonseca's XDot pgtk widgets. That code is 00007 # Copyright 2008 Jose Fonseca 00008 # 00009 # This program is free software: you can redistribute it and/or modify it 00010 # under the terms of the GNU Lesser General Public License as published 00011 # by the Free Software Foundation, either version 3 of the License, or 00012 # (at your option) any later version. 00013 # 00014 # This program is distributed in the hope that it will be useful, 00015 # but WITHOUT ANY WARRANTY; without even the implied warranty of 00016 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00017 # GNU Lesser General Public License for more details. 00018 # 00019 # You should have received a copy of the GNU Lesser General Public License 00020 # along with this program. If not, see <http://www.gnu.org/licenses/>. 00021 00022 from xdot import * 00023 00024 __all__ = ['WxDotWindow', 'WxDotFrame'] 00025 00026 # We need to get the wx version with built-in cairo support 00027 import wxversion 00028 wxversion.select("2.8") 00029 import wx 00030 import wx.lib.wxcairo as wxcairo 00031 00032 # This is a crazy hack to get this to work on 64-bit systems 00033 if 'wxMac' in wx.PlatformInfo: 00034 pass # Implement if necessary 00035 elif 'wxMSW' in wx.PlatformInfo: 00036 pass # Implement if necessary 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 # Bind events 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 # Callback register 00167 self.select_cbs = [] 00168 self.dc = None 00169 self.ctx = None 00170 self.items_by_url = {} 00171 00172 ### User callbacks 00173 def register_select_callback(self, cb): 00174 self.select_cbs.append(cb) 00175 00176 ### Event handlers 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 #print dc 00185 ctx = wxcairo.ContextFromDC(dc) 00186 ctx = pangocairo.CairoContext(ctx) 00187 #print "DRAW" 00188 00189 # Get widget size 00190 width, height = self.GetSize() 00191 #width,height = self.dc.GetSizeTuple() 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 ### Helper functions 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 ### Cursor manipulation 00268 def set_cursor(self, cursor_type): 00269 self.cursor = wx.StockCursor(cursor_type) 00270 self.SetCursor(self.cursor) 00271 00272 ### Zooming methods 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 # Grab the button 00339 button = event.GetButton() 00340 # Grab modifier keys 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 # Get the correct drag action for this click 00349 if button in (wx.MOUSE_BTN_LEFT, wx.MOUSE_BTN_MIDDLE): # left or middle button 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 # Get the item 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 # Get the drag action 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