gladevcp.py
1 #!/usr/bin/env python 2 # vim: sts=4 sw=4 et 3 # This is a component of EMC 4 # gladevcp Copyright 2010 Chris Morley 5 # 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 """ Python / GLADE based Virtual Control Panel for EMC 22 23 A virtual control panel (VCP) is used to display and control 24 HAL pins. 25 26 Usage: gladevcp -g position -c compname -H halfile -x windowid myfile.glade 27 compname is the name of the HAL component to be created. 28 halfile contains hal commands to be executed with halcmd after the hal component is ready 29 The name of the HAL pins associated with the VCP will begin with 'compname.' 30 31 myfile.glade is an XML file which specifies the layout of the VCP. 32 33 -g option allows setting of the initial position of the panel 34 """ 35 import sys, os, subprocess 36 import traceback 37 import warnings 38 39 import hal 40 from optparse import Option, OptionParser 41 import gtk 42 import gtk.glade 43 import gobject 44 import signal 45 46 import gladevcp.makepins 47 from gladevcp.gladebuilder import GladeBuilder 48 from gladevcp import xembed 49 from hal_glib import GStat 50 GSTAT = GStat() 51 52 options = [ Option( '-c', dest='component', metavar='NAME' 53 , help="Set component name to NAME. Default is basename of UI file") 54 , Option( '-d', action='store_true', dest='debug' 55 , help="Enable debug output") 56 , Option( '-g', dest='geometry', default="", help="""Set geometry WIDTHxHEIGHT+XOFFSET+YOFFSET. 57 Values are in pixel units, XOFFSET/YOFFSET is referenced from top left of screen 58 use -g WIDTHxHEIGHT for just setting size or -g +XOFFSET+YOFFSET for just position""") 59 , Option( '-H', dest='halfile', metavar='FILE' 60 , help="execute hal statements from FILE with halcmd after the component is set up and ready") 61 , Option( '-m', dest='maximum', default=False, help="Force panel window to maxumize") 62 , Option( '-r', dest='gtk_rc', default="", 63 help="read custom GTK rc file to set widget style") 64 , Option( '-R', dest='gtk_workaround', action='store_false',default=True, 65 help="disable workaround for GTK bug to properly read ~/.gtkrc-2.0 gtkrc files") 66 , Option( '-t', dest='theme', default="", help="Set gtk theme. Default is system theme") 67 , Option( '-x', dest='parent', type=int, metavar='XID' 68 , help="Reparent gladevcp into an existing window XID instead of creating a new top level window") 69 , Option( '--xid', action='store_true', dest='push_XID' 70 , help="reparent window into a plug add push the plug xid number to standardout") 71 , Option( '-u', dest='usermod', action='append', default=[], metavar='FILE' 72 , help='Use FILEs as additional user defined modules with handlers') 73 , Option( '-U', dest='useropts', action='append', metavar='USEROPT', default=[] 74 , help='pass USEROPTs to Python modules') 75 , Option( '--always_above', action='store_true', dest='always_above_flag' 76 , help="Request the window To always be above other windows") 77 ] 78 79 signal_func = 'on_unix_signal' 80 81 gladevcp_debug = 0 82 def dbg(string): 83 global gladevcp_debug 84 if not gladevcp_debug: return 85 print string 86 87 def on_window_destroy(widget, data=None): 88 gtk.main_quit() 89 90 class Trampoline(object): 91 def __init__(self,methods): 92 self.methods = methods 93 94 def __call__(self, *a, **kw): 95 for m in self.methods: 96 m(*a, **kw) 97 98 def load_handlers(usermod,halcomp,builder,useropts): 99 hdl_func = 'get_handlers' 100 mod = object = None 101 def add_handler(method, f): 102 if method in handlers: 103 handlers[method].append(f) 104 else: 105 handlers[method] = [f] 106 107 handlers = {} 108 for u in usermod: 109 (directory,filename) = os.path.split(u) 110 (basename,extension) = os.path.splitext(filename) 111 if directory == '': 112 directory = '.' 113 if directory not in sys.path: 114 sys.path.insert(0,directory) 115 dbg('adding import dir %s' % directory) 116 117 try: 118 mod = __import__(basename) 119 except ImportError,msg: 120 print "module '%s' skipped - import error: %s" %(basename,msg) 121 continue 122 dbg("module '%s' imported OK" % mod.__name__) 123 try: 124 # look for 'get_handlers' function 125 h = getattr(mod,hdl_func,None) 126 127 if h and callable(h): 128 dbg("module '%s' : '%s' function found" % (mod.__name__,hdl_func)) 129 objlist = h(halcomp,builder,useropts) 130 else: 131 # the module has no get_handlers() callable. 132 # in this case we permit any callable except class Objects in the module to register as handler 133 dbg("module '%s': no '%s' function - registering only functions as callbacks" % (mod.__name__,hdl_func)) 134 objlist = [mod] 135 # extract callback candidates 136 for object in objlist: 137 dbg("Registering handlers in module %s object %s" % (mod.__name__, object)) 138 if isinstance(object, dict): 139 methods = dict.items() 140 else: 141 methods = map(lambda n: (n, getattr(object, n, None)), dir(object)) 142 for method,f in methods: 143 if method.startswith('_'): 144 continue 145 if callable(f): 146 dbg("Register callback '%s' in %s" % (method, object)) 147 add_handler(method, f) 148 except Exception, e: 149 print "gladevcp: trouble looking for handlers in '%s': %s" %(basename, e) 150 traceback.print_exc() 151 152 # Wrap lists in Trampoline, unwrap single functions 153 for n,v in list(handlers.items()): 154 if len(v) == 1: 155 handlers[n] = v[0] 156 else: 157 handlers[n] = Trampoline(v) 158 159 return handlers, mod, object 160 161 def main(): 162 """ creates a HAL component. 163 parsees a glade XML file with gtk.builder or libglade 164 calls gladevcp.makepins with the specified XML file 165 to create pins and register callbacks. 166 main window must be called "window1" 167 """ 168 global gladevcp_debug 169 (progdir, progname) = os.path.split(sys.argv[0]) 170 171 usage = "usage: %prog [options] myfile.ui" 172 parser = OptionParser(usage=usage) 173 parser.disable_interspersed_args() 174 parser.add_options(options) 175 176 (opts, args) = parser.parse_args() 177 if not args: 178 parser.print_help() 179 sys.exit(1) 180 181 gladevcp_debug = debug = opts.debug 182 xmlname = args[0] 183 184 #if there was no component name specified use the xml file name 185 if opts.component is None: 186 opts.component = os.path.splitext(os.path.basename(xmlname))[0] 187 188 #try loading as a libglade project 189 try: 190 builder = gtk.Builder() 191 builder.add_from_file(xmlname) 192 except: 193 try: 194 # try loading as a gtk.builder project 195 dbg("**** GLADE VCP INFO: Not a builder project, trying to load as a lib glade project") 196 builder = gtk.glade.XML(xmlname) 197 builder = GladeBuilder(builder) 198 199 except Exception,e: 200 print >> sys.stderr, "**** GLADE VCP ERROR: With xml file: %s : %s" % (xmlname,e) 201 sys.exit(0) 202 203 window = builder.get_object("window1") 204 205 window.set_title(opts.component) 206 207 try: 208 halcomp = hal.component(opts.component) 209 except: 210 print >> sys.stderr, "*** GLADE VCP ERROR: Asking for a HAL component using a name that already exists." 211 sys.exit(0) 212 213 panel = gladevcp.makepins.GladePanel( halcomp, xmlname, builder, None) 214 215 # at this point, any glade HL widgets and their pins are set up. 216 handlers, mod, obj = load_handlers(opts.usermod,halcomp,builder,opts.useropts) 217 218 # so widgets can call handler functions - give them refeence to the handler object 219 panel.set_handler(obj) 220 221 builder.connect_signals(handlers) 222 223 # This option puts the gladevcp panel into a plug and pushed the plug's 224 # X window id number to standard output - so it can be reparented exterally 225 # it also forwards events to qtvcp 226 if opts.push_XID: 227 if not opts.debug: 228 # supress warnings when x window closes 229 warnings.filterwarnings("ignore") 230 # block X errors since gdk error handling silently exits the 231 # program without even the atexit handler given a chance 232 gtk.gdk.error_trap_push() 233 234 forward = os.environ.get('QTVCP_FORWARD_EVENTS_TO', None) 235 if forward: 236 xembed.keyboard_forward(window, forward) 237 238 # This option reparents gladevcp in a given X window id. 239 # it also forwards keyboard events from gladevcp to AXIS 240 if opts.parent: 241 if not opts.debug: 242 # supress warnings when x window closes 243 warnings.filterwarnings("ignore") 244 # block X errors since gdk error handling silently exits the 245 # program without even the atexit handler given a chance 246 gtk.gdk.error_trap_push() 247 248 window = xembed.reparent(window, opts.parent) 249 250 forward = os.environ.get('AXIS_FORWARD_EVENTS_TO', None) 251 if forward: 252 xembed.keyboard_forward(window, forward) 253 254 window.connect("destroy", on_window_destroy) 255 window.show() 256 257 # for window resize and or position options 258 if "+" in opts.geometry: 259 try: 260 j = opts.geometry.partition("+") 261 pos = j[2].partition("+") 262 window.move( int(pos[0]), int(pos[2]) ) 263 except: 264 print >> sys.stderr, "**** GLADE VCP ERROR: With window position data" 265 parser.print_usage() 266 sys.exit(1) 267 if "x" in opts.geometry: 268 try: 269 if "+" in opts.geometry: 270 j = opts.geometry.partition("+") 271 t = j[0].partition("x") 272 else: 273 t = window_geometry.partition("x") 274 window.resize( int(t[0]), int(t[2]) ) 275 except: 276 print >> sys.stderr, "**** GLADE VCP ERROR: With window resize data" 277 parser.print_usage() 278 sys.exit(1) 279 280 if opts.gtk_workaround: 281 # work around https://bugs.launchpad.net/ubuntu/+source/pygtk/+bug/507739 282 # this makes widget and widget_class matches in gtkrc and theme files actually work 283 dbg( "activating GTK bug workaround for gtkrc files") 284 for o in builder.get_objects(): 285 if isinstance(o, gtk.Widget): 286 # retrieving the name works only for GtkBuilder files, not for 287 # libglade files, so be cautious about it 288 name = gtk.Buildable.get_name(o) 289 if name: o.set_name(name) 290 291 if opts.gtk_rc: 292 dbg( "**** GLADE VCP INFO: %s reading gtkrc file '%s'" %(opts.component,opts.gtk_rc)) 293 gtk.rc_add_default_file(opts.gtk_rc) 294 gtk.rc_parse(opts.gtk_rc) 295 296 if opts.theme: 297 dbg("**** GLADE VCP INFO: Switching %s to '%s' theme" %(opts.component,opts.theme)) 298 settings = gtk.settings_get_default() 299 settings.set_string_property("gtk-theme-name", opts.theme, "") 300 301 # This needs to be done after geometry moves so on dual screens the window maxumizes to the actual used screen size. 302 if opts.maximum: 303 window.window.maximize() 304 if opts.always_above_flag: 305 window.set_keep_above(True) 306 if opts.halfile: 307 if opts.halfile[-4:] == ".tcl": 308 cmd = ["haltcl", opts.halfile] 309 else: 310 cmd = ["halcmd", "-f", opts.halfile] 311 res = subprocess.call(cmd, stdout=sys.stdout, stderr=sys.stderr) 312 if res: 313 print >> sys.stderr, "'%s' exited with %d" %(' '.join(cmd), res) 314 sys.exit(res) 315 316 # User components are set up so report that we are ready 317 halcomp.ready() 318 GSTAT.forced_update() 319 320 # push the XWindow id number to standard out 321 if opts.push_XID or opts.parent: 322 gdkwin = window.get_window() 323 w_id = gdkwin.xid 324 print >> sys.stdout,w_id 325 sys.stdout.flush() 326 327 if handlers.has_key(signal_func): 328 dbg("Register callback '%s' for SIGINT and SIGTERM" %(signal_func)) 329 signal.signal(signal.SIGTERM, handlers[signal_func]) 330 signal.signal(signal.SIGINT, handlers[signal_func]) 331 332 try: 333 gtk.main() 334 except KeyboardInterrupt: 335 sys.exit(0) 336 finally: 337 halcomp.exit() 338 339 if opts.parent or opts.push_XID: 340 gtk.gdk.flush() 341 error = gtk.gdk.error_trap_pop() 342 if error and opts.debug: 343 print >> sys.stderr, "**** GLADE VCP ERROR: X Protocol Error: %s" % str(error) 344 345 346 if __name__ == '__main__': 347 main()