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 
00025 __all__ = ['WxDotWindow', 'WxDotFrame']
00026 
00027 # We need to get the wx version with built-in cairo support
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     # workaround for ws.App segmentation fault (http://trac.wxwidgets.org/ticket/15898)
00034     gtk.remove_log_handlers()
00035 import wx
00036 import wx.lib.wxcairo as wxcairo
00037 
00038 # This is a crazy hack to get this to work on 64-bit systems
00039 if 'wxMac' in wx.PlatformInfo:
00040   pass # Implement if necessary
00041 elif 'wxMSW' in wx.PlatformInfo:
00042   pass # Implement if necessary
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     # Bind events
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     # Callback register
00173     self.select_cbs = []
00174     self.dc = None
00175     self.ctx = None
00176     self.items_by_url = {}
00177 
00178   ### User callbacks
00179   def register_select_callback(self, cb):
00180     self.select_cbs.append(cb)
00181 
00182   ### Event handlers
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     #print dc
00191     ctx = wxcairo.ContextFromDC(dc)
00192     ctx = pangocairo.CairoContext(ctx)
00193     #print "DRAW"
00194 
00195     # Get widget size
00196     width, height = self.GetSize()
00197     #width,height = self.dc.GetSizeTuple()
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   ### Helper functions
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   ### Cursor manipulation
00274   def set_cursor(self, cursor_type):
00275     self.cursor = wx.StockCursor(cursor_type)
00276     self.SetCursor(self.cursor)
00277 
00278   ### Zooming methods
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     # Grab the button
00345     button = event.GetButton()
00346     # Grab modifier keys
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     # Get the correct drag action for this click
00355     if button in (wx.MOUSE_BTN_LEFT, wx.MOUSE_BTN_MIDDLE): # left or middle button
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     # Get the item
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         # Get the drag action
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 


smach_viewer
Author(s): Jonathan Bohren
autogenerated on Thu Jun 6 2019 17:28:28