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