| 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 | |
| 218 | ## If we have a prompt, insert it. |
| 219 | if opts.prompt is not None: |
| 220 | box.pack_start(GTK.Label(opts.prompt), False) |
| 221 | |
| 222 | ## Choose the appropriate widget. |
| 223 | if opts.file is None: |
| 224 | entry = SimpleEntry() |
| 225 | opts.history = False |
| 226 | if opts.invisible: |
| 227 | entry.set_visibility(False) |
| 228 | else: |
| 229 | if opts.nochoice: |
| 230 | entry = Combo() |
| 231 | else: |
| 232 | entry = ComboEntry() |
| 233 | try: |
| 234 | entry.sethistory(chomped(open(opts.file, 'r'))) |
| 235 | except IOError, error: |
| 236 | if error.errno == E.ENOENT and opts.history: |
| 237 | pass |
| 238 | else: |
| 239 | raise |
| 240 | |
| 241 | ## Insert the widget and configure it. |
| 242 | box.pack_start(entry, True) |
| 243 | if opts.default == '@': |
| 244 | try: |
| 245 | entry.setvalue(entry.gethistory.__iter__.next()) |
| 246 | except StopIteration: |
| 247 | pass |
| 248 | elif opts.default is not None: |
| 249 | entry.setvalue(opts.default) |
| 250 | entry.ready() |
| 251 | |
| 252 | ## Sort out the OK button. |
| 253 | ok = GTK.Button('OK') |
| 254 | ok.set_flags(ok.flags() | GTK.CAN_DEFAULT) |
| 255 | box.pack_start(ok, False) |
| 256 | ok.connect('clicked', accept, entry, win) |
| 257 | ok.grab_default() |
| 258 | |
| 259 | ## Handle escape. |
| 260 | win.connect('key-press-event', escape, win) |
| 261 | |
| 262 | ## Done. |
| 263 | win.show_all() |
| 264 | return entry |
| 265 | |
| 266 | ###-------------------------------------------------------------------------- |
| 267 | ### Option parsing. |
| 268 | |
| 269 | def parse_args(): |
| 270 | """ |
| 271 | Parse the command line, returning a triple (PARSER, OPTS, ARGS). |
| 272 | """ |
| 273 | |
| 274 | op = XT.make_optparse \ |
| 275 | ([('H', 'history', |
| 276 | {'action': 'store_true', 'dest': 'history', |
| 277 | 'help': "With `--list', update with new string."}), |
| 278 | ('M', 'history-max', |
| 279 | {'type': 'int', 'dest': 'histmax', |
| 280 | 'help': "Maximum number of items written back to file."}), |
| 281 | ('d', 'default', |
| 282 | {'dest': 'default', |
| 283 | 'help': "Set the default entry."}), |
| 284 | ('i', 'invisible', |
| 285 | {'action': 'store_true', 'dest': 'invisible', |
| 286 | 'help': "Don't show the user's string as it's typed."}), |
| 287 | ('l', 'list', |
| 288 | {'dest': 'file', |
| 289 | 'help': "Read FILE into a drop-down list."}), |
| 290 | ('n', 'no-choice', |
| 291 | {'action': 'store_true', 'dest': 'nochoice', |
| 292 | 'help': "No free text input: user must choose item from list."}), |
| 293 | ('p', 'prompt', |
| 294 | {'dest': 'prompt', |
| 295 | 'help': "Set the window's prompt string."}), |
| 296 | ('t', 'title', |
| 297 | {'dest': 'title', |
| 298 | 'help': "Set the window's title string."})], |
| 299 | version = VERSION, |
| 300 | usage = '%prog [-Hin] [-M HISTMAX] [-p PROMPT] [-l FILE] [-t TITLE]') |
| 301 | |
| 302 | op.set_defaults(title = 'Input request', |
| 303 | invisible = False, |
| 304 | prompt = None, |
| 305 | file = None, |
| 306 | nochoice = False, |
| 307 | history = False, |
| 308 | histmax = 20) |
| 309 | |
| 310 | opts, args = op.parse_args() |
| 311 | return op, opts, args |
| 312 | |
| 313 | ###-------------------------------------------------------------------------- |
| 314 | ### Main program. |
| 315 | |
| 316 | result = None |
| 317 | |
| 318 | def main(): |
| 319 | |
| 320 | ## Startup. |
| 321 | op, opts, args = parse_args() |
| 322 | if len(args) > 0: |
| 323 | op.print_usage(stderr) |
| 324 | exit(1) |
| 325 | entry = make_window(opts) |
| 326 | XT.addreason() |
| 327 | GTK.main() |
| 328 | |
| 329 | ## Closedown. |
| 330 | if result is None: |
| 331 | exit(1) |
| 332 | if opts.history: |
| 333 | try: |
| 334 | new = '%s.new' % opts.file |
| 335 | out = open(new, 'w') |
| 336 | print >>out, result |
| 337 | i = 0 |
| 338 | for l in entry.gethistory(): |
| 339 | if opts.histmax != 0 and i >= opts.histmax: |
| 340 | break |
| 341 | if l != result: |
| 342 | print >>out, l |
| 343 | i += 1 |
| 344 | out.close() |
| 345 | OS.rename(new, opts.file) |
| 346 | finally: |
| 347 | try: |
| 348 | OS.unlink(new) |
| 349 | except OSError, err: |
| 350 | if err.errno != E.ENOENT: |
| 351 | raise |
| 352 | print result |
| 353 | exit(0) |
| 354 | |
| 355 | if __name__ == '__main__': |
| 356 | main() |
| 357 | |
| 358 | ###----- That's all, folks -------------------------------------------------- |