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,
9 The AppRun script of a simple application might look like this:
12 import findrox; findrox.version(1, 9, 12)
16 window.set_title('My window')
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
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.
32 import sys
, os
, codecs
34 _to_utf8
= codecs
.getencoder('utf-8')
36 roxlib_version
= (1, 9, 16)
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.
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')
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
[:]
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
84 # Try again without Zero Install
85 sys
.path
= _old_sys_path
86 if 'gtk' in sys
.modules
:
87 del sys
.modules
['gtk']
89 # Try to support 1.99.12, at least to show an error
90 import pygtk
; pygtk
.require('2.0')
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
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'))
101 # Put argv back the way it was, now that Gtk has initialised
104 def _warn_old_findrox():
108 return # Don't worry too much if it's missing
109 if not hasattr(findrox
, 'version'):
110 print >>sys
.stderr
, "WARNING from ROX-Lib: the version of " \
111 "findrox.py used by this application (%s) is very " \
112 "old and may cause problems." % app_dir
115 # For backwards compatibility. Use True and False in new code.
119 class UserAbort(Exception):
120 """Raised when the user aborts an operation, eg by clicking on Cancel
121 or pressing Escape."""
122 def __init__(self
, message
= None):
123 Exception.__init
__(self
,
124 message
or _("Operation aborted at user's request"))
127 "Display message in an error box. Return when the user closes the box."
129 box
= g
.MessageDialog(None, 0, g
.MESSAGE_ERROR
, g
.BUTTONS_OK
, message
)
130 box
.set_position(g
.WIN_POS_CENTER
)
131 box
.set_title(_('Error'))
136 def bug(message
= "A bug has been detected in this program. Please report "
137 "the problem to the authors."):
138 "Display an error message and offer a debugging prompt."
140 raise Exception(message
)
142 type, value
, tb
= sys
.exc_info()
144 debug
.show_exception(type, value
, tb
, auto_details
= True)
147 """Display message in an error box, then quit the program, returning
148 with a non-zero exit status."""
153 "Display informational message. Returns when the user closes the box."
155 box
= g
.MessageDialog(None, 0, g
.MESSAGE_INFO
, g
.BUTTONS_OK
, message
)
156 box
.set_position(g
.WIN_POS_CENTER
)
157 box
.set_title(_('Information'))
162 def confirm(message
, stock_icon
, action
= None):
163 """Display a <Cancel>/<Action> dialog. Result is true if the user
164 chooses the action, false otherwise. If action is given then that
165 is used as the text instead of the default for the stock item. Eg:
166 if rox.confirm('Really delete everything?', g.STOCK_DELETE): delete()
169 box
= g
.MessageDialog(None, 0, g
.MESSAGE_QUESTION
,
170 g
.BUTTONS_CANCEL
, message
)
172 button
= ButtonMixed(stock_icon
, action
)
174 button
= g
.Button(stock
= stock_icon
)
175 button
.set_flags(g
.CAN_DEFAULT
)
177 box
.add_action_widget(button
, g
.RESPONSE_OK
)
178 box
.set_position(g
.WIN_POS_CENTER
)
179 box
.set_title(_('Confirm:'))
180 box
.set_default_response(g
.RESPONSE_OK
)
184 return resp
== g
.RESPONSE_OK
186 def report_exception():
187 """Display the current python exception in an error box, returning
188 when the user closes the box. This is useful in the 'except' clause
189 of a 'try' block. Uses rox.debug.show_exception()."""
190 type, value
, tb
= sys
.exc_info()
192 debug
.show_exception(type, value
, tb
)
194 _icon_path
= os
.path
.join(app_dir
, '.DirIcon')
196 if os
.path
.exists(_icon_path
):
198 g
.window_set_default_icon_list(g
.gdk
.pixbuf_new_from_file(_icon_path
))
201 _window_icon
= g
.gdk
.pixbuf_new_from_file(_icon_path
)
204 class Window(g
.Window
):
205 """This works in exactly the same way as a GtkWindow, except that
206 it calls the toplevel_(un)ref functions for you automatically,
207 and sets the window icon to <app_dir>/.DirIcon if it exists."""
208 def __init__(*args
, **kwargs
):
209 apply(g
.Window
.__init
__, args
, kwargs
)
211 args
[0].connect('destroy', toplevel_unref
)
214 args
[0].set_icon(_window_icon
)
216 class Dialog(g
.Dialog
):
217 """This works in exactly the same way as a GtkDialog, except that
218 it calls the toplevel_(un)ref functions for you automatically."""
219 def __init__(*args
, **kwargs
):
220 apply(g
.Dialog
.__init
__, args
, kwargs
)
222 args
[0].connect('destroy', toplevel_unref
)
224 class ButtonMixed(g
.Button
):
225 """A button with a standard stock icon, but any label. This is useful
226 when you want to express a concept similar to one of the stock ones."""
227 def __init__(self
, stock
, message
):
228 """Specify the icon and text for the new button. The text
229 may specify the mnemonic for the widget by putting a _ before
231 button = ButtonMixed(g.STOCK_DELETE, '_Delete message')."""
232 g
.Button
.__init
__(self
)
235 label
.set_text_with_mnemonic(message
)
236 label
.set_mnemonic_widget(self
)
238 image
= g
.image_new_from_stock(stock
, g
.ICON_SIZE_BUTTON
)
239 box
= g
.HBox(FALSE
, 2)
240 align
= g
.Alignment(0.5, 0.5, 0.0, 0.0)
242 box
.pack_start(image
, FALSE
, FALSE
, 0)
243 box
.pack_end(label
, FALSE
, FALSE
, 0)
249 _toplevel_windows
= 0
252 """This is a wrapper around the gtk2.mainloop function. It only runs
253 the loop if there are top level references, and exits when
254 rox.toplevel_unref() reduces the count to zero."""
255 global _toplevel_windows
, _in_mainloops
257 _in_mainloops
= _in_mainloops
+ 1 # Python1.5 syntax
259 while _toplevel_windows
:
262 _in_mainloops
= _in_mainloops
- 1
265 """Increment the toplevel ref count. rox.mainloop() won't exit until
266 toplevel_unref() is called the same number of times."""
267 global _toplevel_windows
268 _toplevel_windows
= _toplevel_windows
+ 1
270 def toplevel_unref(*unused
):
271 """Decrement the toplevel ref count. If this is called while in
272 rox.mainloop() and the count has reached zero, then rox.mainloop()
273 will exit. Ignores any arguments passed in, so you can use it
274 easily as a callback function."""
275 global _toplevel_windows
276 assert _toplevel_windows
> 0
277 _toplevel_windows
= _toplevel_windows
- 1
278 if _toplevel_windows
== 0 and _in_mainloops
:
283 """Try to return the canonical name for this computer. This is used
284 in the drag-and-drop protocol to work out whether a drop is coming from
285 a remote machine (and therefore has to be fetched differently)."""
286 from socket
import getfqdn
291 _host_name
= getfqdn()
293 _host_name
= 'localhost'
294 alert("ROX-Lib socket.getfqdn() failed!")
298 "Convert each space to %20, etc"
300 return re
.sub('[^-_./a-zA-Z0-9]',
301 lambda match
: '%%%02x' % ord(match
.group(0)),
305 "Convert each %20 to a space, etc"
306 if '%' not in uri
: return uri
308 return re
.sub('%[0-9a-fA-F][0-9a-fA-F]',
309 lambda match
: chr(int(match
.group(0)[1:], 16)),
312 def get_local_path(uri
):
313 """Convert 'uri' to a local path and return, if possible. If 'uri'
314 is a resource on a remote machine, return None. URI is in the escaped form
321 return unescape(uri
) # A normal Unix pathname
324 return None # //something
326 return unescape(uri
[2:]) # ///path
327 remote_host
= uri
[2:i
]
328 if remote_host
== our_host_name():
329 return unescape(uri
[i
:]) # //localhost/path
331 elif uri
[:5].lower() == 'file:':
333 return get_local_path(uri
[5:])
334 elif uri
[:2] == './' or uri
[:3] == '../':
339 def setup_app_options(program
, leaf
= 'Options.xml'):
340 """Most applications only have one set of options. This function can be
341 used to set up the default group. 'program' is the name of the
342 directory to use in <Choices> and 'leaf' is the name of the file used
343 to store the group. You can refer to the group using rox.app_options.
344 See rox.options.OptionGroup."""
346 assert not app_options
347 from options
import OptionGroup
348 app_options
= OptionGroup(program
, leaf
)
351 def edit_options(options_file
= None):
352 """Edit the app_options (set using setup_app_options()) using the GUI
353 specified in 'options_file' (default <app_dir>/Options.xml).
354 If this function is called again while the box is still open, the
355 old box will be redisplayed to the user."""
360 _options_box
.present()
364 options_file
= os
.path
.join(app_dir
, 'Options.xml')
367 _options_box
= OptionsBox
.OptionsBox(app_options
, options_file
)
371 assert _options_box
== widget
373 _options_box
.connect('destroy', closed
)
379 alert(_("You do not have the Python 'xml' module installed, which "
380 "ROX-Lib2 requires. You need to install python-xmlbase "
381 "(this is a small package; the full PyXML package is not "
384 if g
.pygtk_version
[:2] == (1, 99) and g
.pygtk_version
[2] < 12:
385 # 1.99.12 is really too old too, but RH8.0 uses it so we'll have
386 # to work around any problems...
387 sys
.stderr
.write('Your version of pygtk (%d.%d.%d) is too old. '
388 'Things might not work correctly.' % g
.pygtk_version
)