| 1 | #!/usr/bin/python |
|---|
| 2 | # |
|---|
| 3 | # Urwid advanced example column calculator application |
|---|
| 4 | # Copyright (C) 2004-2009 Ian Ward |
|---|
| 5 | # |
|---|
| 6 | # This library is free software; you can redistribute it and/or |
|---|
| 7 | # modify it under the terms of the GNU Lesser General Public |
|---|
| 8 | # License as published by the Free Software Foundation; either |
|---|
| 9 | # version 2.1 of the License, or (at your option) any later version. |
|---|
| 10 | # |
|---|
| 11 | # This library is distributed in the hope that it will be useful, |
|---|
| 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
|---|
| 14 | # Lesser General Public License for more details. |
|---|
| 15 | # |
|---|
| 16 | # You should have received a copy of the GNU Lesser General Public |
|---|
| 17 | # License along with this library; if not, write to the Free Software |
|---|
| 18 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
|---|
| 19 | # |
|---|
| 20 | # Urwid web site: http://excess.org/urwid/ |
|---|
| 21 | |
|---|
| 22 | """ |
|---|
| 23 | Urwid advanced example column calculator application |
|---|
| 24 | |
|---|
| 25 | Features: |
|---|
| 26 | - multiple separate list boxes within columns |
|---|
| 27 | - custom edit widget for editing calculator cells |
|---|
| 28 | - custom parent widget for links to other columns |
|---|
| 29 | - custom list walker to show and hide cell results as required |
|---|
| 30 | - custom wrap and align modes for editing right-1 aligned numbers |
|---|
| 31 | - outputs commands that may be used to recreate expression on exit |
|---|
| 32 | """ |
|---|
| 33 | |
|---|
| 34 | import urwid |
|---|
| 35 | import urwid.raw_display |
|---|
| 36 | import urwid.web_display |
|---|
| 37 | |
|---|
| 38 | # use appropriate Screen class |
|---|
| 39 | if urwid.web_display.is_web_request(): |
|---|
| 40 | Screen = urwid.web_display.Screen |
|---|
| 41 | else: |
|---|
| 42 | Screen = urwid.raw_display.Screen |
|---|
| 43 | |
|---|
| 44 | |
|---|
| 45 | def div_or_none(a,b): |
|---|
| 46 | """Divide a by b. Return result or None on divide by zero.""" |
|---|
| 47 | if b == 0: |
|---|
| 48 | return None |
|---|
| 49 | return a/b |
|---|
| 50 | |
|---|
| 51 | # operators supported and the functions used to calculate a result |
|---|
| 52 | OPERATORS = { |
|---|
| 53 | '+': (lambda a, b: a+b), |
|---|
| 54 | '-': (lambda a, b: a-b), |
|---|
| 55 | '*': (lambda a, b: a*b), |
|---|
| 56 | '/': div_or_none, |
|---|
| 57 | } |
|---|
| 58 | |
|---|
| 59 | # the uppercase versions of keys used to switch columns |
|---|
| 60 | COLUMN_KEYS = list( "?ABCDEF" ) |
|---|
| 61 | |
|---|
| 62 | # these lists are used to determine when to display errors |
|---|
| 63 | EDIT_KEYS = OPERATORS.keys() + COLUMN_KEYS + ['backspace','delete'] |
|---|
| 64 | MOVEMENT_KEYS = ['up','down','left','right','page up','page down'] |
|---|
| 65 | |
|---|
| 66 | # Event text |
|---|
| 67 | E_no_such_column = "Column %s does not exist." |
|---|
| 68 | E_no_more_columns = "Maxumum number of columns reached." |
|---|
| 69 | E_new_col_cell_not_empty = "Column must be started from an empty cell." |
|---|
| 70 | E_invalid_key = "Invalid key '%s'." |
|---|
| 71 | E_no_parent_column = "There is no parent column to return to." |
|---|
| 72 | E_cant_combine = "Cannot combine cells with sub-expressions." |
|---|
| 73 | E_invalid_in_parent_cell = "Cannot enter numbers into parent cell." |
|---|
| 74 | E_invalid_in_help_col = [ |
|---|
| 75 | "Help Column is in focus. Press ", |
|---|
| 76 | ('key',COLUMN_KEYS[1]),"-",('key',COLUMN_KEYS[-1]), |
|---|
| 77 | " to select another column."] |
|---|
| 78 | |
|---|
| 79 | # Shared layout object |
|---|
| 80 | CALC_LAYOUT = None |
|---|
| 81 | |
|---|
| 82 | |
|---|
| 83 | class CalcEvent: |
|---|
| 84 | """Events triggered by user input.""" |
|---|
| 85 | |
|---|
| 86 | attr = 'event' |
|---|
| 87 | |
|---|
| 88 | def __init__(self, message): |
|---|
| 89 | self.message = message |
|---|
| 90 | |
|---|
| 91 | def widget(self): |
|---|
| 92 | """Return a widget containing event information""" |
|---|
| 93 | text = urwid.Text( self.message, 'center' ) |
|---|
| 94 | return urwid.AttrWrap( text, self.attr ) |
|---|
| 95 | |
|---|
| 96 | class ColumnDeleteEvent(CalcEvent): |
|---|
| 97 | """Sent when user wants to delete a column""" |
|---|
| 98 | |
|---|
| 99 | attr = 'confirm' |
|---|
| 100 | |
|---|
| 101 | def __init__(self, letter, from_parent=0): |
|---|
| 102 | self.message = ["Press ", ('key',"BACKSPACE"), |
|---|
| 103 | " again to confirm column removal."] |
|---|
| 104 | self.letter = letter |
|---|
| 105 | |
|---|
| 106 | class UpdateParentEvent: |
|---|
| 107 | """Sent when parent columns may need to be updated.""" |
|---|
| 108 | pass |
|---|
| 109 | |
|---|
| 110 | |
|---|
| 111 | class Cell: |
|---|
| 112 | def __init__(self, op ): |
|---|
| 113 | self.op = op |
|---|
| 114 | self.is_top = op is None |
|---|
| 115 | self.child = None |
|---|
| 116 | self.setup_edit() |
|---|
| 117 | self.result = urwid.Text("", layout=CALC_LAYOUT) |
|---|
| 118 | |
|---|
| 119 | def show_result(self, next_cell): |
|---|
| 120 | """Return whether this widget should display its result. |
|---|
| 121 | |
|---|
| 122 | next_cell -- the cell following self or None""" |
|---|
| 123 | |
|---|
| 124 | if self.is_top: |
|---|
| 125 | return False |
|---|
| 126 | if next_cell is None: |
|---|
| 127 | return True |
|---|
| 128 | if self.op == "+" and next_cell.op == "+": |
|---|
| 129 | return False |
|---|
| 130 | return True |
|---|
| 131 | |
|---|
| 132 | |
|---|
| 133 | def setup_edit(self): |
|---|
| 134 | """Create the standard edit widget for this cell.""" |
|---|
| 135 | |
|---|
| 136 | self.edit = urwid.IntEdit() |
|---|
| 137 | if not self.is_top: |
|---|
| 138 | self.edit.set_caption( self.op + " " ) |
|---|
| 139 | self.edit.set_layout( None, None, CALC_LAYOUT ) |
|---|
| 140 | |
|---|
| 141 | def get_value(self): |
|---|
| 142 | """Return the numeric value of the cell.""" |
|---|
| 143 | |
|---|
| 144 | if self.child is not None: |
|---|
| 145 | return self.child.get_result() |
|---|
| 146 | else: |
|---|
| 147 | return long("0"+self.edit.edit_text) |
|---|
| 148 | |
|---|
| 149 | def get_result(self): |
|---|
| 150 | """Return the numeric result of this cell's operation.""" |
|---|
| 151 | |
|---|
| 152 | if self.is_top: |
|---|
| 153 | return self.get_value() |
|---|
| 154 | if self.result.text == "": |
|---|
| 155 | return None |
|---|
| 156 | return long(self.result.text) |
|---|
| 157 | |
|---|
| 158 | def set_result(self, result): |
|---|
| 159 | """Set the numeric result for this cell.""" |
|---|
| 160 | |
|---|
| 161 | if result == None: |
|---|
| 162 | self.result.set_text("") |
|---|
| 163 | else: |
|---|
| 164 | self.result.set_text( "%d" %result ) |
|---|
| 165 | |
|---|
| 166 | def become_parent(self, column, letter): |
|---|
| 167 | """Change the edit widget to a parent cell widget.""" |
|---|
| 168 | |
|---|
| 169 | self.child = column |
|---|
| 170 | self.edit = ParentEdit( self.op, letter ) |
|---|
| 171 | |
|---|
| 172 | def remove_child(self): |
|---|
| 173 | """Change the edit widget back to a standard edit widget.""" |
|---|
| 174 | |
|---|
| 175 | self.child = None |
|---|
| 176 | self.setup_edit() |
|---|
| 177 | |
|---|
| 178 | def is_empty( self ): |
|---|
| 179 | """Return True if the cell is "empty".""" |
|---|
| 180 | |
|---|
| 181 | return self.child is None and self.edit.edit_text == "" |
|---|
| 182 | |
|---|
| 183 | |
|---|
| 184 | class ParentEdit(urwid.Edit): |
|---|
| 185 | """Edit widget modified to link to a child column""" |
|---|
| 186 | |
|---|
| 187 | def __init__(self, op, letter): |
|---|
| 188 | """Use the operator and letter of the child column as caption |
|---|
| 189 | |
|---|
| 190 | op -- operator or None |
|---|
| 191 | letter -- letter of child column |
|---|
| 192 | remove_fn -- function to call when user wants to remove child |
|---|
| 193 | function takes no parameters |
|---|
| 194 | """ |
|---|
| 195 | |
|---|
| 196 | urwid.Edit.__init__(self, layout=CALC_LAYOUT) |
|---|
| 197 | self.op = op |
|---|
| 198 | self.set_letter( letter ) |
|---|
| 199 | |
|---|
| 200 | def set_letter(self, letter): |
|---|
| 201 | """Set the letter of the child column for display.""" |
|---|
| 202 | |
|---|
| 203 | self.letter = letter |
|---|
| 204 | caption = "("+letter+")" |
|---|
| 205 | if self.op is not None: |
|---|
| 206 | caption = self.op+" "+caption |
|---|
| 207 | self.set_caption(caption) |
|---|
| 208 | |
|---|
| 209 | def keypress(self, size, key): |
|---|
| 210 | """Disable usual editing, allow only removing of child""" |
|---|
| 211 | |
|---|
| 212 | if key == "backspace": |
|---|
| 213 | raise ColumnDeleteEvent(self.letter, from_parent=True) |
|---|
| 214 | elif key in list("0123456789"): |
|---|
| 215 | raise CalcEvent, E_invalid_in_parent_cell |
|---|
| 216 | else: |
|---|
| 217 | return key |
|---|
| 218 | |
|---|
| 219 | |
|---|
| 220 | class CellWalker(urwid.ListWalker): |
|---|
| 221 | def __init__(self, content): |
|---|
| 222 | self.content = urwid.MonitoredList(content) |
|---|
| 223 | self.content.modified = self._modified |
|---|
| 224 | self.focus = (0,0) |
|---|
| 225 | # everyone can share the same divider widget |
|---|
| 226 | self.div = urwid.Divider("-") |
|---|
| 227 | |
|---|
| 228 | def get_cell(self, i): |
|---|
| 229 | if i < 0 or i >= len(self.content): |
|---|
| 230 | return None |
|---|
| 231 | else: |
|---|
| 232 | return self.content[i] |
|---|
| 233 | |
|---|
| 234 | def _get_at_pos(self, pos): |
|---|
| 235 | i, sub = pos |
|---|
| 236 | assert sub in (0,1,2) |
|---|
| 237 | if i < 0 or i >= len(self.content): |
|---|
| 238 | return None, None |
|---|
| 239 | if sub == 0: |
|---|
| 240 | edit = self.content[i].edit |
|---|
| 241 | return urwid.AttrWrap(edit, 'edit', 'editfocus'), pos |
|---|
| 242 | elif sub == 1: |
|---|
| 243 | return self.div, pos |
|---|
| 244 | else: |
|---|
| 245 | return self.content[i].result, pos |
|---|
| 246 | |
|---|
| 247 | def get_focus(self): |
|---|
| 248 | return self._get_at_pos(self.focus) |
|---|
| 249 | |
|---|
| 250 | def set_focus(self, focus): |
|---|
| 251 | self.focus = focus |
|---|
| 252 | |
|---|
| 253 | def get_next(self, start_from): |
|---|
| 254 | i, sub = start_from |
|---|
| 255 | assert sub in (0,1,2) |
|---|
| 256 | if sub == 0: |
|---|
| 257 | show_result = self.content[i].show_result( |
|---|
| 258 | self.get_cell(i+1)) |
|---|
| 259 | if show_result: |
|---|
| 260 | return self._get_at_pos( (i, 1) ) |
|---|
| 261 | else: |
|---|
| 262 | return self._get_at_pos( (i+1, 0) ) |
|---|
| 263 | elif sub == 1: |
|---|
| 264 | return self._get_at_pos( (i, 2) ) |
|---|
| 265 | else: |
|---|
| 266 | return self._get_at_pos( (i+1, 0) ) |
|---|
| 267 | |
|---|
| 268 | def get_prev(self, start_from): |
|---|
| 269 | i, sub = start_from |
|---|
| 270 | assert sub in (0,1,2) |
|---|
| 271 | if sub == 0: |
|---|
| 272 | if i == 0: return None, None |
|---|
| 273 | show_result = self.content[i-1].show_result( |
|---|
| 274 | self.content[i]) |
|---|
| 275 | if show_result: |
|---|
| 276 | return self._get_at_pos( (i-1, 2) ) |
|---|
| 277 | else: |
|---|
| 278 | return self._get_at_pos( (i-1, 0) ) |
|---|
| 279 | elif sub == 1: |
|---|
| 280 | return self._get_at_pos( (i, 0) ) |
|---|
| 281 | else: |
|---|
| 282 | return self._get_at_pos( (i, 1) ) |
|---|
| 283 | |
|---|
| 284 | |
|---|
| 285 | class CellColumn( urwid.WidgetWrap ): |
|---|
| 286 | def __init__(self, letter): |
|---|
| 287 | self.walker = CellWalker([Cell(None)]) |
|---|
| 288 | self.content = self.walker.content |
|---|
| 289 | self.listbox = urwid.ListBox( self.walker ) |
|---|
| 290 | self.set_letter( letter ) |
|---|
| 291 | urwid.WidgetWrap.__init__(self, self.frame) |
|---|
| 292 | |
|---|
| 293 | def set_letter(self, letter): |
|---|
| 294 | """Set the column header with letter.""" |
|---|
| 295 | |
|---|
| 296 | self.letter = letter |
|---|
| 297 | header = urwid.AttrWrap( |
|---|
| 298 | urwid.Text( ["Column ",('key',letter)], |
|---|
| 299 | layout = CALC_LAYOUT), 'colhead' ) |
|---|
| 300 | self.frame = urwid.Frame( self.listbox, header ) |
|---|
| 301 | |
|---|
| 302 | def keypress(self, size, key): |
|---|
| 303 | key = self.frame.keypress( size, key) |
|---|
| 304 | if key is None: |
|---|
| 305 | changed = self.update_results() |
|---|
| 306 | if changed: |
|---|
| 307 | raise UpdateParentEvent() |
|---|
| 308 | return |
|---|
| 309 | |
|---|
| 310 | f, (i, sub) = self.walker.get_focus() |
|---|
| 311 | if sub != 0: |
|---|
| 312 | # f is not an edit widget |
|---|
| 313 | return key |
|---|
| 314 | if OPERATORS.has_key(key): |
|---|
| 315 | # move trailing text to new cell below |
|---|
| 316 | edit = self.walker.get_cell(i).edit |
|---|
| 317 | cursor_pos = edit.edit_pos |
|---|
| 318 | tail = edit.edit_text[cursor_pos:] |
|---|
| 319 | edit.set_edit_text( edit.edit_text[:cursor_pos] ) |
|---|
| 320 | |
|---|
| 321 | new_cell = Cell( key ) |
|---|
| 322 | new_cell.edit.set_edit_text( tail ) |
|---|
| 323 | self.content[i+1:i+1] = [new_cell] |
|---|
| 324 | |
|---|
| 325 | changed = self.update_results() |
|---|
| 326 | self.move_focus_next( size ) |
|---|
| 327 | self.content[i+1].edit.set_edit_pos(0) |
|---|
| 328 | if changed: |
|---|
| 329 | raise UpdateParentEvent() |
|---|
| 330 | return |
|---|
| 331 | |
|---|
| 332 | elif key == 'backspace': |
|---|
| 333 | # unhandled backspace, we're at beginning of number |
|---|
| 334 | # append current number to cell above, removing operator |
|---|
| 335 | above = self.walker.get_cell(i-1) |
|---|
| 336 | if above is None: |
|---|
| 337 | # we're the first cell |
|---|
| 338 | raise ColumnDeleteEvent( self.letter, |
|---|
| 339 | from_parent=False ) |
|---|
| 340 | |
|---|
| 341 | edit = self.walker.get_cell(i).edit |
|---|
| 342 | # check that we can combine |
|---|
| 343 | if above.child is not None: |
|---|
| 344 | # cell above is parent |
|---|
| 345 | if edit.edit_text: |
|---|
| 346 | # ..and current not empty, no good |
|---|
| 347 | raise CalcEvent, E_cant_combine |
|---|
| 348 | above_pos = 0 |
|---|
| 349 | else: |
|---|
| 350 | # above is normal number cell |
|---|
| 351 | above_pos = len(above.edit.edit_text) |
|---|
| 352 | above.edit.set_edit_text( above.edit.edit_text + |
|---|
| 353 | edit.edit_text ) |
|---|
| 354 | |
|---|
| 355 | self.move_focus_prev( size ) |
|---|
| 356 | self.content[i-1].edit.set_edit_pos(above_pos) |
|---|
| 357 | del self.content[i] |
|---|
| 358 | changed = self.update_results() |
|---|
| 359 | if changed: |
|---|
| 360 | raise UpdateParentEvent() |
|---|
| 361 | return |
|---|
| 362 | |
|---|
| 363 | elif key == 'delete': |
|---|
| 364 | # pull text from next cell into current |
|---|
| 365 | cell = self.walker.get_cell(i) |
|---|
| 366 | below = self.walker.get_cell(i+1) |
|---|
| 367 | if cell.child is not None: |
|---|
| 368 | # this cell is a parent |
|---|
| 369 | raise CalcEvent, E_cant_combine |
|---|
| 370 | if below is None: |
|---|
| 371 | # nothing below |
|---|
| 372 | return key |
|---|
| 373 | if below.child is not None: |
|---|
| 374 | # cell below is a parent |
|---|
| 375 | raise CalcEvent, E_cant_combine |
|---|
| 376 | |
|---|
| 377 | edit = self.walker.get_cell(i).edit |
|---|
| 378 | edit.set_edit_text( edit.edit_text + |
|---|
| 379 | below.edit.edit_text ) |
|---|
| 380 | |
|---|
| 381 | del self.content[i+1] |
|---|
| 382 | changed = self.update_results() |
|---|
| 383 | if changed: |
|---|
| 384 | raise UpdateParentEvent() |
|---|
| 385 | return |
|---|
| 386 | return key |
|---|
| 387 | |
|---|
| 388 | |
|---|
| 389 | def move_focus_next(self, size): |
|---|
| 390 | f, (i, sub) = self.walker.get_focus() |
|---|
| 391 | assert i<len(self.content)-1 |
|---|
| 392 | |
|---|
| 393 | ni = i |
|---|
| 394 | while ni == i: |
|---|
| 395 | self.frame.keypress(size, 'down') |
|---|
| 396 | nf, (ni, nsub) = self.walker.get_focus() |
|---|
| 397 | |
|---|
| 398 | def move_focus_prev(self, size): |
|---|
| 399 | f, (i, sub) = self.walker.get_focus() |
|---|
| 400 | assert i>0 |
|---|
| 401 | |
|---|
| 402 | ni = i |
|---|
| 403 | while ni == i: |
|---|
| 404 | self.frame.keypress(size, 'up') |
|---|
| 405 | nf, (ni, nsub) = self.walker.get_focus() |
|---|
| 406 | |
|---|
| 407 | |
|---|
| 408 | def update_results( self, start_from=None ): |
|---|
| 409 | """Update column. Return True if final result changed. |
|---|
| 410 | |
|---|
| 411 | start_from -- Cell to start updating from or None to start from |
|---|
| 412 | the current focus (default None) |
|---|
| 413 | """ |
|---|
| 414 | |
|---|
| 415 | if start_from is None: |
|---|
| 416 | f, (i, sub) = self.walker.get_focus() |
|---|
| 417 | else: |
|---|
| 418 | i = self.content.index(start_from) |
|---|
| 419 | if i == None: return False |
|---|
| 420 | |
|---|
| 421 | focus_cell = self.walker.get_cell(i) |
|---|
| 422 | |
|---|
| 423 | if focus_cell.is_top: |
|---|
| 424 | x = focus_cell.get_value() |
|---|
| 425 | last_op = None |
|---|
| 426 | else: |
|---|
| 427 | last_cell = self.walker.get_cell(i-1) |
|---|
| 428 | x = last_cell.get_result() |
|---|
| 429 | |
|---|
| 430 | if x is not None and focus_cell.op is not None: |
|---|
| 431 | x = OPERATORS[focus_cell.op]( x, |
|---|
| 432 | focus_cell.get_value() ) |
|---|
| 433 | focus_cell.set_result(x) |
|---|
| 434 | |
|---|
| 435 | for cell in self.content[i+1:]: |
|---|
| 436 | if cell.op is None: |
|---|
| 437 | x = None |
|---|
| 438 | if x is not None: |
|---|
| 439 | x = OPERATORS[cell.op]( x, cell.get_value() ) |
|---|
| 440 | if cell.get_result() == x: |
|---|
| 441 | return False |
|---|
| 442 | cell.set_result(x) |
|---|
| 443 | |
|---|
| 444 | return True |
|---|
| 445 | |
|---|
| 446 | |
|---|
| 447 | def create_child( self, letter ): |
|---|
| 448 | """Return (parent cell,child column) or None,None on failure.""" |
|---|
| 449 | f, (i, sub) = self.walker.get_focus() |
|---|
| 450 | if sub != 0: |
|---|
| 451 | # f is not an edit widget |
|---|
| 452 | return None, None |
|---|
| 453 | |
|---|
| 454 | cell = self.walker.get_cell(i) |
|---|
| 455 | if cell.child is not None: |
|---|
| 456 | raise CalcEvent, E_new_col_cell_not_empty |
|---|
| 457 | if cell.edit.edit_text: |
|---|
| 458 | raise CalcEvent, E_new_col_cell_not_empty |
|---|
| 459 | |
|---|
| 460 | child = CellColumn( letter ) |
|---|
| 461 | cell.become_parent( child, letter ) |
|---|
| 462 | |
|---|
| 463 | return cell, child |
|---|
| 464 | |
|---|
| 465 | def is_empty( self ): |
|---|
| 466 | """Return True if this column is empty.""" |
|---|
| 467 | |
|---|
| 468 | return len(self.content)==1 and self.content[0].is_empty() |
|---|
| 469 | |
|---|
| 470 | |
|---|
| 471 | def get_expression(self): |
|---|
| 472 | """Return the expression as a printable string.""" |
|---|
| 473 | |
|---|
| 474 | l = [] |
|---|
| 475 | for c in self.content: |
|---|
| 476 | if c.op is not None: # only applies to first cell |
|---|
| 477 | l.append(c.op) |
|---|
| 478 | if c.child is not None: |
|---|
| 479 | l.append("("+c.child.get_expression()+")") |
|---|
| 480 | else: |
|---|
| 481 | l.append("%d"%c.get_value()) |
|---|
| 482 | |
|---|
| 483 | return "".join(l) |
|---|
| 484 | |
|---|
| 485 | def get_result(self): |
|---|
| 486 | """Return the result of the last cell in the column.""" |
|---|
| 487 | |
|---|
| 488 | return self.content[-1].get_result() |
|---|
| 489 | |
|---|
| 490 | |
|---|
| 491 | |
|---|
| 492 | |
|---|
| 493 | class HelpColumn(urwid.BoxWidget): |
|---|
| 494 | help_text = [ |
|---|
| 495 | ('title', "Column Calculator"), |
|---|
| 496 | "", |
|---|
| 497 | [ "Numbers: ", ('key', "0"), "-", ('key', "9") ], |
|---|
| 498 | "" , |
|---|
| 499 | [ "Operators: ",('key', "+"), ", ", ('key', "-"), ", ", |
|---|
| 500 | ('key', "*"), " and ", ('key', "/")], |
|---|
| 501 | "", |
|---|
| 502 | [ "Editing: ", ('key', "BACKSPACE"), " and ",('key', "DELETE")], |
|---|
| 503 | "", |
|---|
| 504 | [ "Movement: ", ('key', "UP"), ", ", ('key', "DOWN"), ", ", |
|---|
| 505 | ('key', "LEFT"), ", ", ('key', "RIGHT"), ", ", |
|---|
| 506 | ('key', "PAGE UP"), " and ", ('key', "PAGE DOWN") ], |
|---|
| 507 | "", |
|---|
| 508 | [ "Sub-expressions: ", ('key', "("), " and ", ('key', ")") ], |
|---|
| 509 | "", |
|---|
| 510 | [ "Columns: ", ('key', COLUMN_KEYS[0]), " and ", |
|---|
| 511 | ('key',COLUMN_KEYS[1]), "-", |
|---|
| 512 | ('key',COLUMN_KEYS[-1]) ], |
|---|
| 513 | "", |
|---|
| 514 | [ "Exit: ", ('key', "Q") ], |
|---|
| 515 | "", |
|---|
| 516 | "", |
|---|
| 517 | ["Column Calculator does operations in the order they are ", |
|---|
| 518 | "typed, not by following usual precedence rules. ", |
|---|
| 519 | "If you want to calculate ", ('key', "12 - 2 * 3"), |
|---|
| 520 | " with the multiplication happening before the ", |
|---|
| 521 | "subtraction you must type ", |
|---|
| 522 | ('key', "12 - (2 * 3)"), " instead."], |
|---|
| 523 | ] |
|---|
| 524 | |
|---|
| 525 | def __init__(self): |
|---|
| 526 | self.head = urwid.AttrWrap( |
|---|
| 527 | urwid.Text(["Help Column ", ('key',"?")], |
|---|
| 528 | layout = CALC_LAYOUT), |
|---|
| 529 | 'help') |
|---|
| 530 | self.foot = urwid.AttrWrap( |
|---|
| 531 | urwid.Text(["[text continues.. press ", |
|---|
| 532 | ('key',"?"), " then scroll]"]), 'helpnote' ) |
|---|
| 533 | self.items = [urwid.Text(x) for x in self.help_text] |
|---|
| 534 | self.listbox = urwid.ListBox(urwid.SimpleListWalker(self.items)) |
|---|
| 535 | self.body = urwid.AttrWrap( self.listbox, 'help' ) |
|---|
| 536 | self.frame = urwid.Frame( self.body, header=self.head) |
|---|
| 537 | |
|---|
| 538 | def render(self, size, focus=False): |
|---|
| 539 | maxcol, maxrow = size |
|---|
| 540 | head_rows = self.head.rows((maxcol,)) |
|---|
| 541 | if "bottom" in self.listbox.ends_visible( |
|---|
| 542 | (maxcol, maxrow-head_rows) ): |
|---|
| 543 | self.frame.footer = None |
|---|
| 544 | else: |
|---|
| 545 | self.frame.footer = self.foot |
|---|
| 546 | |
|---|
| 547 | return self.frame.render( (maxcol, maxrow), focus) |
|---|
| 548 | |
|---|
| 549 | def keypress( self, size, key ): |
|---|
| 550 | return self.frame.keypress( size, key ) |
|---|
| 551 | |
|---|
| 552 | |
|---|
| 553 | class CalcDisplay: |
|---|
| 554 | palette = [ |
|---|
| 555 | ('body','white', 'dark blue'), |
|---|
| 556 | ('edit','yellow', 'dark blue'), |
|---|
| 557 | ('editfocus','yellow','dark cyan', 'bold'), |
|---|
| 558 | ('key','dark cyan', 'light gray', ('standout','underline')), |
|---|
| 559 | ('title', 'white', 'light gray', ('bold','standout')), |
|---|
| 560 | ('help', 'black', 'light gray', 'standout'), |
|---|
| 561 | ('helpnote', 'dark green', 'light gray'), |
|---|
| 562 | ('colhead', 'black', 'light gray', 'standout'), |
|---|
| 563 | ('event', 'light red', 'black', 'standout'), |
|---|
| 564 | ('confirm', 'yellow', 'black', 'bold'), |
|---|
| 565 | ] |
|---|
| 566 | |
|---|
| 567 | def __init__(self): |
|---|
| 568 | self.columns = urwid.Columns([HelpColumn(), CellColumn("A")], 1) |
|---|
| 569 | self.col_list = self.columns.widget_list |
|---|
| 570 | self.columns.set_focus_column( 1 ) |
|---|
| 571 | view = urwid.AttrWrap(self.columns, 'body') |
|---|
| 572 | self.view = urwid.Frame(view) # for showing messages |
|---|
| 573 | self.col_link = {} |
|---|
| 574 | |
|---|
| 575 | def main(self): |
|---|
| 576 | self.loop = urwid.MainLoop(self.view, self.palette, screen=Screen(), |
|---|
| 577 | input_filter=self.input_filter) |
|---|
| 578 | self.loop.run() |
|---|
| 579 | |
|---|
| 580 | # on exit write the formula and the result to the console |
|---|
| 581 | expression, result = self.get_expression_result() |
|---|
| 582 | print "Paste this expression into a new Column Calculator session to continue editing:" |
|---|
| 583 | print expression |
|---|
| 584 | print "Result:", result |
|---|
| 585 | |
|---|
| 586 | def input_filter(self, input, raw_input): |
|---|
| 587 | if 'q' in input or 'Q' in input: |
|---|
| 588 | raise urwid.ExitMainLoop() |
|---|
| 589 | |
|---|
| 590 | # handle other keystrokes |
|---|
| 591 | for k in input: |
|---|
| 592 | try: |
|---|
| 593 | self.wrap_keypress(k) |
|---|
| 594 | self.event = None |
|---|
| 595 | self.view.footer = None |
|---|
| 596 | except CalcEvent, e: |
|---|
| 597 | # display any message |
|---|
| 598 | self.event = e |
|---|
| 599 | self.view.footer = e.widget() |
|---|
| 600 | |
|---|
| 601 | # remove all input from further processing by MainLoop |
|---|
| 602 | return [] |
|---|
| 603 | |
|---|
| 604 | def wrap_keypress(self, key): |
|---|
| 605 | """Handle confirmation and throw event on bad input.""" |
|---|
| 606 | |
|---|
| 607 | try: |
|---|
| 608 | key = self.keypress(key) |
|---|
| 609 | |
|---|
| 610 | except ColumnDeleteEvent, e: |
|---|
| 611 | if e.letter == COLUMN_KEYS[1]: |
|---|
| 612 | # cannot delete the first column, ignore key |
|---|
| 613 | return |
|---|
| 614 | |
|---|
| 615 | if not self.column_empty( e.letter ): |
|---|
| 616 | # need to get two in a row, so check last event |
|---|
| 617 | if not isinstance(self.event,ColumnDeleteEvent): |
|---|
| 618 | # ask for confirmation |
|---|
| 619 | raise e |
|---|
| 620 | self.delete_column(e.letter) |
|---|
| 621 | |
|---|
| 622 | except UpdateParentEvent, e: |
|---|
| 623 | self.update_parent_columns() |
|---|
| 624 | return |
|---|
| 625 | |
|---|
| 626 | if key is None: |
|---|
| 627 | return |
|---|
| 628 | |
|---|
| 629 | if self.columns.get_focus_column() == 0: |
|---|
| 630 | if key not in ('up','down','page up','page down'): |
|---|
| 631 | raise CalcEvent, E_invalid_in_help_col |
|---|
| 632 | |
|---|
| 633 | if key not in EDIT_KEYS and key not in MOVEMENT_KEYS: |
|---|
| 634 | raise CalcEvent, E_invalid_key % key.upper() |
|---|
| 635 | |
|---|
| 636 | def keypress(self, key): |
|---|
| 637 | """Handle a keystroke.""" |
|---|
| 638 | |
|---|
| 639 | self.loop.process_input([key]) |
|---|
| 640 | |
|---|
| 641 | if key.upper() in COLUMN_KEYS: |
|---|
| 642 | # column switch |
|---|
| 643 | i = COLUMN_KEYS.index(key.upper()) |
|---|
| 644 | if i >= len( self.col_list ): |
|---|
| 645 | raise CalcEvent, E_no_such_column % key.upper() |
|---|
| 646 | self.columns.set_focus_column( i ) |
|---|
| 647 | return |
|---|
| 648 | elif key == "(": |
|---|
| 649 | # open a new column |
|---|
| 650 | if len( self.col_list ) >= len(COLUMN_KEYS): |
|---|
| 651 | raise CalcEvent, E_no_more_columns |
|---|
| 652 | i = self.columns.get_focus_column() |
|---|
| 653 | if i == 0: |
|---|
| 654 | # makes no sense in help column |
|---|
| 655 | return key |
|---|
| 656 | col = self.col_list[i] |
|---|
| 657 | new_letter = COLUMN_KEYS[len(self.col_list)] |
|---|
| 658 | parent, child = col.create_child( new_letter ) |
|---|
| 659 | if child is None: |
|---|
| 660 | # something invalid in focus |
|---|
| 661 | return key |
|---|
| 662 | self.col_list.append(child) |
|---|
| 663 | self.set_link( parent, col, child ) |
|---|
| 664 | self.columns.set_focus_column(len(self.col_list)-1) |
|---|
| 665 | |
|---|
| 666 | elif key == ")": |
|---|
| 667 | i = self.columns.get_focus_column() |
|---|
| 668 | if i == 0: |
|---|
| 669 | # makes no sense in help column |
|---|
| 670 | return key |
|---|
| 671 | col = self.col_list[i] |
|---|
| 672 | parent, pcol = self.get_parent( col ) |
|---|
| 673 | if parent is None: |
|---|
| 674 | # column has no parent |
|---|
| 675 | raise CalcEvent, E_no_parent_column |
|---|
| 676 | |
|---|
| 677 | new_i = self.col_list.index( pcol ) |
|---|
| 678 | self.columns.set_focus_column( new_i ) |
|---|
| 679 | else: |
|---|
| 680 | return key |
|---|
| 681 | |
|---|
| 682 | def set_link( self, parent, pcol, child ): |
|---|
| 683 | """Store the link between a parent cell and child column. |
|---|
| 684 | |
|---|
| 685 | parent -- parent Cell object |
|---|
| 686 | pcol -- CellColumn where parent resides |
|---|
| 687 | child -- child CellColumn object""" |
|---|
| 688 | |
|---|
| 689 | self.col_link[ child ] = parent, pcol |
|---|
| 690 | |
|---|
| 691 | def get_parent( self, child ): |
|---|
| 692 | """Return the parent and parent column for a given column.""" |
|---|
| 693 | |
|---|
| 694 | return self.col_link.get( child, (None,None) ) |
|---|
| 695 | |
|---|
| 696 | def column_empty(self, letter): |
|---|
| 697 | """Return True if the column passed is empty.""" |
|---|
| 698 | |
|---|
| 699 | i = COLUMN_KEYS.index(letter) |
|---|
| 700 | col = self.col_list[i] |
|---|
| 701 | return col.is_empty() |
|---|
| 702 | |
|---|
| 703 | |
|---|
| 704 | def delete_column(self, letter): |
|---|
| 705 | """Delete the column with the given letter.""" |
|---|
| 706 | |
|---|
| 707 | i = COLUMN_KEYS.index(letter) |
|---|
| 708 | col = self.col_list[i] |
|---|
| 709 | |
|---|
| 710 | parent, pcol = self.get_parent( col ) |
|---|
| 711 | |
|---|
| 712 | f = self.columns.get_focus_column() |
|---|
| 713 | if f == i: |
|---|
| 714 | # move focus to the parent column |
|---|
| 715 | f = self.col_list.index(pcol) |
|---|
| 716 | self.columns.set_focus_column(f) |
|---|
| 717 | |
|---|
| 718 | parent.remove_child() |
|---|
| 719 | pcol.update_results(parent) |
|---|
| 720 | del self.col_list[i] |
|---|
| 721 | |
|---|
| 722 | # delete children of this column |
|---|
| 723 | keep_right_cols = [] |
|---|
| 724 | remove_cols = [col] |
|---|
| 725 | for rcol in self.col_list[i:]: |
|---|
| 726 | parent, pcol = self.get_parent( rcol ) |
|---|
| 727 | if pcol in remove_cols: |
|---|
| 728 | remove_cols.append( rcol ) |
|---|
| 729 | else: |
|---|
| 730 | keep_right_cols.append( rcol ) |
|---|
| 731 | for rc in remove_cols: |
|---|
| 732 | # remove the links |
|---|
| 733 | del self.col_link[rc] |
|---|
| 734 | # keep only the non-children |
|---|
| 735 | self.col_list[i:] = keep_right_cols |
|---|
| 736 | |
|---|
| 737 | # fix the letter assigmnents |
|---|
| 738 | for j in range(i, len(self.col_list)): |
|---|
| 739 | col = self.col_list[j] |
|---|
| 740 | # fix the column heading |
|---|
| 741 | col.set_letter( COLUMN_KEYS[j] ) |
|---|
| 742 | parent, pcol = self.get_parent( col ) |
|---|
| 743 | # fix the parent cell |
|---|
| 744 | parent.edit.set_letter( COLUMN_KEYS[j] ) |
|---|
| 745 | |
|---|
| 746 | def update_parent_columns(self): |
|---|
| 747 | "Update the parent columns of the current focus column." |
|---|
| 748 | |
|---|
| 749 | f = self.columns.get_focus_column() |
|---|
| 750 | col = self.col_list[f] |
|---|
| 751 | while 1: |
|---|
| 752 | parent, pcol = self.get_parent(col) |
|---|
| 753 | if pcol is None: |
|---|
| 754 | return |
|---|
| 755 | |
|---|
| 756 | changed = pcol.update_results( start_from = parent ) |
|---|
| 757 | if not changed: |
|---|
| 758 | return |
|---|
| 759 | col = pcol |
|---|
| 760 | |
|---|
| 761 | |
|---|
| 762 | def get_expression_result(self): |
|---|
| 763 | """Return (expression, result) as strings.""" |
|---|
| 764 | |
|---|
| 765 | col = self.col_list[1] |
|---|
| 766 | return col.get_expression(), "%d"%col.get_result() |
|---|
| 767 | |
|---|
| 768 | |
|---|
| 769 | |
|---|
| 770 | class CalcNumLayout(urwid.TextLayout): |
|---|
| 771 | """ |
|---|
| 772 | TextLayout class for bottom-right aligned numbers with a space on |
|---|
| 773 | the last line for the cursor. |
|---|
| 774 | """ |
|---|
| 775 | def layout( self, text, width, align, wrap ): |
|---|
| 776 | """ |
|---|
| 777 | Return layout structure for calculator number display. |
|---|
| 778 | """ |
|---|
| 779 | lt = len(text) + 1 # extra space for cursor |
|---|
| 780 | r = (lt) % width # remaining segment not full width wide |
|---|
| 781 | linestarts = range( r, lt, width ) |
|---|
| 782 | l = [] |
|---|
| 783 | if linestarts: |
|---|
| 784 | if r: |
|---|
| 785 | # right-align the remaining segment on 1st line |
|---|
| 786 | l.append( [(width-r,None),(r, 0, r)] ) |
|---|
| 787 | # fill all but the last line |
|---|
| 788 | for x in linestarts[:-1]: |
|---|
| 789 | l.append( [(width, x, x+width)] ) |
|---|
| 790 | s = linestarts[-1] |
|---|
| 791 | # add the last line with a cursor hint |
|---|
| 792 | l.append( [(width-1, s, lt-1), (0, lt-1)] ) |
|---|
| 793 | elif lt-1: |
|---|
| 794 | # all fits on one line, so right align the text |
|---|
| 795 | # with a cursor hint at the end |
|---|
| 796 | l.append( [(width-lt,None),(lt-1,0,lt-1), (0,lt-1)] ) |
|---|
| 797 | else: |
|---|
| 798 | # nothing on the line, right align a cursor hint |
|---|
| 799 | l.append( [(width-1,None),(0,0)] ) |
|---|
| 800 | |
|---|
| 801 | return l |
|---|
| 802 | |
|---|
| 803 | |
|---|
| 804 | |
|---|
| 805 | |
|---|
| 806 | def main(): |
|---|
| 807 | """Launch Column Calculator.""" |
|---|
| 808 | global CALC_LAYOUT |
|---|
| 809 | CALC_LAYOUT = CalcNumLayout() |
|---|
| 810 | |
|---|
| 811 | urwid.web_display.set_preferences("Column Calculator") |
|---|
| 812 | # try to handle short web requests quickly |
|---|
| 813 | if urwid.web_display.handle_short_request(): |
|---|
| 814 | return |
|---|
| 815 | |
|---|
| 816 | CalcDisplay().main() |
|---|
| 817 | |
|---|
| 818 | if '__main__'==__name__ or urwid.web_display.is_web_request(): |
|---|
| 819 | main() |
|---|