/ lib / python / gremlin_view.py
gremlin_view.py
  1  #!/usr/bin/env python
  2  
  3  #------------------------------------------------------------------------------
  4  # Copyright: 2013
  5  # Author:    Dewey Garrett <dgarrett@panix.com>
  6  #
  7  # This program is free software; you can redistribute it and/or modify
  8  # it under the terms of the GNU General Public License as published by
  9  # the Free Software Foundation; either version 2 of the License, or
 10  # (at your option) any later version.
 11  #
 12  # This program is distributed in the hope that it will be useful,
 13  # but WITHOUT ANY WARRANTY; without even the implied warranty of
 14  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15  # GNU General Public License for more details.
 16  #
 17  # You should have received a copy of the GNU General Public License
 18  # along with this program; if not, write to the Free Software
 19  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 20  #------------------------------------------------------------------------------
 21  
 22  """gremlin_view
 23  Provide class: GremlinView for gremlin with buttons for simpler embedding
 24  Standalone functionality if linuxcnc running
 25  
 26  A default ui file (gremlin_view.ui) is provided for a default
 27  button arrangement but a user may provide their own by supplying
 28  the glade_file argument.
 29  
 30  The following objects are mandatory:
 31  
 32    'gremlin_view_window'      toplevel window
 33    'gremlin_view_hal_gremlin' hal_gremlin
 34    'gremlin_view_box'         HBox or VBox' containing hal_gremlin
 35  
 36  Optional radiobutton group names:
 37    'select_p_view'
 38    'select_x_view'
 39    'select_y_view'
 40    'select_z_view'
 41    'select_z2_view'
 42  
 43  Optional Checkbuttons names:
 44    'enable_dro'
 45    'show_machine_speed
 46    'show_distance_to_go'
 47    'show_limits'
 48    'show_extents'
 49    'show_tool'
 50    'show_metric'
 51  
 52  Callbacks are provided for the following buttons actions
 53    on_clear_live_plotter_clicked
 54    on_enable_dro_clicked
 55    on_zoomin_pressed
 56    on_zoomout_pressed
 57    on_pan_x_minus_pressed
 58    on_pan_x_plus_pressed
 59    on_pan_y_minus_pressed
 60    on_pan_y_plus_pressed
 61    on_show_tool_clicked
 62    on_show_metric_clicked
 63    on_show_extents_clicked
 64    on_select_p_view_clicked
 65    on_select_x_view_clicked
 66    on_select_y_view_clicked
 67    on_select_z_view_clicked
 68    on_select_z2_view_clicked
 69    on_show_distance_to_go_clicked
 70    on_show_machine_speed_clicked
 71    on_show_limits_clicked
 72  """
 73  
 74  import os
 75  import sys
 76  import gtk
 77  import gladevcp.hal_actions  # reqd for Builder
 78  import linuxcnc
 79  import time
 80  import subprocess
 81  import gettext
 82  import datetime
 83  import gobject
 84  import glib # for glib.GError
 85  
 86  g_ui_dir          = linuxcnc.SHARE + "/linuxcnc"
 87  g_periodic_secs   = 1 # integer
 88  g_delta_pixels    = 10
 89  g_move_delay_secs = 0.2
 90  g_progname        = os.path.basename(sys.argv[0])
 91  g_verbose         = False
 92  
 93  LOCALEDIR = linuxcnc.SHARE + "/locale"
 94  gettext.install("linuxcnc", localedir=LOCALEDIR, unicode=True)
 95  
 96  def ini_check ():
 97      """set environmental variable and change directory"""
 98      # Note:
 99      #   hal_gremlin gets inifile from os.environ (only)
100      #   hal_gremlin expects cwd to be same as ini file
101      ini_filename = get_linuxcnc_ini_file()
102      if ini_filename is not None:
103          os.putenv('INI_FILE_NAME',ini_filename)    # ineffective
104          os.environ['INI_FILE_NAME'] = ini_filename # need for hal_gremlin
105          os.chdir(os.path.dirname(ini_filename))
106          if g_verbose:
107              print('ini_check: INI_FILENAME= %s' % ini_filename)
108              print('ini_check:       curdir= %s' % os.path.curdir)
109          return True # success
110      print(_('%s:linuxcnc ini file  not available') % g_progname)
111      return False # exit here crashes glade-gtk2
112  
113  def get_linuxcnc_ini_file():
114      """find linuxcnc ini file with pgrep"""
115      ps   = subprocess.Popen('ps -C linuxcncsvr --no-header -o args'.split(),
116                               stdout=subprocess.PIPE
117                             )
118      p,e = ps.communicate()
119  
120      if ps.returncode:
121          print(_('get_linuxcnc_ini_file: stdout= %s') % p)
122          print(_('get_linuxcnc_ini_file: stderr= %s') % e)
123          return None
124  
125      ans = p.split()[p.split().index('-ini')+1]
126      return ans
127  
128  class GremlinView():
129      """Implement a standalone gremlin with some buttons
130         and provide means to embed using a glade ui file"""
131      def __init__(self
132                  ,glade_file=None #None: use default ui
133                  ,parent=None
134                  ,width=None
135                  ,height=None
136                  ,alive=True
137                  ,gtk_theme_name="Follow System Theme"
138                  ):
139  
140          self.alive = alive
141          linuxcnc_running = False
142          if ini_check():
143              linuxcnc_running = True
144  
145          if (glade_file == None):
146              glade_file = os.path.join(g_ui_dir,'gremlin_view.ui')
147  
148          bldr = gtk.Builder()
149          try:
150              bldr.add_from_file(glade_file)
151          except glib.GError,detail:
152              print('\nGremlinView:%s\n' % detail)
153              raise glib.GError,detail # re-raise
154  
155          # required objects:
156          self.topwindow = bldr.get_object('gremlin_view_window')
157          self.gbox      = bldr.get_object('gremlin_view_box')
158          self.halg      = bldr.get_object('gremlin_view_hal_gremlin')
159  
160          #self.halg.show_lathe_radius = 1 # for test, hal_gremlin default is Dia
161  
162          if not linuxcnc_running:
163              # blanks display area:
164              self.halg.set_has_window(False)
165  
166          # radiobuttons for selecting view: (expect at least one)
167          select_view_letters = ['p','x','y','z','z2']
168          found_view = None
169          for vletter in select_view_letters:
170              try:
171                  obj = bldr.get_object('select_' + vletter + '_view')
172              except:
173                  continue
174              if obj is not None:
175                  setattr(self,vletter + '_view',obj)
176                  if found_view is None:
177                      found_view = obj
178                      self.my_view = vletter
179                      obj.set_group(None)
180                      obj.set_active(True)
181                  else:
182                      obj.set_group(found_view)
183          if found_view is None:
184              print('%s:Expected to find "select_*_view"' % __file__)
185  
186          check_button_objects = ['enable_dro'
187                                 ,'show_machine_speed'
188                                 ,'show_distance_to_go'
189                                 ,'show_limits'
190                                 ,'show_extents'
191                                 ,'show_tool'
192                                 ,'show_metric'
193                                 ]
194          for objname in check_button_objects:
195              obj = bldr.get_object(objname)
196              if obj is not None:
197                  setattr(self,'objname',obj)
198                  obj.set_active(True)
199              else:
200                  if g_verbose:
201                      print('%s: Optional object omitted <%s>'
202                            % (__file__,objname))
203  
204          # show_metric: use ini file
205  #FIXME  show_metric,lunits s/b mandatory?
206          try:
207              objname = 'show_metric'
208              self.show_metric = bldr.get_object('show_metric')
209              lunits = self.halg.inifile.find('TRAJ','LINEAR_UNITS')
210          except AttributeError:
211              if g_verbose:
212                  print('%s: Problem for <%s>' % (__file__,objname))
213  
214          if linuxcnc_running:
215              if   lunits == 'inch':
216                  self.halg.metric_units = False
217              elif lunits == 'mm':
218                  self.halg.metric_units = True
219              else:
220                  raise AttributeError,('%s: unknown [TRAJ]LINEAR_UNITS] <%s>'
221                                       % (__file__,lunits))
222  
223          if self.halg.get_show_metric():
224              self.show_metric.set_active(True)
225          else:
226              self.show_metric.set_active(False)
227  
228          if alive:
229              bldr.connect_signals(self)
230              # todo: to remove other signals on halg:
231              # bldr.disconnect(integer_handle_id)
232              # bldr.disconnect_by_func('func_name')
233              # bldr.handler_disconnect()
234  
235          minwidth  = 300 # smallest size
236          minheight = 300 # smallest size
237  
238          if (width  is None):
239              width = minwidth
240          else:
241              width = int(width)
242  
243          if (height is None):
244              height = minheight
245          else:
246              height = int(height)
247  
248          if width  < minwidth:
249              width  = minwidth
250          if height < minheight:
251              height = minheight
252  
253          # err from gremlin if omit this
254          self.halg.width  = width
255          self.halg.height = height
256          self.halg.set_size_request(width,height)
257  
258          # self.x,self.y used in conjunction with pan buttons
259          # but using mouse may change internal values in gremlin
260          # resulting in unexpected movement if both mouse and
261          # pan buttons are used
262          self.x = 0
263          self.y = 0
264  
265          #  prevent flashing topwindow
266          self.topwindow.iconify()
267          self.topwindow.show_all()
268          self.topwindow.hide()
269  
270          self.preview_file(None)
271          if linuxcnc_running:
272              try:
273                  self.preview_file(None)
274              except linuxcnc.error,detail:
275                  print('linuxcnc.error')
276                  print('        detail=',detail)
277  
278          try:
279              self.last_file = self.halg._current_file
280          except AttributeError:
281              self.last_file = None
282          self.last_file_mtime = None
283  
284          self.parent = parent
285          if self.parent is None:
286              # topwindow (standalone) application
287              # print "TOP:",gtk_theme_name
288              screen   = self.topwindow.get_screen()
289          else:
290              # print "REPARENT:",gtk_theme_name
291              screen   = self.halg.get_screen()
292  
293          settings = gtk.settings_get_for_screen(screen)
294          systname = settings.get_property("gtk-theme-name")
295          if (   (gtk_theme_name is None)
296              or (gtk_theme_name == "")
297              or (gtk_theme_name == "Follow System Theme")):
298              gtk_theme_name = systname
299          settings.set_string_property('gtk-theme-name',gtk_theme_name,"")
300  
301          self.topwindow.connect('destroy',self._topwindowquit)
302          self.topwindow.show_all()
303          self.running = True
304  
305          if self.last_file is not None:
306              self.topwindow.set_title(g_progname
307                        + ': ' + os.path.basename(self.last_file))
308              self.last_file_mtime = datetime.datetime.fromtimestamp(
309                                     os.path.getmtime(self.last_file))
310  
311          self.ct = 0
312          if self.parent is None: self.topwindow.deiconify()
313          self._periodic('BEGIN')
314          gobject.timeout_add_seconds(g_periodic_secs,self._periodic,'Continue')
315          # or use gobject.timeout_add() interval units in mS
316  
317      def _periodic(self,arg):
318          # print "_periodic:",self.ct,arg
319          self.ct +=1
320          self.halg.poll()
321  
322          if (self.parent is not None) and (self.ct) == 2:
323              # not sure why delay is needed for reparenting
324              # but without, the display of the (rgb) axes
325              # and the cone to not appear in gremlin
326              # print "REPARENT:",self.gbox, self.parent
327              #-----------------------------------------------------------------------------
328              # determine if glade interface designer is running
329              # to avoid assertion error:
330              # gtk_widget_reparent_fixup_child: assertion failed: (client_data != NULL)
331              is_glade = False
332              if 'glade' in sys.argv[0] and 'gladevcp' not in sys.argv[0]:
333                  for d in os.environ['PATH'].split(':'):
334                      f = os.path.join(d,sys.argv[0])
335                      if (    os.path.isfile(f)
336                          and os.access(f, os.X_OK)):
337                          is_glade = True
338                          break
339              #-----------------------------------------------------------------------------
340              if (not is_glade):
341                  self.gbox.reparent(self.parent)
342              self.gbox.show_all()
343              self.gbox.connect('destroy',self._gboxquit)
344              return True
345  
346          try:
347              current_file = self.halg._current_file
348          except AttributeError:
349              current_file = None
350          if current_file is None:
351              return True # keep trying _periodic()
352          current_file_mtime = datetime.datetime.fromtimestamp(
353                                 os.path.getmtime(current_file))
354          if (   current_file       != self.last_file
355              or current_file_mtime != self.last_file_mtime):
356              # print('old,new',self.last_file_mtime,current_file_mtime)
357              self.last_file       = current_file
358              self.last_file_mtime = current_file_mtime
359              self.halg.hide()
360              self.halg.load()
361              getattr(self.halg,'set_view_%s' % self.my_view)()
362              self.halg.show()
363              if self.topwindow is not None:
364                  self.topwindow.set_title(g_progname
365                             + ': ' + os.path.basename(self.last_file))
366          return True # repeat _periodic()
367  
368      def preview_file(self,filename):
369          self.halg.hide()
370          # handle exception in case glade is running
371          try:
372              self.halg.load(filename or None)
373          except Exception, detail:
374              if self.alive:
375                  print "file load fail:",Exception,detail
376              pass
377          getattr(self.halg,'set_view_%s' % self.my_view)()
378          self.halg.show()
379  
380      def _gboxquit(self,w):
381          self.running = False # stop periodic checks
382  
383      def _topwindowquit(self,w):
384          self.running = False # stop periodic checks
385          gtk.main_quit()
386  
387      def expose(self):
388          self.halg.expose()
389  
390      def on_zoomin_pressed(self,w):
391          while w.get_state() == gtk.STATE_ACTIVE:
392              self.halg.zoomin()
393              time.sleep(g_move_delay_secs)
394              gtk.main_iteration_do()
395  
396      def on_zoomout_pressed(self,w):
397          while w.get_state() == gtk.STATE_ACTIVE:
398              self.halg.zoomout()
399              time.sleep(g_move_delay_secs)
400              gtk.main_iteration_do()
401  
402      def on_pan_x_minus_pressed(self,w):
403          while w.get_state() == gtk.STATE_ACTIVE:
404              self.x -= g_delta_pixels
405              self.halg.translate(self.x,self.y)
406              time.sleep(g_move_delay_secs)
407              gtk.main_iteration_do()
408  
409      def on_pan_x_plus_pressed(self,w):
410          while w.get_state() == gtk.STATE_ACTIVE:
411              self.x += g_delta_pixels
412              self.halg.translate(self.x,self.y)
413              time.sleep(g_move_delay_secs)
414              gtk.main_iteration_do()
415  
416      def on_pan_y_minus_pressed(self,w):
417          while w.get_state() == gtk.STATE_ACTIVE:
418              self.y += g_delta_pixels
419              self.halg.translate(self.x,self.y)
420              time.sleep(g_move_delay_secs)
421              gtk.main_iteration_do()
422  
423      def on_pan_y_plus_pressed(self,w):
424          while w.get_state() == gtk.STATE_ACTIVE:
425              self.y -= g_delta_pixels
426              self.halg.translate(self.x,self.y)
427              time.sleep(g_move_delay_secs)
428              gtk.main_iteration_do()
429  
430      def on_clear_live_plotter_clicked(self,w):
431          self.halg.clear_live_plotter()
432  
433      def on_enable_dro_clicked(self,w):
434          if w.get_active():
435              self.halg.enable_dro = True
436          else:
437              self.halg.enable_dro = False
438          self.expose()
439  
440      def on_show_machine_speed_clicked(self,w):
441          if w.get_active():
442              self.halg.show_velocity = True
443          else:
444              self.halg.show_velocity = False
445          self.expose()
446  
447      def on_show_distance_to_go_clicked(self,w):
448          if w.get_active():
449              self.halg.show_dtg = True
450          else:
451              self.halg.show_dtg = False
452          self.expose()
453  
454      def on_show_limits_clicked(self,w):
455          if w.get_active():
456              self.halg.show_limits = True
457          else:
458              self.halg.show_limits = False
459          self.expose()
460  
461      def on_show_extents_clicked(self,w):
462          if w.get_active():
463              self.halg.show_extents_option = True
464          else:
465              self.halg.show_extents_option = False
466          self.expose()
467  
468      def on_show_tool_clicked(self,w):
469          if w.get_active():
470              self.halg.show_tool = True
471          else:
472              self.halg.show_tool = False
473          self.expose()
474  
475      def on_show_metric_clicked(self,w):
476          if w.get_active():
477              self.halg.metric_units = True
478          else:
479              self.halg.metric_units = False
480          self.expose()
481  
482      def on_select_p_view_clicked(self,w):
483          self.set_view_per_w(w,'p')
484  
485      def on_select_x_view_clicked(self,w):
486          self.set_view_per_w(w,'x')
487  
488      def on_select_y_view_clicked(self,w):
489          self.set_view_per_w(w,'y')
490  
491      def on_select_z_view_clicked(self,w):
492          self.set_view_per_w(w,'z')
493  
494      def on_select_z2_view_clicked(self,w):
495          self.set_view_per_w(w,'z2')
496  
497      def set_view_per_w(self,w,vletter):
498          if not w.get_active(): return
499          self.halg.hide()
500          getattr(self.halg,'set_view_%s' % vletter)()
501          self.my_view = vletter
502          self.halg.show()
503  
504  #-----------------------------------------------------------------------------
505  # Standalone (and demo) usage:
506  def standalone_gremlin_view():
507  
508      import getopt
509      #---------------------------------------
510      def usage(msg=None):
511  
512          print("""\n
513  Usage:   %s [options]\n
514  Options: [-h | --help]
515           [-v | --verbose]
516           [-W | --width]  width
517           [-H | --height] height
518           [-f | --file]   glade_file
519  
520  Note: linuxcnc must be running on same machine
521  """) % g_progname
522          if msg:
523              print('\n%s' % msg)
524      #---------------------------------------
525  
526      glade_file  = None
527      width       = None
528      height      = None
529      vbose       = False
530      try:
531          options,remainder = getopt.getopt(sys.argv[1:]
532                                           , 'f:hH:vW:'
533                                           , ['file='
534                                             ,'help'
535                                             ,'width='
536                                             ,'height='
537                                             ]
538                                           )
539      except getopt.GetoptError,msg:
540          usage()
541          print('GetoptError: %s' % msg)
542          sys.exit(1)
543      for opt,arg in options:
544          if opt in ('-h','--help'):
545              usage(),sys.exit(0)
546          if opt in ('-v','--verbose'):
547              g_verbose = True
548              continue
549          if opt in ('-W','--width' ): width=arg
550          if opt in ('-H','--height'): height=arg
551          if opt in ('-f','--file'):   glade_file=arg
552      if remainder:
553          usage('unknown argument:%s' % remainder)
554          sys.exit(1)
555  
556      try:
557          g = GremlinView(glade_file=glade_file
558                         ,width=width
559                         ,height=height
560                         )
561          gtk.main()
562      except linuxcnc.error,detail:
563          gtk.main()
564          print('linuxcnc.error:',detail)
565          usage()
566  
567  # vim: sts=4 sw=4 et