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