wxxdot.py
Go to the documentation of this file.
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 


xdot
Author(s): Jose Fonseca, ROS package and WX frontend by Jonathan Bohren
autogenerated on Mon Oct 6 2014 07:25:57