Style improvements.
[rox-lib.git] / python / rox / __init__.py
blob09fa699a9959bc359909f1f95b539bc0f9edc72d
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, 10)
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 try:
106 raise Exception(message)
107 except:
108 type, value, tb = sys.exc_info()
109 import debug
110 debug.show_exception(type, value, tb, auto_details = True)
112 def croak(message):
113 """Display message in an error box, then quit the program, returning
114 with a non-zero exit status."""
115 alert(message)
116 sys.exit(1)
118 def info(message):
119 "Display informational message. Returns when the user closes the box."
120 toplevel_ref()
121 box = g.MessageDialog(None, 0, g.MESSAGE_INFO, g.BUTTONS_OK, message)
122 box.set_position(g.WIN_POS_CENTER)
123 box.set_title(_('Information'))
124 box.run()
125 box.destroy()
126 toplevel_unref()
128 def confirm(message, stock_icon, action = None):
129 """Display a <Cancel>/<Action> dialog. Result is true if the user
130 chooses the action, false otherwise. If action is given then that
131 is used as the text instead of the default for the stock item. Eg:
132 if rox.confirm('Really delete everything?', g.STOCK_DELETE): delete()
134 toplevel_ref()
135 box = g.MessageDialog(None, 0, g.MESSAGE_QUESTION,
136 g.BUTTONS_CANCEL, message)
137 if action:
138 button = ButtonMixed(stock_icon, action)
139 else:
140 button = g.Button(stock = stock_icon)
141 button.set_flags(g.CAN_DEFAULT)
142 button.show()
143 box.add_action_widget(button, g.RESPONSE_OK)
144 box.set_position(g.WIN_POS_CENTER)
145 box.set_title(_('Confirm:'))
146 box.set_default_response(g.RESPONSE_OK)
147 resp = box.run()
148 box.destroy()
149 toplevel_unref()
150 return resp == g.RESPONSE_OK
152 def report_exception():
153 """Display the current python exception in an error box, returning
154 when the user closes the box. This is useful in the 'except' clause
155 of a 'try' block. Uses rox.debug.show_exception()."""
156 type, value, tb = sys.exc_info()
157 import debug
158 debug.show_exception(type, value, tb)
160 _icon_path = os.path.join(app_dir, '.DirIcon')
161 _window_icon = None
162 if os.path.exists(_icon_path):
163 try:
164 g.window_set_default_icon_list(g.gdk.pixbuf_new_from_file(_icon_path))
165 except:
166 # Older pygtk
167 _window_icon = g.gdk.pixbuf_new_from_file(_icon_path)
168 del _icon_path
170 class Window(g.Window):
171 """This works in exactly the same way as a GtkWindow, except that
172 it calls the toplevel_(un)ref functions for you automatically,
173 and sets the window icon to <app_dir>/.DirIcon if it exists."""
174 def __init__(*args, **kwargs):
175 apply(g.Window.__init__, args, kwargs)
176 toplevel_ref()
177 args[0].connect('destroy', toplevel_unref)
179 if _window_icon:
180 args[0].set_icon(_window_icon)
182 class Dialog(g.Dialog):
183 """This works in exactly the same way as a GtkDialog, except that
184 it calls the toplevel_(un)ref functions for you automatically."""
185 def __init__(*args, **kwargs):
186 apply(g.Dialog.__init__, args, kwargs)
187 toplevel_ref()
188 args[0].connect('destroy', toplevel_unref)
190 class ButtonMixed(g.Button):
191 """A button with a standard stock icon, but any label. This is useful
192 when you want to express a concept similar to one of the stock ones."""
193 def __init__(self, stock, message):
194 """Specify the icon and text for the new button. The text
195 may specify the mnemonic for the widget by putting a _ before
196 the letter, eg:
197 button = ButtonMixed(g.STOCK_DELETE, '_Delete message')."""
198 g.Button.__init__(self)
200 label = g.Label('')
201 label.set_text_with_mnemonic(message)
202 label.set_mnemonic_widget(self)
204 image = g.image_new_from_stock(stock, g.ICON_SIZE_BUTTON)
205 box = g.HBox(FALSE, 2)
206 align = g.Alignment(0.5, 0.5, 0.0, 0.0)
208 box.pack_start(image, FALSE, FALSE, 0)
209 box.pack_end(label, FALSE, FALSE, 0)
211 self.add(align)
212 align.add(box)
213 align.show_all()
215 _toplevel_windows = 0
216 _in_mainloops = 0
217 def mainloop():
218 """This is a wrapper around the gtk2.mainloop function. It only runs
219 the loop if there are top level references, and exits when
220 rox.toplevel_unref() reduces the count to zero."""
221 global _toplevel_windows, _in_mainloops
223 _in_mainloops = _in_mainloops + 1 # Python1.5 syntax
224 try:
225 while _toplevel_windows:
226 g.mainloop()
227 finally:
228 _in_mainloops = _in_mainloops - 1
230 def toplevel_ref():
231 """Increment the toplevel ref count. rox.mainloop() won't exit until
232 toplevel_unref() is called the same number of times."""
233 global _toplevel_windows
234 _toplevel_windows = _toplevel_windows + 1
236 def toplevel_unref(*unused):
237 """Decrement the toplevel ref count. If this is called while in
238 rox.mainloop() and the count has reached zero, then rox.mainloop()
239 will exit. Ignores any arguments passed in, so you can use it
240 easily as a callback function."""
241 global _toplevel_windows
242 assert _toplevel_windows > 0
243 _toplevel_windows = _toplevel_windows - 1
244 if _toplevel_windows == 0 and _in_mainloops:
245 g.mainquit()
247 _host_name = None
248 def our_host_name():
249 """Try to return the canonical name for this computer. This is used
250 in the drag-and-drop protocol to work out whether a drop is coming from
251 a remote machine (and therefore has to be fetched differently)."""
252 from socket import getfqdn
253 global _host_name
254 if _host_name:
255 return _host_name
256 try:
257 _host_name = getfqdn()
258 except:
259 _host_name = 'localhost'
260 alert("ROX-Lib socket.getfqdn() failed!")
261 return _host_name
263 def get_local_path(uri):
264 """Convert 'uri' to a local path and return, if possible. If 'uri'
265 is a resource on a remote machine, return None."""
266 if not uri:
267 return None
269 if uri[0] == '/':
270 if uri[1:2] != '/':
271 return uri # A normal Unix pathname
272 i = uri.find('/', 2)
273 if i == -1:
274 return None # //something
275 if i == 2:
276 return uri[2:] # ///path
277 remote_host = uri[2:i]
278 if remote_host == our_host_name():
279 return uri[i:] # //localhost/path
280 # //otherhost/path
281 elif uri[:5].lower() == 'file:':
282 if uri[5:6] == '/':
283 return get_local_path(uri[5:])
284 elif uri[:2] == './' or uri[:3] == '../':
285 return uri
286 return None
288 app_options = None
289 def setup_app_options(program, leaf = 'Options.xml'):
290 """Most applications only have one set of options. This function can be
291 used to set up the default group. 'program' is the name of the
292 directory to use in <Choices> and 'leaf' is the name of the file used
293 to store the group. You can refer to the group using rox.app_options.
294 See rox.options.OptionGroup."""
295 global app_options
296 assert not app_options
297 from options import OptionGroup
298 app_options = OptionGroup(program, leaf)
300 _options_box = None
301 def edit_options(options_file = None):
302 """Edit the app_options (set using setup_app_options()) using the GUI
303 specified in 'options_file' (default <app_dir>/Options.xml).
304 If this function is called again while the box is still open, the
305 old box will be redisplayed to the user."""
306 assert app_options
308 global _options_box
309 if _options_box:
310 _options_box.present()
311 return
313 if not options_file:
314 options_file = os.path.join(app_dir, 'Options.xml')
316 import OptionsBox
317 _options_box = OptionsBox.OptionsBox(app_options, options_file)
319 def closed(widget):
320 global _options_box
321 assert _options_box == widget
322 _options_box = None
323 _options_box.connect('destroy', closed)
324 _options_box.open()
326 try:
327 import xml
328 except:
329 alert(_("You do not have the Python 'xml' module installed, which "
330 "ROX-Lib2 requires. You need to install python-xmlbase "
331 "(this is a small package; the full PyXML package is not "
332 "required)."))
334 if g.pygtk_version[:2] == (1, 99) and g.pygtk_version[2] < 12:
335 # 1.99.12 is really too old too, but RH8.0 uses it so we'll have
336 # to work around any problems...
337 sys.stderr.write('Your version of pygtk (%d.%d.%d) is too old. '
338 'Things might not work correctly.' % g.pygtk_version)