Moved into a sub-dir, so that a svn checkout has the same structure as
[rox-lib/lack.git] / ROX-Lib2 / python / rox / __init__.py
blob6148bbe685d46940250ab4b82ff97dda21d7a545
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
84 # Put argv back the way it was, now that Gtk has initialised
85 sys.argv[0] = _path
86 if _have_stdin and '-' not in sys.argv:
87 sys.argv.append('-')
89 def _warn_old_findrox():
90 try:
91 import findrox
92 except:
93 return # Don't worry too much if it's missing
94 if not hasattr(findrox, 'version'):
95 print >>sys.stderr, "WARNING from ROX-Lib: the version of " \
96 "findrox.py used by this application (%s) is very " \
97 "old and may cause problems." % app_dir
98 _warn_old_findrox()
100 import warnings as _warnings
101 def _stdout_warn(message, category, filename, lineno, file = None,
102 showwarning = _warnings.showwarning):
103 if file is None: file = sys.stdout
104 showwarning(message, category, filename, lineno, file)
105 _warnings.showwarning = _stdout_warn
107 # For backwards compatibility. Use True and False in new code.
108 TRUE = True
109 FALSE = False
111 class UserAbort(Exception):
112 """Raised when the user aborts an operation, eg by clicking on Cancel
113 or pressing Escape."""
114 def __init__(self, message = None):
115 Exception.__init__(self,
116 message or _("Operation aborted at user's request"))
118 def alert(message):
119 "Display message in an error box. Return when the user closes the box."
120 toplevel_ref()
121 box = g.MessageDialog(None, 0, g.MESSAGE_ERROR, g.BUTTONS_OK, message)
122 box.set_position(g.WIN_POS_CENTER)
123 box.set_title(_('Error'))
124 box.run()
125 box.destroy()
126 toplevel_unref()
128 def bug(message = "A bug has been detected in this program. Please report "
129 "the problem to the authors."):
130 "Display an error message and offer a debugging prompt."
131 try:
132 raise Exception(message)
133 except:
134 type, value, tb = sys.exc_info()
135 import debug
136 debug.show_exception(type, value, tb, auto_details = True)
138 def croak(message):
139 """Display message in an error box, then quit the program, returning
140 with a non-zero exit status."""
141 alert(message)
142 sys.exit(1)
144 def info(message):
145 "Display informational message. Returns when the user closes the box."
146 toplevel_ref()
147 box = g.MessageDialog(None, 0, g.MESSAGE_INFO, g.BUTTONS_OK, message)
148 box.set_position(g.WIN_POS_CENTER)
149 box.set_title(_('Information'))
150 box.run()
151 box.destroy()
152 toplevel_unref()
154 def confirm(message, stock_icon, action = None):
155 """Display a <Cancel>/<Action> dialog. Result is true if the user
156 chooses the action, false otherwise. If action is given then that
157 is used as the text instead of the default for the stock item. Eg:
158 if rox.confirm('Really delete everything?', g.STOCK_DELETE): delete()
160 toplevel_ref()
161 box = g.MessageDialog(None, 0, g.MESSAGE_QUESTION,
162 g.BUTTONS_CANCEL, message)
163 if action:
164 button = ButtonMixed(stock_icon, action)
165 else:
166 button = g.Button(stock = stock_icon)
167 button.set_flags(g.CAN_DEFAULT)
168 button.show()
169 box.add_action_widget(button, g.RESPONSE_OK)
170 box.set_position(g.WIN_POS_CENTER)
171 box.set_title(_('Confirm:'))
172 box.set_default_response(g.RESPONSE_OK)
173 resp = box.run()
174 box.destroy()
175 toplevel_unref()
176 return resp == int(g.RESPONSE_OK)
178 def report_exception():
179 """Display the current python exception in an error box, returning
180 when the user closes the box. This is useful in the 'except' clause
181 of a 'try' block. Uses rox.debug.show_exception()."""
182 type, value, tb = sys.exc_info()
183 _excepthook(type, value, tb)
185 def _excepthook(type, value, tb):
186 _old_excepthook(type, value, tb)
187 if issubclass(type, KeyboardInterrupt): return
188 import debug
189 debug.show_exception(type, value, tb)
191 _old_excepthook = sys.excepthook
192 sys.excepthook = _excepthook
194 _icon_path = os.path.join(app_dir, '.DirIcon')
195 _window_icon = None
196 if os.path.exists(_icon_path):
197 try:
198 g.window_set_default_icon_list(g.gdk.pixbuf_new_from_file(_icon_path))
199 except:
200 # Older pygtk
201 _window_icon = g.gdk.pixbuf_new_from_file(_icon_path)
202 del _icon_path
204 class Window(g.Window):
205 """This works in exactly the same way as a GtkWindow, except that
206 it calls the toplevel_(un)ref functions for you automatically,
207 and sets the window icon to <app_dir>/.DirIcon if it exists."""
208 def __init__(*args, **kwargs):
209 apply(g.Window.__init__, args, kwargs)
210 toplevel_ref()
211 args[0].connect('destroy', toplevel_unref)
213 if _window_icon:
214 args[0].set_icon(_window_icon)
216 class Dialog(g.Dialog):
217 """This works in exactly the same way as a GtkDialog, except that
218 it calls the toplevel_(un)ref functions for you automatically."""
219 def __init__(*args, **kwargs):
220 apply(g.Dialog.__init__, args, kwargs)
221 toplevel_ref()
222 args[0].connect('destroy', toplevel_unref)
224 class ButtonMixed(g.Button):
225 """A button with a standard stock icon, but any label. This is useful
226 when you want to express a concept similar to one of the stock ones."""
227 def __init__(self, stock, message):
228 """Specify the icon and text for the new button. The text
229 may specify the mnemonic for the widget by putting a _ before
230 the letter, eg:
231 button = ButtonMixed(g.STOCK_DELETE, '_Delete message')."""
232 g.Button.__init__(self)
234 label = g.Label('')
235 label.set_text_with_mnemonic(message)
236 label.set_mnemonic_widget(self)
238 image = g.image_new_from_stock(stock, g.ICON_SIZE_BUTTON)
239 box = g.HBox(FALSE, 2)
240 align = g.Alignment(0.5, 0.5, 0.0, 0.0)
242 box.pack_start(image, FALSE, FALSE, 0)
243 box.pack_end(label, FALSE, FALSE, 0)
245 self.add(align)
246 align.add(box)
247 align.show_all()
249 _toplevel_windows = 0
250 _in_mainloops = 0
251 def mainloop():
252 """This is a wrapper around the gtk2.mainloop function. It only runs
253 the loop if there are top level references, and exits when
254 rox.toplevel_unref() reduces the count to zero."""
255 global _toplevel_windows, _in_mainloops
257 _in_mainloops = _in_mainloops + 1 # Python1.5 syntax
258 try:
259 while _toplevel_windows:
260 g.main()
261 finally:
262 _in_mainloops = _in_mainloops - 1
264 def toplevel_ref():
265 """Increment the toplevel ref count. rox.mainloop() won't exit until
266 toplevel_unref() is called the same number of times."""
267 global _toplevel_windows
268 _toplevel_windows = _toplevel_windows + 1
270 def toplevel_unref(*unused):
271 """Decrement the toplevel ref count. If this is called while in
272 rox.mainloop() and the count has reached zero, then rox.mainloop()
273 will exit. Ignores any arguments passed in, so you can use it
274 easily as a callback function."""
275 global _toplevel_windows
276 assert _toplevel_windows > 0
277 _toplevel_windows = _toplevel_windows - 1
278 if _toplevel_windows == 0 and _in_mainloops:
279 g.main_quit()
281 _host_name = None
282 def our_host_name():
283 """Try to return the canonical name for this computer. This is used
284 in the drag-and-drop protocol to work out whether a drop is coming from
285 a remote machine (and therefore has to be fetched differently)."""
286 from socket import getfqdn
287 global _host_name
288 if _host_name:
289 return _host_name
290 try:
291 _host_name = getfqdn()
292 except:
293 _host_name = 'localhost'
294 alert("ROX-Lib socket.getfqdn() failed!")
295 return _host_name
297 def escape(uri):
298 "Convert each space to %20, etc"
299 import re
300 return re.sub('[^:-_./a-zA-Z0-9]',
301 lambda match: '%%%02x' % ord(match.group(0)),
302 _to_utf8(uri)[0])
304 def unescape(uri):
305 "Convert each %20 to a space, etc"
306 if '%' not in uri: return uri
307 import re
308 return re.sub('%[0-9a-fA-F][0-9a-fA-F]',
309 lambda match: chr(int(match.group(0)[1:], 16)),
310 uri)
312 def get_local_path(uri):
313 """Convert 'uri' to a local path and return, if possible. If 'uri'
314 is a resource on a remote machine, return None. URI is in the escaped form
315 (%20 for space)."""
316 if not uri:
317 return None
319 if uri[0] == '/':
320 if uri[1:2] != '/':
321 return unescape(uri) # A normal Unix pathname
322 i = uri.find('/', 2)
323 if i == -1:
324 return None # //something
325 if i == 2:
326 return unescape(uri[2:]) # ///path
327 remote_host = uri[2:i]
328 if remote_host == our_host_name():
329 return unescape(uri[i:]) # //localhost/path
330 # //otherhost/path
331 elif uri[:5].lower() == 'file:':
332 if uri[5:6] == '/':
333 return get_local_path(uri[5:])
334 elif uri[:2] == './' or uri[:3] == '../':
335 return unescape(uri)
336 return None
338 app_options = None
339 def setup_app_options(program, leaf = 'Options.xml', site = None):
340 """Most applications only have one set of options. This function can be
341 used to set up the default group. 'program' is the name of the
342 directory to use and 'leaf' is the name of the file used to store the
343 group. You can refer to the group using rox.app_options.
345 If site is given, the basedir module is used for saving options (the
346 new system). Otherwise, the deprecated choices module is used.
348 See rox.options.OptionGroup."""
349 global app_options
350 assert not app_options
351 from options import OptionGroup
352 app_options = OptionGroup(program, leaf, site)
354 _options_box = None
355 def edit_options(options_file = None):
356 """Edit the app_options (set using setup_app_options()) using the GUI
357 specified in 'options_file' (default <app_dir>/Options.xml).
358 If this function is called again while the box is still open, the
359 old box will be redisplayed to the user."""
360 assert app_options
362 global _options_box
363 if _options_box:
364 _options_box.present()
365 return
367 if not options_file:
368 options_file = os.path.join(app_dir, 'Options.xml')
370 import OptionsBox
371 _options_box = OptionsBox.OptionsBox(app_options, options_file)
373 def closed(widget):
374 global _options_box
375 assert _options_box == widget
376 _options_box = None
377 _options_box.connect('destroy', closed)
378 _options_box.open()
380 def isappdir(path):
381 """Return True if the path refers to a valid ROX AppDir.
382 The tests are:
383 - path is a directory
384 - path is not world writable
385 - path contains an executable AppRun
386 - path/AppRun is not world writable
387 - path and path/AppRun are owned by the same user."""
389 if not os.path.isdir(path):
390 return False
391 run=os.path.join(path, 'AppRun')
392 if not os.path.isfile(run) and not os.path.islink(run):
393 return False
394 try:
395 spath=os.stat(path)
396 srun=os.stat(run)
397 except OSError:
398 return False
400 if not os.access(run, os.X_OK):
401 return False
403 if spath.st_mode & os.path.stat.S_IWOTH:
404 return False
406 if srun.st_mode & os.path.stat.S_IWOTH:
407 return False
409 return spath.st_uid==srun.st_uid
411 try:
412 import xml
413 except:
414 alert(_("You do not have the Python 'xml' module installed, which "
415 "ROX-Lib2 requires. You need to install python-xmlbase "
416 "(this is a small package; the full PyXML package is not "
417 "required)."))
419 if g.pygtk_version[:2] == (1, 99) and g.pygtk_version[2] < 12:
420 # 1.99.12 is really too old too, but RH8.0 uses it so we'll have
421 # to work around any problems...
422 sys.stderr.write('Your version of pygtk (%d.%d.%d) is too old. '
423 'Things might not work correctly.' % g.pygtk_version)