wxxdot.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 #
3 # wxpython widgets for using Jose Fonseca's cairo graphviz visualizer
4 # Copyright (c) 2010, Willow Garage, Inc.
5 #
6 # Source modified from Jose Fonseca's XDot pgtk widgets. That code is
7 # Copyright 2008 Jose Fonseca
8 #
9 # This program is free software: you can redistribute it and/or modify it
10 # under the terms of the GNU Lesser General Public License as published
11 # by the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU Lesser General Public License for more details.
18 #
19 # You should have received a copy of the GNU Lesser General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 
22 from xdot import *
23 
24 
25 __all__ = ['WxDotWindow', 'WxDotFrame']
26 
27 # We need to get the wx version with built-in cairo support
28 import wxversion
29 if wxversion.checkInstalled("2.8"):
30  wxversion.select("2.8")
31 else:
32  print("wxversion 2.8 is not installed, installed versions are {}".format(wxversion.getInstalled()))
33  # workaround for ws.App segmentation fault (http://trac.wxwidgets.org/ticket/15898)
34  gtk.remove_log_handlers()
35 import wx
36 import wx.lib.wxcairo as wxcairo
37 
38 # This is a crazy hack to get this to work on 64-bit systems
39 if 'wxMac' in wx.PlatformInfo:
40  pass # Implement if necessary
41 elif 'wxMSW' in wx.PlatformInfo:
42  pass # Implement if necessary
43 elif 'wxGTK' in wx.PlatformInfo:
44  import ctypes
45  gdkLib = wx.lib.wxcairo._findGDKLib()
46  gdkLib.gdk_cairo_create.restype = ctypes.c_void_p
47 
48 class WxDragAction(object):
49  def __init__(self, dot_widget):
50  self.dot_widget = dot_widget
51 
52  def on_button_press(self, event):
53  x,y = event.GetPositionTuple()
54  self.startmousex = self.prevmousex = x
55  self.startmousey = self.prevmousey = y
56  self.start()
57 
58  def on_motion_notify(self, event):
59  x,y = event.GetPositionTuple()
60  deltax = self.prevmousex - x
61  deltay = self.prevmousey - y
62  self.drag(deltax, deltay)
63  self.prevmousex = x
64  self.prevmousey = y
65 
66  def on_button_release(self, event):
67  x,y = event.GetPositionTuple()
68  self.stopmousex = x
69  self.stopmousey = y
70  self.stop()
71 
72  def draw(self, cr):
73  pass
74 
75  def start(self):
76  pass
77 
78  def drag(self, deltax, deltay):
79  pass
80 
81  def stop(self):
82  pass
83 
84  def abort(self):
85  pass
86 
87 class WxNullAction(WxDragAction):
88  def on_motion_notify(self, event):
89  pass
90 
91 class WxPanAction(WxDragAction):
92  def start(self):
93  self.dot_widget.set_cursor(wx.CURSOR_SIZING)
94 
95  def drag(self, deltax, deltay):
96  self.dot_widget.x += deltax / self.dot_widget.zoom_ratio
97  self.dot_widget.y += deltay / self.dot_widget.zoom_ratio
98  self.dot_widget.Refresh()
99 
100  def stop(self):
101  self.dot_widget.set_cursor(wx.CURSOR_ARROW)
102 
103  abort = stop
104 
106  def drag(self, deltax, deltay):
107  self.dot_widget.zoom_ratio *= 1.005 ** (deltax + deltay)
108  self.dot_widget.zoom_to_fit_on_resize = False
109  self.dot_widget.Refresh()
110 
111  def stop(self):
112  self.dot_widget.Refresh()
113 
115  def drag(self, deltax, deltay):
116  self.dot_widget.Refresh()
117 
118  def draw(self, cr):
119  cr.save()
120  cr.set_source_rgba(.5, .5, 1.0, 0.25)
121  cr.rectangle(self.startmousex, self.startmousey,
122  self.prevmousex - self.startmousex,
123  self.prevmousey - self.startmousey)
124  cr.fill()
125  cr.set_source_rgba(.5, .5, 1.0, 1.0)
126  cr.set_line_width(1)
127  cr.rectangle(self.startmousex - .5, self.startmousey - .5,
128  self.prevmousex - self.startmousex + 1,
129  self.prevmousey - self.startmousey + 1)
130  cr.stroke()
131  cr.restore()
132 
133  def stop(self):
134  x1, y1 = self.dot_widget.window2graph(self.startmousex,
135  self.startmousey)
136  x2, y2 = self.dot_widget.window2graph(self.stopmousex,
137  self.stopmousey)
138  self.dot_widget.zoom_to_area(x1, y1, x2, y2)
139 
140  def abort(self):
141  self.dot_widget.Refresh()
142 
143 class WxDotWindow(wx.Panel):
144  """wxpython Frame that draws dot graphs."""
145  filter = 'dot'
146 
147  def __init__(self, parent, id):
148  """constructor"""
149  wx.Panel.__init__(self, parent, id)
150 
151  self.graph = Graph()
152  self.openfilename = None
153 
154  self.x, self.y = 0.0, 0.0
155  self.zoom_ratio = 1.0
157  self.animation = NoAnimation(self)
159  self.presstime = None
160  self.highlight = None
161 
162  # Bind events
163  self.Bind(wx.EVT_PAINT, self.OnPaint)
164  self.Bind(wx.EVT_SIZE, self.OnResize)
165 
166  self.Bind(wx.EVT_MOUSEWHEEL, self.OnScroll)
167 
168  self.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse)
169 
170  self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
171 
172  # Callback register
173  self.select_cbs = []
174  self.dc = None
175  self.ctx = None
176  self.items_by_url = {}
177 
178  ### User callbacks
180  self.select_cbs.append(cb)
181 
182  ### Event handlers
183  def OnResize(self, event):
184  self.Refresh()
185 
186  def OnPaint(self, event):
187  """Redraw the graph."""
188  dc = wx.PaintDC(self)
189 
190  #print dc
191  ctx = wxcairo.ContextFromDC(dc)
192  ctx = pangocairo.CairoContext(ctx)
193  #print "DRAW"
194 
195  # Get widget size
196  width, height = self.GetSize()
197  #width,height = self.dc.GetSizeTuple()
198 
199  ctx.rectangle(0,0,width,height)
200  ctx.clip()
201 
202  ctx.set_source_rgba(1.0, 1.0, 1.0, 1.0)
203  ctx.paint()
204 
205  ctx.save()
206  ctx.translate(0.5*width, 0.5*height)
207 
208  ctx.scale(self.zoom_ratio, self.zoom_ratio)
209  ctx.translate(-self.x, -self.y)
210  self.graph.draw(ctx, highlight_items=self.highlight)
211  ctx.restore()
212 
213  self.drag_action.draw(ctx)
214 
215  def OnScroll(self, event):
216  """Zoom the view."""
217  if event.GetWheelRotation() > 0:
218  self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT,
219  pos=(event.GetX(), event.GetY()))
220  else:
221  self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT,
222  pos=(event.GetX(), event.GetY()))
223 
224  def OnKeyDown(self, event):
225  """Process key down event."""
226  key = event.GetKeyCode()
227  if key == wx.WXK_LEFT:
228  self.x -= self.POS_INCREMENT/self.zoom_ratio
229  self.Refresh()
230  if key == wx.WXK_RIGHT:
231  self.x += self.POS_INCREMENT/self.zoom_ratio
232  self.Refresh()
233  if key == wx.WXK_UP:
234  self.y -= self.POS_INCREMENT/self.zoom_ratio
235  self.Refresh()
236  if key == wx.WXK_DOWN:
237  self.y += self.POS_INCREMENT/self.zoom_ratio
238  self.Refresh()
239  if key == wx.WXK_PAGEUP:
240  self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
241  self.Refresh()
242  if key == wx.WXK_PAGEDOWN:
243  self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
244  self.Refresh()
245  if key == wx.WXK_ESCAPE:
246  self.drag_action.abort()
247  self.drag_action = WxNullAction(self)
248  if key == ord('F'):
249  self.zoom_to_fit()
250  if key == ord('R'):
251  self.reload()
252  if key == ord('Q'):
253  self.reload()
254  exit(0)
255 
256  ### Helper functions
257  def get_current_pos(self):
258  """Get the current graph position."""
259  return self.x, self.y
260 
261  def set_current_pos(self, x, y):
262  """Set the current graph position."""
263  self.x = x
264  self.y = y
265  self.Refresh()
266 
267  def set_highlight(self, items):
268  """Set a number of items to be hilighted."""
269  if self.highlight != items:
270  self.highlight = items
271  self.Refresh()
272 
273  ### Cursor manipulation
274  def set_cursor(self, cursor_type):
275  self.cursor = wx.StockCursor(cursor_type)
276  self.SetCursor(self.cursor)
277 
278  ### Zooming methods
279  def zoom_image(self, zoom_ratio, center=False, pos=None):
280  """Zoom the graph."""
281  if center:
282  self.x = self.graph.width/2
283  self.y = self.graph.height/2
284  elif pos is not None:
285  width, height = self.GetSize()
286  x, y = pos
287  x -= 0.5*width
288  y -= 0.5*height
289  self.x += x / self.zoom_ratio - x / zoom_ratio
290  self.y += y / self.zoom_ratio - y / zoom_ratio
291  self.zoom_ratio = zoom_ratio
292  self.zoom_to_fit_on_resize = False
293  self.Refresh()
294 
295  def zoom_to_area(self, x1, y1, x2, y2):
296  """Zoom to an area of the graph."""
297  width, height = self.GetSize()
298  area_width = abs(x1 - x2)
299  area_height = abs(y1 - y2)
300  self.zoom_ratio = min(
301  float(width)/float(area_width),
302  float(height)/float(area_height)
303  )
304  self.zoom_to_fit_on_resize = False
305  self.x = (x1 + x2) / 2
306  self.y = (y1 + y2) / 2
307  self.Refresh()
308 
309  def zoom_to_fit(self):
310  """Zoom to fit the size of the graph."""
311  width,height = self.GetSize()
312  x = self.ZOOM_TO_FIT_MARGIN
313  y = self.ZOOM_TO_FIT_MARGIN
314  width -= 2 * self.ZOOM_TO_FIT_MARGIN
315  height -= 2 * self.ZOOM_TO_FIT_MARGIN
316 
317  if float(self.graph.width) > 0 and float(self.graph.height) > 0 and width > 0 and height > 0:
318  zoom_ratio = min(
319  float(width)/float(self.graph.width),
320  float(height)/float(self.graph.height)
321  )
322  self.zoom_image(zoom_ratio, center=True)
323  self.zoom_to_fit_on_resize = True
324 
325  ZOOM_INCREMENT = 1.25
326  ZOOM_TO_FIT_MARGIN = 12
327 
328  def on_zoom_in(self, action):
329  self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
330 
331  def on_zoom_out(self, action):
332  self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
333 
334  def on_zoom_fit(self, action):
335  self.zoom_to_fit()
336 
337  def on_zoom_100(self, action):
338  self.zoom_image(1.0)
339 
340  POS_INCREMENT = 100
341 
342  def get_drag_action(self, event):
343  """Get a drag action for this click."""
344  # Grab the button
345  button = event.GetButton()
346  # Grab modifier keys
347  control_down = event.ControlDown()
348  alt_down = event.AltDown()
349  shift_down = event.ShiftDown()
350 
351  drag = event.Dragging()
352  motion = event.Moving()
353 
354  # Get the correct drag action for this click
355  if button in (wx.MOUSE_BTN_LEFT, wx.MOUSE_BTN_MIDDLE): # left or middle button
356  if control_down:
357  if shift_down:
358  return WxZoomAreaAction(self)
359  else:
360  return WxZoomAction(self)
361  else:
362  return WxPanAction(self)
363 
364  return WxNullAction(self)
365 
366  def OnMouse(self, event):
367  x,y = event.GetPositionTuple()
368 
369  item = None
370 
371  # Get the item
372  if not event.Dragging():
373  item = self.get_url(x, y)
374  if item is None:
375  item = self.get_jump(x, y)
376 
377  if item is not None:
378  self.set_cursor(wx.CURSOR_HAND)
379  self.set_highlight(item.highlight)
380 
381  for cb in self.select_cbs:
382  cb(item,event)
383  else:
384  self.set_cursor(wx.CURSOR_ARROW)
385  self.set_highlight(None)
386 
387  if item is None:
388  if event.ButtonDown():
389  self.animation.stop()
390  self.drag_action.abort()
391 
392  # Get the drag action
393  self.drag_action = self.get_drag_action(event)
394  self.drag_action.on_button_press(event)
395 
396  self.pressx = x
397  self.pressy = y
398 
399  if event.Dragging() or event.Moving():
400  self.drag_action.on_motion_notify(event)
401 
402  if event.ButtonUp():
403  self.drag_action.on_button_release(event)
404  self.drag_action = WxNullAction(self)
405 
406  event.Skip()
407 
408 
409  def on_area_size_allocate(self, area, allocation):
410  if self.zoom_to_fit_on_resize:
411  self.zoom_to_fit()
412 
413  def animate_to(self, x, y):
414  self.animation = ZoomToAnimation(self, x, y)
415  self.animation.start()
416 
417  def window2graph(self, x, y):
418  "Get the x,y coordinates in the graph from the x,y coordinates in the window."""
419  width, height = self.GetSize()
420  x -= 0.5*width
421  y -= 0.5*height
422  x /= self.zoom_ratio
423  y /= self.zoom_ratio
424  x += self.x
425  y += self.y
426  return x, y
427 
428  def get_url(self, x, y):
429  x, y = self.window2graph(x, y)
430  return self.graph.get_url(x, y)
431 
432  def get_jump(self, x, y):
433  x, y = self.window2graph(x, y)
434  return self.graph.get_jump(x, y)
435 
436  def set_filter(self, filter):
437  self.filter = filter
438 
439  def set_dotcode(self, dotcode, filename='<stdin>'):
440  if isinstance(dotcode, unicode):
441  dotcode = dotcode.encode('utf8')
442  p = subprocess.Popen(
443  [self.filter, '-Txdot'],
444  stdin=subprocess.PIPE,
445  stdout=subprocess.PIPE,
446  stderr=subprocess.PIPE,
447  shell=False,
448  universal_newlines=True
449  )
450  xdotcode, error = p.communicate(dotcode)
451  if p.returncode != 0:
452  print "ERROR PARSING DOT CODE", error
453  dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
454  message_format=error,
455  buttons=gtk.BUTTONS_OK)
456  dialog.set_title('Dot Viewer')
457  dialog.run()
458  dialog.destroy()
459  return False
460  try:
461  self.set_xdotcode(xdotcode)
462 
463  # Store references to all the items
464  self.items_by_url = {}
465  for item in self.graph.nodes + self.graph.edges:
466  if item.url is not None:
467  self.items_by_url[item.url] = item
468 
469  # Store references to subgraph states
470  self.subgraph_shapes = self.graph.subgraph_shapes
471 
472  except ParseError, ex:
473  print "ERROR PARSING XDOT CODE"
474  dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
475  message_format=str(ex),
476  buttons=gtk.BUTTONS_OK)
477  dialog.set_title('Dot Viewer')
478  dialog.run()
479  dialog.destroy()
480  return False
481  else:
482  self.openfilename = filename
483  return True
484 
485  def set_xdotcode(self, xdotcode):
486  """Set xdot code."""
487  #print xdotcode
488  parser = XDotParser(xdotcode)
489  self.graph = parser.parse()
490  self.highlight = None
491  #self.zoom_image(self.zoom_ratio, center=True)
492 
493  def reload(self):
494  if self.openfilename is not None:
495  try:
496  fp = file(self.openfilename, 'rt')
497  self.set_dotcode(fp.read(), self.openfilename)
498  fp.close()
499  except IOError:
500  pass
501 
502 
503 class WxDotFrame(wx.Frame):
504  def __init__(self):
505  wx.Frame.__init__(self, None, -1, "Dot Viewer", size=(512,512))
506 
507  vbox = wx.BoxSizer(wx.VERTICAL)
508 
509  # Construct toolbar
510  toolbar = wx.ToolBar(self, -1)
511  toolbar.AddLabelTool(wx.ID_OPEN, 'Open File',
512  wx.ArtProvider.GetBitmap(wx.ART_FOLDER_OPEN,wx.ART_OTHER,(16,16)))
513  toolbar.AddLabelTool(wx.ID_HELP, 'Help',
514  wx.ArtProvider.GetBitmap(wx.ART_HELP,wx.ART_OTHER,(16,16)) )
515  toolbar.Realize()
516 
517  self.Bind(wx.EVT_TOOL, self.DoOpenFile, id=wx.ID_OPEN)
518  self.Bind(wx.EVT_TOOL, self.ShowControlsDialog, id=wx.ID_HELP)
519 
520  # Create dot widge
521  self.widget = WxDotWindow(self, -1)
522 
523  # Add elements to sizer
524  vbox.Add(toolbar, 0, wx.EXPAND)
525  vbox.Add(self.widget, 100, wx.EXPAND | wx.ALL)
526 
527  self.SetSizer(vbox)
528  self.Center()
529 
530  def ShowControlsDialog(self,event):
531  dial = wx.MessageDialog(None,
532  "\
533 Pan: Arrow Keys\n\
534 Zoom: PageUp / PageDown\n\
535 Zoom To Fit: F\n\
536 Refresh: R",
537  'Keyboard Controls', wx.OK)
538  dial.ShowModal()
539 
540  def DoOpenFile(self,event):
541  wcd = 'All files (*)|*|GraphViz Dot Files(*.dot)|*.dot|'
542  dir = os.getcwd()
543  open_dlg = wx.FileDialog(self, message='Choose a file', defaultDir=dir, defaultFile='',
544  wildcard=wcd, style=wx.OPEN|wx.CHANGE_DIR)
545  if open_dlg.ShowModal() == wx.ID_OK:
546  path = open_dlg.GetPath()
547 
548  try:
549  self.open_file(path)
550 
551  except IOError, error:
552  dlg = wx.MessageDialog(self, 'Error opening file\n' + str(error))
553  dlg.ShowModal()
554 
555  except UnicodeDecodeError, error:
556  dlg = wx.MessageDialog(self, 'Error opening file\n' + str(error))
557  dlg.ShowModal()
558 
559  open_dlg.Destroy()
560 
561  def OnExit(self, event):
562  pass
563 
564  def set_dotcode(self, dotcode, filename='<stdin>'):
565  if self.widget.set_dotcode(dotcode, filename):
566  self.SetTitle(os.path.basename(filename) + ' - Dot Viewer')
567  self.widget.zoom_to_fit()
568 
569  def set_xdotcode(self, xdotcode, filename='<stdin>'):
570  if self.widget.set_xdotcode(xdotcode):
571  self.SetTitle(os.path.basename(filename) + ' - Dot Viewer')
572  self.widget.zoom_to_fit()
573 
574  def open_file(self, filename):
575  try:
576  fp = file(filename, 'rt')
577  self.set_dotcode(fp.read(), filename)
578  fp.close()
579  except IOError, ex:
580  """
581  dlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
582  message_format=str(ex),
583  buttons=gtk.BUTTONS_OK)
584  dlg.set_title('Dot Viewer')
585  dlg.run()
586  dlg.destroy()
587  """
588 
589  def set_filter(self, filter):
590  self.widget.set_filter(filter)
591 
def on_area_size_allocate(self, area, allocation)
Definition: wxxdot.py:409
def on_motion_notify(self, event)
Definition: wxxdot.py:58
def zoom_image(self, zoom_ratio, center=False, pos=None)
Zooming methods.
Definition: wxxdot.py:279
def on_zoom_fit(self, action)
Definition: wxxdot.py:334
def register_select_callback(self, cb)
User callbacks.
Definition: wxxdot.py:179
def zoom_to_area(self, x1, y1, x2, y2)
Definition: wxxdot.py:295
def __init__(self, parent, id)
Definition: wxxdot.py:147
def drag(self, deltax, deltay)
Definition: wxxdot.py:78
def get_drag_action(self, event)
Definition: wxxdot.py:342
def set_highlight(self, items)
Definition: wxxdot.py:267
def on_zoom_in(self, action)
Definition: wxxdot.py:328
def drag(self, deltax, deltay)
Definition: wxxdot.py:95
def on_motion_notify(self, event)
Definition: wxxdot.py:88
def on_button_release(self, event)
Definition: wxxdot.py:66
def on_zoom_100(self, action)
Definition: wxxdot.py:337
def get_current_pos(self)
Helper functions.
Definition: wxxdot.py:257
def OnResize(self, event)
Event handlers.
Definition: wxxdot.py:183
def set_cursor(self, cursor_type)
Cursor manipulation.
Definition: wxxdot.py:274
def on_zoom_out(self, action)
Definition: wxxdot.py:331
def __init__(self, dot_widget)
Definition: wxxdot.py:49
def drag(self, deltax, deltay)
Definition: wxxdot.py:115
def on_button_press(self, event)
Definition: wxxdot.py:52
def drag(self, deltax, deltay)
Definition: wxxdot.py:106


smach_viewer
Author(s): Jonathan Bohren
autogenerated on Mon Jun 10 2019 13:13:42