Use gdk.display_get_default() as gdk.get_display() is 2.8 or later (Stephen Watson).
[rox-lib.git] / ROX-Lib2 / python / rox / __init__.py
blob0d01c1e376d19b5aab5c39211d0790293a23b587
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, 3)
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
84 if not have_display:
85 sys.stderr.write(_("WARNING from ROX-Lib: This does not appear to be a valid X environment (DISPLAY is not set), many functions will not work and may cause a segmentation fault.")+"\n")
87 # Put argv back the way it was, now that Gtk has initialised
88 sys.argv[0] = _path
89 if _have_stdin and '-' not in sys.argv:
90 sys.argv.append('-')
92 def _warn_old_findrox():
93 try:
94 import findrox
95 except:
96 return # Don't worry too much if it's missing
97 if not hasattr(findrox, 'version'):
98 print >>sys.stderr, _("WARNING from ROX-Lib: the version of " \
99 "findrox.py used by this application (%s) is very " \
100 "old and may cause problems.") % app_dir
101 _warn_old_findrox()
103 import warnings as _warnings
104 def _stdout_warn(message, category, filename, lineno, file = None,
105 showwarning = _warnings.showwarning):
106 if file is None: file = sys.stdout
107 showwarning(message, category, filename, lineno, file)
108 _warnings.showwarning = _stdout_warn
110 # For backwards compatibility. Use True and False in new code.
111 TRUE = True
112 FALSE = False
114 class UserAbort(Exception):
115 """Raised when the user aborts an operation, eg by clicking on Cancel
116 or pressing Escape."""
117 def __init__(self, message = None):
118 Exception.__init__(self,
119 message or _("Operation aborted at user's request"))
121 def alert(message):
122 "Display message in an error box. Return when the user closes the box."
123 toplevel_ref()
124 box = g.MessageDialog(None, 0, g.MESSAGE_ERROR, g.BUTTONS_OK, message)
125 box.set_position(g.WIN_POS_CENTER)
126 box.set_title(_('Error'))
127 box.run()
128 box.destroy()
129 toplevel_unref()
131 def bug(message = "A bug has been detected in this program. Please report "
132 "the problem to the authors."):
133 "Display an error message and offer a debugging prompt."
134 try:
135 raise Exception(message)
136 except:
137 type, value, tb = sys.exc_info()
138 import debug
139 debug.show_exception(type, value, tb, auto_details = True)
141 def croak(message):
142 """Display message in an error box, then quit the program, returning
143 with a non-zero exit status."""
144 alert(message)
145 sys.exit(1)
147 def info(message):
148 "Display informational message. Returns when the user closes the box."
149 toplevel_ref()
150 box = g.MessageDialog(None, 0, g.MESSAGE_INFO, g.BUTTONS_OK, message)
151 box.set_position(g.WIN_POS_CENTER)
152 box.set_title(_('Information'))
153 box.run()
154 box.destroy()
155 toplevel_unref()
157 def confirm(message, stock_icon, action = None):
158 """Display a <Cancel>/<Action> dialog. Result is true if the user
159 chooses the action, false otherwise. If action is given then that
160 is used as the text instead of the default for the stock item. Eg:
161 if rox.confirm('Really delete everything?', g.STOCK_DELETE): delete()
163 toplevel_ref()
164 box = g.MessageDialog(None, 0, g.MESSAGE_QUESTION,
165 g.BUTTONS_CANCEL, message)
166 if action:
167 button = ButtonMixed(stock_icon, action)
168 else:
169 button = g.Button(stock = stock_icon)
170 button.set_flags(g.CAN_DEFAULT)
171 button.show()
172 box.add_action_widget(button, g.RESPONSE_OK)
173 box.set_position(g.WIN_POS_CENTER)
174 box.set_title(_('Confirm:'))
175 box.set_default_response(g.RESPONSE_OK)
176 resp = box.run()
177 box.destroy()
178 toplevel_unref()
179 return resp == int(g.RESPONSE_OK)
181 def report_exception():
182 """Display the current python exception in an error box, returning
183 when the user closes the box. This is useful in the 'except' clause
184 of a 'try' block. Uses rox.debug.show_exception()."""
185 type, value, tb = sys.exc_info()
186 _excepthook(type, value, tb)
188 def _excepthook(ex_type, value, tb):
189 _old_excepthook(ex_type, value, tb)
190 if type(ex_type) == type and issubclass(ex_type, KeyboardInterrupt): return
191 if have_display:
192 import debug
193 debug.show_exception(ex_type, value, tb)
195 _old_excepthook = sys.excepthook
196 sys.excepthook = _excepthook
198 _icon_path = os.path.join(app_dir, '.DirIcon')
199 _window_icon = None
200 if os.path.exists(_icon_path):
201 try:
202 g.window_set_default_icon_list(g.gdk.pixbuf_new_from_file(_icon_path))
203 except:
204 # Older pygtk
205 _window_icon = g.gdk.pixbuf_new_from_file(_icon_path)
206 del _icon_path
208 class Window(g.Window):
209 """This works in exactly the same way as a GtkWindow, except that
210 it calls the toplevel_(un)ref functions for you automatically,
211 and sets the window icon to <app_dir>/.DirIcon if it exists."""
212 def __init__(*args, **kwargs):
213 apply(g.Window.__init__, args, kwargs)
214 toplevel_ref()
215 args[0].connect('destroy', toplevel_unref)
217 if _window_icon:
218 args[0].set_icon(_window_icon)
220 class Dialog(g.Dialog):
221 """This works in exactly the same way as a GtkDialog, except that
222 it calls the toplevel_(un)ref functions for you automatically."""
223 def __init__(*args, **kwargs):
224 apply(g.Dialog.__init__, args, kwargs)
225 toplevel_ref()
226 args[0].connect('destroy', toplevel_unref)
228 class ButtonMixed(g.Button):
229 """A button with a standard stock icon, but any label. This is useful
230 when you want to express a concept similar to one of the stock ones."""
231 def __init__(self, stock, message):
232 """Specify the icon and text for the new button. The text
233 may specify the mnemonic for the widget by putting a _ before
234 the letter, eg:
235 button = ButtonMixed(g.STOCK_DELETE, '_Delete message')."""
236 g.Button.__init__(self)
238 label = g.Label('')
239 label.set_text_with_mnemonic(message)
240 label.set_mnemonic_widget(self)
242 image = g.image_new_from_stock(stock, g.ICON_SIZE_BUTTON)
243 box = g.HBox(FALSE, 2)
244 align = g.Alignment(0.5, 0.5, 0.0, 0.0)
246 box.pack_start(image, FALSE, FALSE, 0)
247 box.pack_end(label, FALSE, FALSE, 0)
249 self.add(align)
250 align.add(box)
251 align.show_all()
253 _toplevel_windows = 0
254 _in_mainloops = 0
255 def mainloop():
256 """This is a wrapper around the gtk2.mainloop function. It only runs
257 the loop if there are top level references, and exits when
258 rox.toplevel_unref() reduces the count to zero."""
259 global _toplevel_windows, _in_mainloops
261 _in_mainloops = _in_mainloops + 1 # Python1.5 syntax
262 try:
263 while _toplevel_windows:
264 g.main()
265 finally:
266 _in_mainloops = _in_mainloops - 1
268 def toplevel_ref():
269 """Increment the toplevel ref count. rox.mainloop() won't exit until
270 toplevel_unref() is called the same number of times."""
271 global _toplevel_windows
272 _toplevel_windows = _toplevel_windows + 1
274 def toplevel_unref(*unused):
275 """Decrement the toplevel ref count. If this is called while in
276 rox.mainloop() and the count has reached zero, then rox.mainloop()
277 will exit. Ignores any arguments passed in, so you can use it
278 easily as a callback function."""
279 global _toplevel_windows
280 assert _toplevel_windows > 0
281 _toplevel_windows = _toplevel_windows - 1
282 if _toplevel_windows == 0 and _in_mainloops:
283 g.main_quit()
285 _host_name = None
286 def our_host_name():
287 """Try to return the canonical name for this computer. This is used
288 in the drag-and-drop protocol to work out whether a drop is coming from
289 a remote machine (and therefore has to be fetched differently)."""
290 from socket import getfqdn
291 global _host_name
292 if _host_name:
293 return _host_name
294 try:
295 _host_name = getfqdn()
296 except:
297 _host_name = 'localhost'
298 alert("ROX-Lib socket.getfqdn() failed!")
299 return _host_name
301 def escape(uri):
302 "Convert each space to %20, etc"
303 import re
304 return re.sub('[^-:_./a-zA-Z0-9]',
305 lambda match: '%%%02x' % ord(match.group(0)),
306 _to_utf8(uri)[0])
308 def unescape(uri):
309 "Convert each %20 to a space, etc"
310 if '%' not in uri: return uri
311 import re
312 return re.sub('%[0-9a-fA-F][0-9a-fA-F]',
313 lambda match: chr(int(match.group(0)[1:], 16)),
314 uri)
316 def get_local_path(uri):
317 """Convert 'uri' to a local path and return, if possible. If 'uri'
318 is a resource on a remote machine, return None. URI is in the escaped form
319 (%20 for space)."""
320 if not uri:
321 return None
323 if uri[0] == '/':
324 if uri[1:2] != '/':
325 return unescape(uri) # A normal Unix pathname
326 i = uri.find('/', 2)
327 if i == -1:
328 return None # //something
329 if i == 2:
330 return unescape(uri[2:]) # ///path
331 remote_host = uri[2:i]
332 if remote_host == our_host_name():
333 return unescape(uri[i:]) # //localhost/path
334 # //otherhost/path
335 elif uri[:5].lower() == 'file:':
336 if uri[5:6] == '/':
337 return get_local_path(uri[5:])
338 elif uri[:2] == './' or uri[:3] == '../':
339 return unescape(uri)
340 return None
342 app_options = None
343 def setup_app_options(program, leaf = 'Options.xml', site = None):
344 """Most applications only have one set of options. This function can be
345 used to set up the default group. 'program' is the name of the
346 directory to use and 'leaf' is the name of the file used to store the
347 group. You can refer to the group using rox.app_options.
349 If site is given, the basedir module is used for saving options (the
350 new system). Otherwise, the deprecated choices module is used.
352 See rox.options.OptionGroup."""
353 global app_options
354 assert not app_options
355 from options import OptionGroup
356 app_options = OptionGroup(program, leaf, site)
358 _options_box = None
359 def edit_options(options_file = None):
360 """Edit the app_options (set using setup_app_options()) using the GUI
361 specified in 'options_file' (default <app_dir>/Options.xml).
362 If this function is called again while the box is still open, the
363 old box will be redisplayed to the user."""
364 assert app_options
366 global _options_box
367 if _options_box:
368 _options_box.present()
369 return
371 if not options_file:
372 options_file = os.path.join(app_dir, 'Options.xml')
374 import OptionsBox
375 _options_box = OptionsBox.OptionsBox(app_options, options_file)
377 def closed(widget):
378 global _options_box
379 assert _options_box == widget
380 _options_box = None
381 _options_box.connect('destroy', closed)
382 _options_box.open()
384 def isappdir(path):
385 """Return True if the path refers to a valid ROX AppDir.
386 The tests are:
387 - path is a directory
388 - path is not world writable
389 - path contains an executable AppRun
390 - path/AppRun is not world writable
391 - path and path/AppRun are owned by the same user."""
393 if not os.path.isdir(path):
394 return False
395 run=os.path.join(path, 'AppRun')
396 if not os.path.isfile(run) and not os.path.islink(run):
397 return False
398 try:
399 spath=os.stat(path)
400 srun=os.stat(run)
401 except OSError:
402 return False
404 if not os.access(run, os.X_OK):
405 return False
407 if spath.st_mode & os.path.stat.S_IWOTH:
408 return False
410 if srun.st_mode & os.path.stat.S_IWOTH:
411 return False
413 return spath.st_uid==srun.st_uid
415 def get_icon(path):
416 """Looks up an icon for the file named by path, in the order below, using the first
417 found:
418 1. The Filer's globicons file (not implemented)
419 2. A directory's .DirIcon file
420 3. A file in ~/.thumbnails whose name is the md5 hash of os.path.abspath(path), suffixed with '.png'
421 4. A file in $XDG_CONFIG_HOME/rox.sourceforge.net/MIME-Icons for the full type of the file.
422 5. An icon of the form 'gnome-mime-media-subtype' in the current GTK icon theme.
423 6. A file in $XDG_CONFIG_HOME/rox.sourceforge.net/MIME-Icons for the 'media' part of the file's type (eg, 'text')
424 7. An icon of the form 'gnome-mime-media' in the current icon theme.
426 Returns a gtk.gdk.Pixbuf instance for the chosen icon.
429 # Load globicons and examine here...
431 if os.path.isdir(path):
432 dir_icon=os.path.join(path, '.DirIcon')
433 if os.access(dir_icon, os.R_OK):
434 # Check it is safe
435 import stat
437 d=os.stat(path)
438 i=os.stat(dir_icon)
440 if d.st_uid==i.st_uid and not (stat.IWOTH & d.st_mode) and not (stat.IWOTH & i.st_mode):
441 return g.gdk.pixbuf_new_from_file(dir_icon)
443 import thumbnail
444 pixbuf=thumbnail.get_image(path)
445 if pixbuf:
446 return pixbuf
448 import mime
449 mimetype = mime.get_type(path)
450 if mimetype:
451 return mimetype.get_icon()
453 try:
454 import xml
455 except:
456 alert(_("You do not have the Python 'xml' module installed, which "
457 "ROX-Lib2 requires. You need to install python-xmlbase "
458 "(this is a small package; the full PyXML package is not "
459 "required)."))
461 if g.pygtk_version[:2] == (1, 99) and g.pygtk_version[2] < 12:
462 # 1.99.12 is really too old too, but RH8.0 uses it so we'll have
463 # to work around any problems...
464 sys.stderr.write('Your version of pygtk (%d.%d.%d) is too old. '
465 'Things might not work correctly.' % g.pygtk_version)