Commit | Line | Data |
---|---|---|
bce8c6ee MW |
1 | #! @PYTHON@ |
2 | ### -*-python-*- | |
3 | ### | |
4 | ### Utility module for xtoys Python programs | |
5 | ### | |
6 | ### (c) 2007 Straylight/Edgeware | |
7 | ### | |
8 | ||
9 | ###----- Licensing notice --------------------------------------------------- | |
10 | ### | |
11 | ### This file is part of the Edgeware XT tools collection. | |
12 | ### | |
13 | ### XT tools is free software; you can redistribute it and/or modify | |
14 | ### it under the terms of the GNU General Public License as published by | |
15 | ### the Free Software Foundation; either version 2 of the License, or | |
16 | ### (at your option) any later version. | |
17 | ### | |
18 | ### XT tools is distributed in the hope that it will be useful, | |
19 | ### but WITHOUT ANY WARRANTY; without even the implied warranty of | |
20 | ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
21 | ### GNU General Public License for more details. | |
22 | ### | |
23 | ### You should have received a copy of the GNU General Public License | |
24 | ### along with XT tools; if not, write to the Free Software Foundation, | |
25 | ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. | |
26 | ||
27 | VERSION = '@VERSION@' | |
28 | ||
29 | ###-------------------------------------------------------------------------- | |
30 | ### External dependencies. | |
31 | ||
32 | import optparse as O | |
33 | from sys import stdout, stderr, exit | |
34 | import os as OS | |
35 | import errno as E | |
36 | ||
37 | import xtoys as XT | |
38 | GTK, GDK, GO = XT.GTK, XT.GDK, XT.GO | |
39 | ||
40 | ###-------------------------------------------------------------------------- | |
41 | ### Entry classes. | |
42 | ||
43 | ### These package up the mess involved with the different kinds of dialogue | |
44 | ### box xgetline can show. The common interface is informal, but looks like | |
45 | ### this: | |
46 | ### | |
47 | ### ready(): prepare the widget for action | |
48 | ### value(): extract the value the user entered | |
49 | ### setvalue(VALUE): show VALUE as the existing value in the widget | |
50 | ### sethistory(HISTORY): store the HISTORY in the widget's history list | |
51 | ### gethistory(): extract the history list back out again | |
52 | ||
53 | def setup_entry(entry): | |
54 | """Standard things to do to an entry widget.""" | |
55 | entry.grab_focus() | |
56 | entry.set_activates_default(True) | |
57 | ||
58 | class SimpleEntry (GTK.Entry): | |
59 | """A plain old Entry widget with no bells or whistles.""" | |
60 | def setvalue(me, value): | |
61 | me.set_text(value) | |
62 | def ready(me): | |
63 | setup_entry(me) | |
64 | def value(me): | |
65 | return me.get_text() | |
66 | def gethistory(me): | |
67 | return () | |
68 | ||
69 | class ModelMixin (object): | |
70 | """ | |
71 | A helper mixin for classes which make use of a TreeModel. | |
72 | ||
73 | It provides the sethistory and gethistory methods for the common widget | |
74 | interface, and can produce a CellRenderer for stuffing into the viewer | |
75 | widget, whatever that might be. | |
76 | """ | |
77 | ||
78 | def __init__(me): | |
79 | """Initialize the ModelMixin.""" | |
80 | me.model = GTK.ListStore(GO.TYPE_STRING) | |
81 | me.set_model(me.model) | |
82 | ||
83 | def setlayout(me): | |
84 | """Insert a CellRenderer for displaying history items into the widget.""" | |
85 | cell = GTK.CellRendererText() | |
86 | me.pack_start(cell) | |
87 | me.set_attributes(cell, text = 0) | |
88 | ||
89 | def sethistory(me, history): | |
90 | """Write the HISTORY into the model.""" | |
91 | for line in history: | |
92 | me.model.append([line]) | |
93 | ||
94 | def gethistory(me): | |
95 | """Extract the history from the model.""" | |
96 | return (l[0] for l in me.model) | |
97 | ||
98 | class Combo (ModelMixin, GTK.ComboBox): | |
99 | """ | |
100 | A widget which uses a non-editable Combo box for entry. | |
101 | """ | |
102 | ||
103 | def __init__(me): | |
104 | """Initialize the widget.""" | |
105 | GTK.ComboBox.__init__(me) | |
106 | ModelMixin.__init__(me) | |
107 | me.setlayout() | |
108 | ||
109 | def sethistory(me, history): | |
110 | """ | |
111 | Insert the HISTORY. | |
112 | ||
113 | We have to select some item, so it might as well be the first one. | |
114 | """ | |
115 | ModelMixin.sethistory(me, history) | |
116 | me.set_active(0) | |
117 | ||
118 | def ready(me): | |
119 | """Nothing special needed to make us ready.""" | |
120 | pass | |
121 | ||
122 | def setvalue(me, value): | |
123 | """ | |
124 | Store a value in the widget. | |
125 | ||
126 | This involves finding it in the list and setting it by index. I suppose | |
127 | I could keep a dictionary, but it seems bad to have so many copies. | |
128 | """ | |
129 | for i in xrange(len(me.model)): | |
130 | if me.model[i][0] == value: | |
131 | me.set_active(i) | |
132 | ||
133 | def value(me): | |
134 | """Extract the current selection.""" | |
135 | return me.model[me.get_active()][0] | |
136 | ||
137 | class ComboEntry (ModelMixin, GTK.ComboBoxEntry): | |
138 | """ | |
139 | A widget which uses an editable combo box. | |
140 | """ | |
141 | ||
142 | def __init__(me): | |
143 | """ | |
144 | Initialize the widget. | |
145 | """ | |
146 | GTK.ComboBoxEntry.__init__(me) | |
147 | ModelMixin.__init__(me) | |
148 | me.set_text_column(0) | |
149 | ||
150 | def ready(me): | |
151 | """ | |
152 | Set up the entry widget. | |
153 | ||
154 | We grab the arrow keys to step through the history. | |
155 | """ | |
156 | setup_entry(me.child) | |
157 | me.child.connect('key-press-event', me.press) | |
158 | ||
159 | def press(me, _, event): | |
160 | """ | |
161 | Handle key-press events. | |
162 | ||
163 | Specifically, up and down to move through the history. | |
164 | """ | |
165 | if GDK.keyval_name(event.keyval) in ('Up', 'Down'): | |
166 | me.popup() | |
167 | return True | |
168 | return False | |
169 | ||
170 | def setvalue(me, value): | |
171 | me.child.set_text(value) | |
172 | def value(me): | |
173 | return me.child.get_text() | |
174 | ||
175 | ###-------------------------------------------------------------------------- | |
176 | ### Utility functions. | |
177 | ||
178 | def chomped(lines): | |
179 | """For each line in LINES, generate a line without trailing newline.""" | |
180 | for line in lines: | |
181 | if line != '' and line[-1] == '\n': | |
182 | line = line[:-1] | |
183 | yield line | |
184 | ||
185 | ###-------------------------------------------------------------------------- | |
186 | ### Create the window. | |
187 | ||
188 | def escape(_, event, win): | |
189 | """Key-press handler: on escape, destroy WIN.""" | |
190 | if GDK.keyval_name(event.keyval) == 'Escape': | |
191 | win.destroy() | |
192 | return True | |
193 | return False | |
194 | ||
195 | def accept(_, entry, win): | |
196 | """OK button handler: store user's value and end.""" | |
197 | global result | |
198 | result = entry.value() | |
199 | win.destroy() | |
200 | return True | |
201 | ||
202 | def make_window(opts): | |
203 | """ | |
204 | Make and return the main window. | |
205 | """ | |
206 | ||
207 | ## Create the window. | |
208 | win = GTK.Window(GTK.WINDOW_TOPLEVEL) | |
209 | win.set_title(opts.title) | |
210 | win.set_position(GTK.WIN_POS_MOUSE) | |
211 | win.connect('destroy', lambda _: XT.delreason()) | |
212 | ||
213 | ## Make a horizontal box for the widgets. | |
214 | box = GTK.HBox(spacing = 4) | |
215 | box.set_border_width(4) | |
216 | win.add(box) | |
217 | ||
bce8c6ee MW |
218 | ## Choose the appropriate widget. |
219 | if opts.file is None: | |
220 | entry = SimpleEntry() | |
221 | opts.history = False | |
222 | if opts.invisible: | |
223 | entry.set_visibility(False) | |
224 | else: | |
225 | if opts.nochoice: | |
226 | entry = Combo() | |
227 | else: | |
228 | entry = ComboEntry() | |
229 | try: | |
230 | entry.sethistory(chomped(open(opts.file, 'r'))) | |
231 | except IOError, error: | |
232 | if error.errno == E.ENOENT and opts.history: | |
233 | pass | |
234 | else: | |
235 | raise | |
236 | ||
583efdd3 MW |
237 | ## If we have a prompt, insert it. |
238 | if opts.prompt is not None: | |
239 | label = GTK.Label(opts.prompt) | |
240 | label.set_properties(mnemonic_widget = entry, | |
241 | use_underline = True) | |
242 | box.pack_start(label, False) | |
243 | ||
bce8c6ee MW |
244 | ## Insert the widget and configure it. |
245 | box.pack_start(entry, True) | |
246 | if opts.default == '@': | |
247 | try: | |
248 | entry.setvalue(entry.gethistory.__iter__.next()) | |
249 | except StopIteration: | |
250 | pass | |
251 | elif opts.default is not None: | |
252 | entry.setvalue(opts.default) | |
253 | entry.ready() | |
254 | ||
255 | ## Sort out the OK button. | |
256 | ok = GTK.Button('OK') | |
257 | ok.set_flags(ok.flags() | GTK.CAN_DEFAULT) | |
258 | box.pack_start(ok, False) | |
259 | ok.connect('clicked', accept, entry, win) | |
260 | ok.grab_default() | |
261 | ||
262 | ## Handle escape. | |
263 | win.connect('key-press-event', escape, win) | |
264 | ||
265 | ## Done. | |
266 | win.show_all() | |
267 | return entry | |
268 | ||
269 | ###-------------------------------------------------------------------------- | |
270 | ### Option parsing. | |
271 | ||
272 | def parse_args(): | |
273 | """ | |
274 | Parse the command line, returning a triple (PARSER, OPTS, ARGS). | |
275 | """ | |
276 | ||
277 | op = XT.make_optparse \ | |
278 | ([('H', 'history', | |
279 | {'action': 'store_true', 'dest': 'history', | |
280 | 'help': "With `--list', update with new string."}), | |
281 | ('M', 'history-max', | |
282 | {'type': 'int', 'dest': 'histmax', | |
283 | 'help': "Maximum number of items written back to file."}), | |
284 | ('d', 'default', | |
285 | {'dest': 'default', | |
286 | 'help': "Set the default entry."}), | |
287 | ('i', 'invisible', | |
288 | {'action': 'store_true', 'dest': 'invisible', | |
289 | 'help': "Don't show the user's string as it's typed."}), | |
290 | ('l', 'list', | |
291 | {'dest': 'file', | |
292 | 'help': "Read FILE into a drop-down list."}), | |
293 | ('n', 'no-choice', | |
294 | {'action': 'store_true', 'dest': 'nochoice', | |
295 | 'help': "No free text input: user must choose item from list."}), | |
296 | ('p', 'prompt', | |
297 | {'dest': 'prompt', | |
298 | 'help': "Set the window's prompt string."}), | |
299 | ('t', 'title', | |
300 | {'dest': 'title', | |
301 | 'help': "Set the window's title string."})], | |
302 | version = VERSION, | |
303 | usage = '%prog [-Hin] [-M HISTMAX] [-p PROMPT] [-l FILE] [-t TITLE]') | |
304 | ||
305 | op.set_defaults(title = 'Input request', | |
306 | invisible = False, | |
307 | prompt = None, | |
308 | file = None, | |
309 | nochoice = False, | |
310 | history = False, | |
311 | histmax = 20) | |
312 | ||
313 | opts, args = op.parse_args() | |
314 | return op, opts, args | |
315 | ||
316 | ###-------------------------------------------------------------------------- | |
317 | ### Main program. | |
318 | ||
319 | result = None | |
320 | ||
321 | def main(): | |
322 | ||
323 | ## Startup. | |
324 | op, opts, args = parse_args() | |
325 | if len(args) > 0: | |
326 | op.print_usage(stderr) | |
327 | exit(1) | |
328 | entry = make_window(opts) | |
329 | XT.addreason() | |
330 | GTK.main() | |
331 | ||
332 | ## Closedown. | |
333 | if result is None: | |
334 | exit(1) | |
335 | if opts.history: | |
336 | try: | |
337 | new = '%s.new' % opts.file | |
338 | out = open(new, 'w') | |
339 | print >>out, result | |
340 | i = 0 | |
341 | for l in entry.gethistory(): | |
342 | if opts.histmax != 0 and i >= opts.histmax: | |
343 | break | |
344 | if l != result: | |
345 | print >>out, l | |
346 | i += 1 | |
347 | out.close() | |
348 | OS.rename(new, opts.file) | |
349 | finally: | |
350 | try: | |
351 | OS.unlink(new) | |
352 | except OSError, err: | |
353 | if err.errno != E.ENOENT: | |
354 | raise | |
355 | print result | |
356 | exit(0) | |
357 | ||
358 | if __name__ == '__main__': | |
359 | main() | |
360 | ||
361 | ###----- That's all, folks -------------------------------------------------- |