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