Added interface for gtk.StatusIcon
[rox-lib.git] / ROX-Lib2 / python / rox / __init__.py
blobdc98f82d927788c9ababfb73391693b57c62c5ec
1 """To use ROX-Lib2 you need to copy the findrox.py script into your application
2 directory and import that before anything else. This module will locate
3 ROX-Lib2 and add ROX-Lib2/python to sys.path. If ROX-Lib2 is not found, it
4 will display a suitable error and quit.
6 Since the name of the gtk2 module can vary, it is best to import it from rox,
7 where it is named 'g'.
9 The AppRun script of a simple application might look like this:
11 #!/usr/bin/env python
12 import findrox; findrox.version(1, 9, 12)
13 import rox
15 window = rox.Window()
16 window.set_title('My window')
17 window.show()
19 rox.mainloop()
21 This program creates and displays a window. The rox.Window widget keeps
22 track of how many toplevel windows are open. rox.mainloop() will return
23 when the last one is closed.
25 'rox.app_dir' is set to the absolute pathname of your application (extracted
26 from sys.argv).
28 The builtin names True and False are defined to 1 and 0, if your version of
29 python is old enough not to include them already.
30 """
32 import sys, os, codecs
34 _to_utf8 = codecs.getencoder('utf-8')
36 roxlib_version = (2, 0, 5)
38 _path = os.path.realpath(sys.argv[0])
39 app_dir = os.path.dirname(_path)
40 if _path.endswith('/AppRun') or _path.endswith('/AppletRun'):
41 sys.argv[0] = os.path.dirname(_path)
43 # In python2.3 there is a bool type. Later versions of 2.2 use ints, but
44 # early versions don't support them at all, so create them here.
45 try:
46 True
47 except:
48 import __builtin__
49 __builtin__.False = 0
50 __builtin__.True = 1
52 try:
53 iter
54 except:
55 sys.stderr.write('Sorry, you need to have python 2.2, and it \n'
56 'must be the default version. You may be able to \n'
57 'change the first line of your program\'s AppRun \n'
58 'file to end \'python2.2\' as a workaround.\n')
59 raise SystemExit(1)
61 import i18n
63 _roxlib_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
64 _ = i18n.translation(os.path.join(_roxlib_dir, 'Messages'))
66 # Work-around for GTK bug #303166
67 _have_stdin = '-' in sys.argv
69 try:
70 import pygtk; pygtk.require('2.0')
71 except:
72 sys.stderr.write(_('The pygtk2 package (2.0.0 or later) must be '
73 'installed to use this program:\n'
74 'http://rox.sourceforge.net/desktop/ROX-Lib\n'))
75 raise
77 try:
78 import gtk; g = gtk # Don't syntax error for python1.5
79 except ImportError:
80 sys.stderr.write(_('Broken pygtk installation: found pygtk (%s), but not gtk!\n') % pygtk.__file__)
81 raise
82 assert g.Window # Ensure not 1.2 bindings
83 have_display=g.gdk.display_get_default() is not None
85 # Put argv back the way it was, now that Gtk has initialised
86 sys.argv[0] = _path
87 if _have_stdin and '-' not in sys.argv:
88 sys.argv.append('-')
90 def _warn_old_findrox():
91 try:
92 import findrox
93 except:
94 return # Don't worry too much if it's missing
95 if not hasattr(findrox, 'version'):
96 print >>sys.stderr, _("WARNING from ROX-Lib: the version of " \
97 "findrox.py used by this application (%s) is very " \
98 "old and may cause problems.") % app_dir
99 _warn_old_findrox()
101 import warnings as _warnings
102 def _stdout_warn(message, category, filename, lineno, file = None,
103 showwarning = _warnings.showwarning):
104 if file is None: file = sys.stdout
105 showwarning(message, category, filename, lineno, file)
106 _warnings.showwarning = _stdout_warn
108 # For backwards compatibility. Use True and False in new code.
109 TRUE = True
110 FALSE = False
112 class UserAbort(Exception):
113 """Raised when the user aborts an operation, eg by clicking on Cancel
114 or pressing Escape."""
115 def __init__(self, message = None):
116 Exception.__init__(self,
117 message or _("Operation aborted at user's request"))
119 def alert(message):
120 "Display message in an error box. Return when the user closes the box."
121 toplevel_ref()
122 box = g.MessageDialog(None, 0, g.MESSAGE_ERROR, g.BUTTONS_OK, message)
123 box.set_position(g.WIN_POS_CENTER)
124 box.set_title(_('Error'))
125 box.run()
126 box.destroy()
127 toplevel_unref()
129 def bug(message = "A bug has been detected in this program. Please report "
130 "the problem to the authors."):
131 "Display an error message and offer a debugging prompt."
132 try:
133 raise Exception(message)
134 except:
135 type, value, tb = sys.exc_info()
136 import debug
137 debug.show_exception(type, value, tb, auto_details = True)
139 def croak(message):
140 """Display message in an error box, then quit the program, returning
141 with a non-zero exit status."""
142 alert(message)
143 sys.exit(1)
145 def info(message):
146 "Display informational message. Returns when the user closes the box."
147 toplevel_ref()
148 box = g.MessageDialog(None, 0, g.MESSAGE_INFO, g.BUTTONS_OK, message)
149 box.set_position(g.WIN_POS_CENTER)
150 box.set_title(_('Information'))
151 box.run()
152 box.destroy()
153 toplevel_unref()
155 def confirm(message, stock_icon, action = None):
156 """Display a <Cancel>/<Action> dialog. Result is true if the user
157 chooses the action, false otherwise. If action is given then that
158 is used as the text instead of the default for the stock item. Eg:
159 if rox.confirm('Really delete everything?', g.STOCK_DELETE): delete()
161 toplevel_ref()
162 box = g.MessageDialog(None, 0, g.MESSAGE_QUESTION,
163 g.BUTTONS_CANCEL, message)
164 if action:
165 button = ButtonMixed(stock_icon, action)
166 else:
167 button = g.Button(stock = stock_icon)
168 button.set_flags(g.CAN_DEFAULT)
169 button.show()
170 box.add_action_widget(button, g.RESPONSE_OK)
171 box.set_position(g.WIN_POS_CENTER)
172 box.set_title(_('Confirm:'))
173 box.set_default_response(g.RESPONSE_OK)
174 resp = box.run()
175 box.destroy()
176 toplevel_unref()
177 return resp == int(g.RESPONSE_OK)
179 def report_exception():
180 """Display the current python exception in an error box, returning
181 when the user closes the box. This is useful in the 'except' clause
182 of a 'try' block. Uses rox.debug.show_exception()."""
183 type, value, tb = sys.exc_info()
184 _excepthook(type, value, tb)
186 def _excepthook(ex_type, value, tb):
187 _old_excepthook(ex_type, value, tb)
188 if type(ex_type) == type and issubclass(ex_type, KeyboardInterrupt): return
189 if have_display:
190 import debug
191 debug.show_exception(ex_type, value, tb)
193 _old_excepthook = sys.excepthook
194 sys.excepthook = _excepthook
196 _icon_path = os.path.join(app_dir, '.DirIcon')
197 _window_icon = None
198 if os.path.exists(_icon_path):
199 try:
200 g.window_set_default_icon_list(g.gdk.pixbuf_new_from_file(_icon_path))
201 except:
202 # Older pygtk
203 _window_icon = g.gdk.pixbuf_new_from_file(_icon_path)
204 del _icon_path
206 class Window(g.Window):
207 """This works in exactly the same way as a GtkWindow, except that
208 it calls the toplevel_(un)ref functions for you automatically,
209 and sets the window icon to <app_dir>/.DirIcon if it exists."""
210 def __init__(*args, **kwargs):
211 apply(g.Window.__init__, args, kwargs)
212 toplevel_ref()
213 args[0].connect('destroy', toplevel_unref)
215 if _window_icon:
216 args[0].set_icon(_window_icon)
218 class Dialog(g.Dialog):
219 """This works in exactly the same way as a GtkDialog, except that
220 it calls the toplevel_(un)ref functions for you automatically."""
221 def __init__(*args, **kwargs):
222 apply(g.Dialog.__init__, args, kwargs)
223 toplevel_ref()
224 args[0].connect('destroy', toplevel_unref)
226 if hasattr(g, 'StatusIcon'):
227 # Introduced in PyGTK 2.10
229 class StatusIcon(g.StatusIcon):
230 """Wrap GtkStatusIcon to call toplevel_(un)ref functions for
231 you. Calling toplevel_unref isn't automatic, because a
232 GtkStatusIcon is not a GtkWidget.
234 GtkStatusIcon was added in GTK+ 2.10, so you will need
235 pygtk 2.10 or later to use this class. Check by using
237 import rox
238 if hasattr(rox, 'StatusIcon'):
239 ....
241 def __init__(self, add_ref=True, menu=None,
242 show=True,
243 icon_pixbuf=None, icon_name=None,
244 icon_stock=None, icon_file=None):
245 """Initialise the StatusIcon.
247 add_ref - if True (the default) call toplevel_ref() for
248 this icon and toplevel_unref() when removed. Set to
249 False if you want the main loop to finish if only the
250 icon is present and no other windows
251 menu - if not None then this is the menu to show when
252 the popup-menu signal is received. Alternatively
253 add a handler for then popup-menu signal yourself for
254 more sophisticated menus
255 show - True to show them icon initially, False to start
256 with the icon hidden.
257 icon_pixbuf - image (a gdk.pixbuf) to use as an icon
258 icon_name - name of the icon from the current icon
259 theme to use as an icon
260 icon_stock - name of stock icon to use as an icon
261 icon_file - file name of the image to use as an icon
263 The icon used is selected is the first of
264 (icon_pixbuf, icon_name, icon_stock, icon_file) not
265 to be None. If no icon is given, it is taken from
266 $APP_DIR/.DirIcon, scaled to 22 pixels.
268 NOTE: even if show is set to True, the icon may not
269 be visible if no system tray application is running.
272 g.StatusIcon.__init__(self)
274 if icon_pixbuf:
275 self.set_from_pixbuf(icon_pixbuf)
277 elif icon_name:
278 self.set_from_icon_name(icon_name)
280 elif icon_stock:
281 self.set_from_stock(icon_stock)
283 elif icon_file:
284 self.set_from_file(icon_file)
286 else:
287 icon_path=os.path.join(app_dir, '.DirIcon')
288 if os.path.exists(icon_path):
289 pbuf=g.gdk.pixbuf_new_from_file_at_size(icon_path, 22, 22)
290 self.set_from_pixbuf(pbuf)
292 self.add_ref=add_ref
293 self.icon_menu=menu
295 if show:
296 self.set_visible(True)
298 if self.add_ref:
299 toplevel_ref()
301 if self.icon_menu:
302 self.connect('popup-menu', self.popup_menu)
304 def popup_menu(self, icon, button, act_time):
305 """Show the default menu, if one was specified
306 in the constructor."""
307 def pos_menu(menu):
308 return g.status_icon_position_menu(menu, self)
309 if self.icon_menu:
310 self.icon_menu.popup(self, None, pos_menu)
312 def remove_icon(self):
313 """Hides the icon and drops the top level reference,
314 if it was holding one. This may cause the main loop
315 to exit."""
316 # Does not seem to be a way of removing it...
317 self.set_visible(False)
318 if self.add_ref:
319 toplevel_unref()
320 self.add_ref=False
323 class ButtonMixed(g.Button):
324 """A button with a standard stock icon, but any label. This is useful
325 when you want to express a concept similar to one of the stock ones."""
326 def __init__(self, stock, message):
327 """Specify the icon and text for the new button. The text
328 may specify the mnemonic for the widget by putting a _ before
329 the letter, eg:
330 button = ButtonMixed(g.STOCK_DELETE, '_Delete message')."""
331 g.Button.__init__(self)
333 label = g.Label('')
334 label.set_text_with_mnemonic(message)
335 label.set_mnemonic_widget(self)
337 image = g.image_new_from_stock(stock, g.ICON_SIZE_BUTTON)
338 box = g.HBox(FALSE, 2)
339 align = g.Alignment(0.5, 0.5, 0.0, 0.0)
341 box.pack_start(image, FALSE, FALSE, 0)
342 box.pack_end(label, FALSE, FALSE, 0)
344 self.add(align)
345 align.add(box)
346 align.show_all()
348 _toplevel_windows = 0
349 _in_mainloops = 0
350 def mainloop():
351 """This is a wrapper around the gtk2.mainloop function. It only runs
352 the loop if there are top level references, and exits when
353 rox.toplevel_unref() reduces the count to zero."""
354 global _toplevel_windows, _in_mainloops
356 _in_mainloops = _in_mainloops + 1 # Python1.5 syntax
357 try:
358 while _toplevel_windows:
359 g.main()
360 finally:
361 _in_mainloops = _in_mainloops - 1
363 def toplevel_ref():
364 """Increment the toplevel ref count. rox.mainloop() won't exit until
365 toplevel_unref() is called the same number of times."""
366 global _toplevel_windows
367 _toplevel_windows = _toplevel_windows + 1
369 def toplevel_unref(*unused):
370 """Decrement the toplevel ref count. If this is called while in
371 rox.mainloop() and the count has reached zero, then rox.mainloop()
372 will exit. Ignores any arguments passed in, so you can use it
373 easily as a callback function."""
374 global _toplevel_windows
375 assert _toplevel_windows > 0
376 _toplevel_windows = _toplevel_windows - 1
377 if _toplevel_windows == 0 and _in_mainloops:
378 g.main_quit()
380 _host_name = None
381 def our_host_name():
382 """Try to return the canonical name for this computer. This is used
383 in the drag-and-drop protocol to work out whether a drop is coming from
384 a remote machine (and therefore has to be fetched differently)."""
385 from socket import getfqdn
386 global _host_name
387 if _host_name:
388 return _host_name
389 try:
390 _host_name = getfqdn()
391 except:
392 _host_name = 'localhost'
393 alert("ROX-Lib socket.getfqdn() failed!")
394 return _host_name
396 def escape(uri):
397 "Convert each space to %20, etc"
398 import re
399 return re.sub('[^-:_./a-zA-Z0-9]',
400 lambda match: '%%%02x' % ord(match.group(0)),
401 _to_utf8(uri)[0])
403 def unescape(uri):
404 "Convert each %20 to a space, etc"
405 if '%' not in uri: return uri
406 import re
407 return re.sub('%[0-9a-fA-F][0-9a-fA-F]',
408 lambda match: chr(int(match.group(0)[1:], 16)),
409 uri)
411 def get_local_path(uri):
412 """Convert 'uri' to a local path and return, if possible. If 'uri'
413 is a resource on a remote machine, return None. URI is in the escaped form
414 (%20 for space)."""
415 if not uri:
416 return None
418 if uri[0] == '/':
419 if uri[1:2] != '/':
420 return unescape(uri) # A normal Unix pathname
421 i = uri.find('/', 2)
422 if i == -1:
423 return None # //something
424 if i == 2:
425 return unescape(uri[2:]) # ///path
426 remote_host = uri[2:i]
427 if remote_host == our_host_name():
428 return unescape(uri[i:]) # //localhost/path
429 # //otherhost/path
430 elif uri[:5].lower() == 'file:':
431 if uri[5:6] == '/':
432 return get_local_path(uri[5:])
433 elif uri[:2] == './' or uri[:3] == '../':
434 return unescape(uri)
435 return None
437 app_options = None
438 def setup_app_options(program, leaf = 'Options.xml', site = None):
439 """Most applications only have one set of options. This function can be
440 used to set up the default group. 'program' is the name of the
441 directory to use and 'leaf' is the name of the file used to store the
442 group. You can refer to the group using rox.app_options.
444 If site is given, the basedir module is used for saving options (the
445 new system). Otherwise, the deprecated choices module is used.
447 See rox.options.OptionGroup."""
448 global app_options
449 assert not app_options
450 from options import OptionGroup
451 app_options = OptionGroup(program, leaf, site)
453 _options_box = None
454 def edit_options(options_file = None):
455 """Edit the app_options (set using setup_app_options()) using the GUI
456 specified in 'options_file' (default <app_dir>/Options.xml).
457 If this function is called again while the box is still open, the
458 old box will be redisplayed to the user."""
459 assert app_options
461 global _options_box
462 if _options_box:
463 _options_box.present()
464 return
466 if not options_file:
467 options_file = os.path.join(app_dir, 'Options.xml')
469 import OptionsBox
470 _options_box = OptionsBox.OptionsBox(app_options, options_file)
472 def closed(widget):
473 global _options_box
474 assert _options_box == widget
475 _options_box = None
476 _options_box.connect('destroy', closed)
477 _options_box.open()
479 def isappdir(path):
480 """Return True if the path refers to a valid ROX AppDir.
481 The tests are:
482 - path is a directory
483 - path is not world writable
484 - path contains an executable AppRun
485 - path/AppRun is not world writable
486 - path and path/AppRun are owned by the same user."""
488 if not os.path.isdir(path):
489 return False
490 run=os.path.join(path, 'AppRun')
491 if not os.path.isfile(run) and not os.path.islink(run):
492 return False
493 try:
494 spath=os.stat(path)
495 srun=os.stat(run)
496 except OSError:
497 return False
499 if not os.access(run, os.X_OK):
500 return False
502 if spath.st_mode & os.path.stat.S_IWOTH:
503 return False
505 if srun.st_mode & os.path.stat.S_IWOTH:
506 return False
508 return spath.st_uid==srun.st_uid
510 def get_icon(path):
511 """Looks up an icon for the file named by path, in the order below, using the first
512 found:
513 1. The Filer's globicons file (not implemented)
514 2. A directory's .DirIcon file
515 3. A file in ~/.thumbnails whose name is the md5 hash of os.path.abspath(path), suffixed with '.png'
516 4. A file in $XDG_CONFIG_HOME/rox.sourceforge.net/MIME-Icons for the full type of the file.
517 5. An icon of the form 'gnome-mime-media-subtype' in the current GTK icon theme.
518 6. A file in $XDG_CONFIG_HOME/rox.sourceforge.net/MIME-Icons for the 'media' part of the file's type (eg, 'text')
519 7. An icon of the form 'gnome-mime-media' in the current icon theme.
521 Returns a gtk.gdk.Pixbuf instance for the chosen icon.
524 # Load globicons and examine here...
526 if os.path.isdir(path):
527 dir_icon=os.path.join(path, '.DirIcon')
528 if os.access(dir_icon, os.R_OK):
529 # Check it is safe
530 import stat
532 d=os.stat(path)
533 i=os.stat(dir_icon)
535 if d.st_uid==i.st_uid and not (stat.IWOTH & d.st_mode) and not (stat.IWOTH & i.st_mode):
536 return g.gdk.pixbuf_new_from_file(dir_icon)
538 import thumbnail
539 pixbuf=thumbnail.get_image(path)
540 if pixbuf:
541 return pixbuf
543 import mime
544 mimetype = mime.get_type(path)
545 if mimetype:
546 return mimetype.get_icon()
548 try:
549 import xml
550 except:
551 alert(_("You do not have the Python 'xml' module installed, which "
552 "ROX-Lib2 requires. You need to install python-xmlbase "
553 "(this is a small package; the full PyXML package is not "
554 "required)."))
556 if g.pygtk_version[:2] == (1, 99) and g.pygtk_version[2] < 12:
557 # 1.99.12 is really too old too, but RH8.0 uses it so we'll have
558 # to work around any problems...
559 sys.stderr.write('Your version of pygtk (%d.%d.%d) is too old. '
560 'Things might not work correctly.' % g.pygtk_version)