Commit | Line | Data |
---|---|---|
bce8c6ee MW |
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 -------------------------------------------------- |