Release 1.6.0.
[xtoys] / xtoys.py
1 ### -*-python-*-
2 ###
3 ### Utility module for xtoys Python programs
4 ###
5 ### (c) 2007 Straylight/Edgeware
6 ###
7
8 ###----- Licensing notice ---------------------------------------------------
9 ###
10 ### This file is part of the Edgeware X tools collection.
11 ###
12 ### X tools is free software; you can redistribute it and/or modify
13 ### it under the terms of the GNU General Public License as published by
14 ### the Free Software Foundation; either version 2 of the License, or
15 ### (at your option) any later version.
16 ###
17 ### X tools is distributed in the hope that it will be useful,
18 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ### GNU General Public License for more details.
21 ###
22 ### You should have received a copy of the GNU General Public License
23 ### along with X tools; if not, write to the Free Software Foundation,
24 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25
26 ###--------------------------------------------------------------------------
27 ### External dependencies.
28
29 import os as OS
30 import optparse as O
31 from sys import stdin, stdout, exit, argv
32
33 import pygtk
34 pygtk.require('2.0')
35 import gtk as GTK
36 GDK = GTK.gdk
37 import gobject as GO
38 del pygtk
39
40 ###--------------------------------------------------------------------------
41 ### Reasons for living.
42
43 _reasons = 0
44
45 def addreason():
46 """Add a reason."""
47 global _reasons
48 _reasons += 1
49
50 def delreason():
51 """Drop a reason. When reasons reach zero, the main loop stops."""
52 global _reasons
53 _reasons -= 1
54 if _reasons == 0:
55 GTK.main_quit()
56
57 ###--------------------------------------------------------------------------
58 ### General utilities.
59
60 def make_optparse(options, **kw):
61 """
62 Construct an option parser object.
63
64 The KW are keyword arguments to be passed to OptionParser. The OPTIONS are
65 a list of (SHORT, LONG, KW) triples; the SHORT and LONG strings do /not/
66 have leading dashes.
67 """
68 op = O.OptionParser(**kw)
69 for short, long, kw in options:
70 names = ['--%s' % long]
71 if short is not None:
72 names.append('-%s' % short)
73 op.add_option(*names, **kw)
74 return op
75
76 ###--------------------------------------------------------------------------
77 ### Message boxes.
78
79 class MessageButton (object):
80 """
81 An object storing information about a button in a Message.
82 """
83
84 def __init__(me, label, value = None, *options):
85 """
86 Initialize a button definition.
87
88 If LABEL is a tuple, then it should have the form
89 (LABEL, VALUE, OPTIONS...); the initialization parser is applied to its
90 contents. This is not done recursively.
91
92 If LABEL is a string, and VALUE and OPTIONS are omitted, then it is
93 parsed as OPT:OPT:...:LABEL, where OPT is either an option (see below) or
94 `=VALUE'. Only one VALUE may be given.
95
96 The LABEL is the label to put on the button, or the GTK stock id. The
97 VALUE is the value to return from Message.ask if the button is chosen.
98 The OPTIONS are:
99
100 * 'default': this is the default button
101 * 'cancel': this is the cancel button
102 """
103 if value is not None or len(options) > 0:
104 me._doinit(label, value, *options)
105 elif isinstance(label, tuple):
106 me._doinit(*label)
107 else:
108 i = 0
109 options = []
110 while 0 <= i < len(label):
111 if label[i] == '!':
112 i += 1
113 break
114 j = label.find(':', i)
115 if j < 0:
116 break
117 if label[i] == '=':
118 if value is not None:
119 raise ValueError, 'Duplicate value in button spec %s' % label
120 value = label[i + 1:j]
121 else:
122 options.append(label[i:j])
123 i = j + 1
124 label = label[i:]
125 me._doinit(label, value, *options)
126
127 def _doinit(me, label, value = None, *options):
128 """
129 Does the work of processing the initialization parameters.
130 """
131 me.label = label
132 if value is None:
133 me.value = label
134 else:
135 me.value = value
136 me.defaultp = me.cancelp = False
137 for opt in options:
138 if opt == 'default':
139 me.defaultp = True
140 elif opt == 'cancel':
141 me.cancelp = True
142 else:
143 raise ValueError, 'unknown button option %s' % opt
144
145 class Message (GTK.MessageDialog):
146 """
147 A simple message-box window: contains text and some buttons.
148
149 See __init__ for the usage instructions.
150 """
151
152 ## Mapping from Pythonic strings to GTK constants.
153 dboxtype = {'info': GTK.MESSAGE_INFO,
154 'warning': GTK.MESSAGE_WARNING,
155 'question': GTK.MESSAGE_QUESTION,
156 'error': GTK.MESSAGE_ERROR}
157
158 def __init__(me,
159 title = None,
160 type = 'info',
161 message = '',
162 headline = None,
163 buttons = [],
164 markupp = False):
165 """
166 Report a message to the user and get a response back.
167
168 The TITLE is placed in the window's title bar.
169
170 The TYPE controls what kind of icon is placed in the window; it should be
171 one of 'info', 'warning', 'question' or 'error'.
172
173 The MESSAGE is the string which should be displayed. It may have
174 multiple lines. There may also be a HEADLINE message. The messages are
175 parsed for Pango markup if MARKUPP is set.
176
177 The BUTTONS are a list of buttons to show, right to left. Each one
178 should be a string which may either be a GTK stock tag or a plain label
179 string. This may be prefixed, optionally, by `!' to ignore other prefix
180 characters, `+' to make this button the default, or `:' to make this the
181 `cancel' button, chosen by pressing escape. If no button is marked as
182 cancel, we just use the first.
183 """
184
185 ## Initialize superclasses.
186 GTK.MessageDialog.__init__(me, type = me.dboxtype.get(type))
187
188 ## Set the title.
189 if title is None:
190 title = OS.path.basename(argv[0])
191 me.set_title(title)
192
193 ## Add the message strings.
194 me.set_property('use-markup', markupp)
195 if headline is None:
196 me.set_property('text', message)
197 else:
198 me.set_property('text', headline)
199 me.set_property('secondary-text', message)
200 me.set_property('secondary-use-markup', markupp)
201
202 ## Include the buttons.
203 if len(buttons) == 0:
204 buttons = [MessageButton('gtk-ok', True, 'default', 'cancel')]
205 else:
206 buttons = buttons[:]
207 buttons.reverse()
208 me.buttons = [isinstance(b, MessageButton) and b or MessageButton(b)
209 for b in buttons]
210 cancel = -1
211 default = -1
212 for i in xrange(len(buttons)):
213 button = me.buttons[i]
214 label = button.label
215 if GTK.stock_lookup(label) is None:
216 b = GTK.Button(label = label)
217 else:
218 b = GTK.Button(stock = label)
219 if button.defaultp:
220 default = i
221 if button.cancelp:
222 cancel = i
223 b.set_flags(b.flags() | GTK.CAN_DEFAULT)
224 button.widget = b
225 me.add_action_widget(b, i)
226
227 ## Choose default buttons.
228 if cancel == -1:
229 cancel = 0
230 if default == -1:
231 default = len(me.buttons) - 1
232 me.cancel = cancel
233 me.default = default
234
235 ## Connect up the handlers and go.
236 me.connect('key-press-event', me.keypress, me.buttons[cancel])
237 me.buttons[default].widget.grab_default()
238
239 def keypress(me, win, event, cancel):
240 """
241 Handle key-press events.
242
243 If the EVENT was an escape-press, then activate the CANCEL button.
244 """
245 key = GDK.keyval_name(event.keyval)
246 if key != 'Escape':
247 return False
248 cancel.widget.activate()
249 return True
250
251 def ask(me):
252 """
253 Display the message, and wait for a response.
254
255 The return value is the label of the button which was chosen.
256 """
257 me.show_all()
258 r = me.run()
259 if r < 0:
260 r = me.cancel
261 me.hide()
262 return me.buttons[r].value
263
264 ###----- That's all, folks --------------------------------------------------