| 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 -------------------------------------------------- |