Try again to get fallback right.
[rox-lib.git] / python / rox / __init__.py
blob5f3cc90324c69c98ac1fa6e4fd0db3873ee8ce0b
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
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
34 roxlib_version = (1, 9, 11)
36 _path = os.path.realpath(sys.argv[0])
37 app_dir = os.path.dirname(_path)
38 if _path.endswith('/AppRun') or _path.endswith('/AppletRun'):
39 sys.argv[0] = os.path.dirname(_path)
41 # In python2.3 there is a bool type. Later versions of 2.2 use ints, but
42 # early versions don't support them at all, so create them here.
43 try:
44 True
45 except:
46 import __builtin__
47 __builtin__.False = 0
48 __builtin__.True = 1
50 try:
51 iter
52 except:
53 sys.stderr.write('Sorry, you need to have python 2.2, and it \n'
54 'must be the default version. You may be able to \n'
55 'change the first line of your program\'s AppRun \n'
56 'file to end \'python2.2\' as a workaround.\n')
57 raise SystemExit(1)
59 import i18n
61 _roxlib_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
62 _ = i18n.translation(os.path.join(_roxlib_dir, 'Messages'))
64 _old_sys_path = sys.path[:]
66 try:
67 zhost = 'zero-install.sourceforge.net'
68 zpath = '/uri/0install/' + zhost
69 if not os.getenv('ROXLIB_DISABLE_ZEROINSTALL') and os.path.exists(zpath):
70 zpath = os.path.join(zpath, 'libs/pygtk-2/platform/latest')
71 if not os.path.exists(zpath):
72 os.system('0refresh ' + zhost)
73 if os.path.exists(zpath):
74 sys.path.insert(0, zpath +
75 '/lib/python%d.%d/site-packages' % sys.version_info[:2])
76 import pygtk; pygtk.require('2.0')
77 import gtk._gtk # If this fails, don't leave a half-inited gtk
78 import gtk; g = gtk # Don't syntax error for python1.5
79 assert g.Window # Ensure not 1.2 bindings
80 except:
81 try:
82 # Try again without Zero Install
83 sys.path = _old_sys_path
84 if 'gtk' in sys.modules:
85 del sys.modules['gtk']
86 try:
87 # Try to support 1.99.12, at least to show an error
88 import pygtk; pygtk.require('2.0')
89 except:
90 print "Warning: very old pygtk!"
91 import gtk; g = gtk # Don't syntax error for python1.5
92 assert g.Window # Ensure not 1.2 bindings
93 except:
94 sys.stderr.write(_('The pygtk2 package (1.99.13 or later) must be '
95 'installed to use this program:\n'
96 'http://rox.sourceforge.net/rox_lib.php3\n'))
97 raise
99 # Put argv back the way it was, now that Gtk has initialised
100 sys.argv[0] = _path
102 TRUE = True
103 FALSE = False
105 def alert(message):
106 "Display message in an error box. Return when the user closes the box."
107 toplevel_ref()
108 box = g.MessageDialog(None, 0, g.MESSAGE_ERROR, g.BUTTONS_OK, message)
109 box.set_position(g.WIN_POS_CENTER)
110 box.set_title(_('Error'))
111 box.run()
112 box.destroy()
113 toplevel_unref()
115 def bug(message = "A bug has been detected in this program. Please report "
116 "the problem to the authors."):
117 "Display an error message and offer a debugging prompt."
118 try:
119 raise Exception(message)
120 except:
121 type, value, tb = sys.exc_info()
122 import debug
123 debug.show_exception(type, value, tb, auto_details = True)
125 def croak(message):
126 """Display message in an error box, then quit the program, returning
127 with a non-zero exit status."""
128 alert(message)
129 sys.exit(1)
131 def info(message):
132 "Display informational message. Returns when the user closes the box."
133 toplevel_ref()
134 box = g.MessageDialog(None, 0, g.MESSAGE_INFO, g.BUTTONS_OK, message)
135 box.set_position(g.WIN_POS_CENTER)
136 box.set_title(_('Information'))
137 box.run()
138 box.destroy()
139 toplevel_unref()
141 def confirm(message, stock_icon, action = None):
142 """Display a <Cancel>/<Action> dialog. Result is true if the user
143 chooses the action, false otherwise. If action is given then that
144 is used as the text instead of the default for the stock item. Eg:
145 if rox.confirm('Really delete everything?', g.STOCK_DELETE): delete()
147 toplevel_ref()
148 box = g.MessageDialog(None, 0, g.MESSAGE_QUESTION,
149 g.BUTTONS_CANCEL, message)
150 if action:
151 button = ButtonMixed(stock_icon, action)
152 else:
153 button = g.Button(stock = stock_icon)
154 button.set_flags(g.CAN_DEFAULT)
155 button.show()
156 box.add_action_widget(button, g.RESPONSE_OK)
157 box.set_position(g.WIN_POS_CENTER)
158 box.set_title(_('Confirm:'))
159 box.set_default_response(g.RESPONSE_OK)
160 resp = box.run()
161 box.destroy()
162 toplevel_unref()
163 return resp == g.RESPONSE_OK
165 def report_exception():
166 """Display the current python exception in an error box, returning
167 when the user closes the box. This is useful in the 'except' clause
168 of a 'try' block. Uses rox.debug.show_exception()."""
169 type, value, tb = sys.exc_info()
170 import debug
171 debug.show_exception(type, value, tb)
173 _icon_path = os.path.join(app_dir, '.DirIcon')
174 _window_icon = None
175 if os.path.exists(_icon_path):
176 try:
177 g.window_set_default_icon_list(g.gdk.pixbuf_new_from_file(_icon_path))
178 except:
179 # Older pygtk
180 _window_icon = g.gdk.pixbuf_new_from_file(_icon_path)
181 del _icon_path
183 class Window(g.Window):
184 """This works in exactly the same way as a GtkWindow, except that
185 it calls the toplevel_(un)ref functions for you automatically,
186 and sets the window icon to <app_dir>/.DirIcon if it exists."""
187 def __init__(*args, **kwargs):
188 apply(g.Window.__init__, args, kwargs)
189 toplevel_ref()
190 args[0].connect('destroy', toplevel_unref)
192 if _window_icon:
193 args[0].set_icon(_window_icon)
195 class Dialog(g.Dialog):
196 """This works in exactly the same way as a GtkDialog, except that
197 it calls the toplevel_(un)ref functions for you automatically."""
198 def __init__(*args, **kwargs):
199 apply(g.Dialog.__init__, args, kwargs)
200 toplevel_ref()
201 args[0].connect('destroy', toplevel_unref)
203 class ButtonMixed(g.Button):
204 """A button with a standard stock icon, but any label. This is useful
205 when you want to express a concept similar to one of the stock ones."""
206 def __init__(self, stock, message):
207 """Specify the icon and text for the new button. The text
208 may specify the mnemonic for the widget by putting a _ before
209 the letter, eg:
210 button = ButtonMixed(g.STOCK_DELETE, '_Delete message')."""
211 g.Button.__init__(self)
213 label = g.Label('')
214 label.set_text_with_mnemonic(message)
215 label.set_mnemonic_widget(self)
217 image = g.image_new_from_stock(stock, g.ICON_SIZE_BUTTON)
218 box = g.HBox(FALSE, 2)
219 align = g.Alignment(0.5, 0.5, 0.0, 0.0)
221 box.pack_start(image, FALSE, FALSE, 0)
222 box.pack_end(label, FALSE, FALSE, 0)
224 self.add(align)
225 align.add(box)
226 align.show_all()
228 _toplevel_windows = 0
229 _in_mainloops = 0
230 def mainloop():
231 """This is a wrapper around the gtk2.mainloop function. It only runs
232 the loop if there are top level references, and exits when
233 rox.toplevel_unref() reduces the count to zero."""
234 global _toplevel_windows, _in_mainloops
236 _in_mainloops = _in_mainloops + 1 # Python1.5 syntax
237 try:
238 while _toplevel_windows:
239 g.mainloop()
240 finally:
241 _in_mainloops = _in_mainloops - 1
243 def toplevel_ref():
244 """Increment the toplevel ref count. rox.mainloop() won't exit until
245 toplevel_unref() is called the same number of times."""
246 global _toplevel_windows
247 _toplevel_windows = _toplevel_windows + 1
249 def toplevel_unref(*unused):
250 """Decrement the toplevel ref count. If this is called while in
251 rox.mainloop() and the count has reached zero, then rox.mainloop()
252 will exit. Ignores any arguments passed in, so you can use it
253 easily as a callback function."""
254 global _toplevel_windows
255 assert _toplevel_windows > 0
256 _toplevel_windows = _toplevel_windows - 1
257 if _toplevel_windows == 0 and _in_mainloops:
258 g.mainquit()
260 _host_name = None
261 def our_host_name():
262 """Try to return the canonical name for this computer. This is used
263 in the drag-and-drop protocol to work out whether a drop is coming from
264 a remote machine (and therefore has to be fetched differently)."""
265 from socket import getfqdn
266 global _host_name
267 if _host_name:
268 return _host_name
269 try:
270 _host_name = getfqdn()
271 except:
272 _host_name = 'localhost'
273 alert("ROX-Lib socket.getfqdn() failed!")
274 return _host_name
276 def get_local_path(uri):
277 """Convert 'uri' to a local path and return, if possible. If 'uri'
278 is a resource on a remote machine, return None."""
279 if not uri:
280 return None
282 if uri[0] == '/':
283 if uri[1:2] != '/':
284 return uri # A normal Unix pathname
285 i = uri.find('/', 2)
286 if i == -1:
287 return None # //something
288 if i == 2:
289 return uri[2:] # ///path
290 remote_host = uri[2:i]
291 if remote_host == our_host_name():
292 return uri[i:] # //localhost/path
293 # //otherhost/path
294 elif uri[:5].lower() == 'file:':
295 if uri[5:6] == '/':
296 return get_local_path(uri[5:])
297 elif uri[:2] == './' or uri[:3] == '../':
298 return uri
299 return None
301 app_options = None
302 def setup_app_options(program, leaf = 'Options.xml'):
303 """Most applications only have one set of options. This function can be
304 used to set up the default group. 'program' is the name of the
305 directory to use in <Choices> and 'leaf' is the name of the file used
306 to store the group. You can refer to the group using rox.app_options.
307 See rox.options.OptionGroup."""
308 global app_options
309 assert not app_options
310 from options import OptionGroup
311 app_options = OptionGroup(program, leaf)
313 _options_box = None
314 def edit_options(options_file = None):
315 """Edit the app_options (set using setup_app_options()) using the GUI
316 specified in 'options_file' (default <app_dir>/Options.xml).
317 If this function is called again while the box is still open, the
318 old box will be redisplayed to the user."""
319 assert app_options
321 global _options_box
322 if _options_box:
323 _options_box.present()
324 return
326 if not options_file:
327 options_file = os.path.join(app_dir, 'Options.xml')
329 import OptionsBox
330 _options_box = OptionsBox.OptionsBox(app_options, options_file)
332 def closed(widget):
333 global _options_box
334 assert _options_box == widget
335 _options_box = None
336 _options_box.connect('destroy', closed)
337 _options_box.open()
339 try:
340 import xml
341 except:
342 alert(_("You do not have the Python 'xml' module installed, which "
343 "ROX-Lib2 requires. You need to install python-xmlbase "
344 "(this is a small package; the full PyXML package is not "
345 "required)."))
347 if g.pygtk_version[:2] == (1, 99) and g.pygtk_version[2] < 12:
348 # 1.99.12 is really too old too, but RH8.0 uses it so we'll have
349 # to work around any problems...
350 sys.stderr.write('Your version of pygtk (%d.%d.%d) is too old. '
351 'Things might not work correctly.' % g.pygtk_version)