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