##// END OF EJS Templates
The input object can now be passed to the constructor of ibrowse/igrid....
walter.doerwald -
Show More
@@ -1,1765 +1,1769 b''
1 1 # -*- coding: iso-8859-1 -*-
2 2
3 3 import curses, fcntl, signal, struct, tty, textwrap, inspect
4 4
5 5 from IPython import ipapi
6 6
7 7 import astyle, ipipe
8 8
9 9
10 10 # Python 2.3 compatibility
11 11 try:
12 12 set
13 13 except NameError:
14 14 import sets
15 15 set = sets.Set
16 16
17 17 # Python 2.3 compatibility
18 18 try:
19 19 sorted
20 20 except NameError:
21 21 from ipipe import sorted
22 22
23 23
24 24 class UnassignedKeyError(Exception):
25 25 """
26 26 Exception that is used for reporting unassigned keys.
27 27 """
28 28
29 29
30 30 class UnknownCommandError(Exception):
31 31 """
32 32 Exception that is used for reporting unknown commands (this should never
33 33 happen).
34 34 """
35 35
36 36
37 37 class CommandError(Exception):
38 38 """
39 39 Exception that is used for reporting that a command can't be executed.
40 40 """
41 41
42 42
43 43 class Keymap(dict):
44 44 """
45 45 Stores mapping of keys to commands.
46 46 """
47 47 def __init__(self):
48 48 self._keymap = {}
49 49
50 50 def __setitem__(self, key, command):
51 51 if isinstance(key, str):
52 52 for c in key:
53 53 dict.__setitem__(self, ord(c), command)
54 54 else:
55 55 dict.__setitem__(self, key, command)
56 56
57 57 def __getitem__(self, key):
58 58 if isinstance(key, str):
59 59 key = ord(key)
60 60 return dict.__getitem__(self, key)
61 61
62 62 def __detitem__(self, key):
63 63 if isinstance(key, str):
64 64 key = ord(key)
65 65 dict.__detitem__(self, key)
66 66
67 67 def register(self, command, *keys):
68 68 for key in keys:
69 69 self[key] = command
70 70
71 71 def get(self, key, default=None):
72 72 if isinstance(key, str):
73 73 key = ord(key)
74 74 return dict.get(self, key, default)
75 75
76 76 def findkey(self, command, default=ipipe.noitem):
77 77 for (key, commandcandidate) in self.iteritems():
78 78 if commandcandidate == command:
79 79 return key
80 80 if default is ipipe.noitem:
81 81 raise KeyError(command)
82 82 return default
83 83
84 84
85 85 class _BrowserCachedItem(object):
86 86 # This is used internally by ``ibrowse`` to store a item together with its
87 87 # marked status.
88 88 __slots__ = ("item", "marked")
89 89
90 90 def __init__(self, item):
91 91 self.item = item
92 92 self.marked = False
93 93
94 94
95 95 class _BrowserHelp(object):
96 96 style_header = astyle.Style.fromstr("yellow:black:bold")
97 97 # This is used internally by ``ibrowse`` for displaying the help screen.
98 98 def __init__(self, browser):
99 99 self.browser = browser
100 100
101 101 def __xrepr__(self, mode):
102 102 yield (-1, True)
103 103 if mode == "header" or mode == "footer":
104 104 yield (astyle.style_default, "ibrowse help screen")
105 105 else:
106 106 yield (astyle.style_default, repr(self))
107 107
108 108 def __iter__(self):
109 109 # Get reverse key mapping
110 110 allkeys = {}
111 111 for (key, cmd) in self.browser.keymap.iteritems():
112 112 allkeys.setdefault(cmd, []).append(key)
113 113
114 114 fields = ("key", "description")
115 115
116 116 commands = []
117 117 for name in dir(self.browser):
118 118 if name.startswith("cmd_"):
119 119 command = getattr(self.browser, name)
120 120 commands.append((inspect.getsourcelines(command)[-1], name[4:], command))
121 121 commands.sort()
122 122 commands = [(c[1], c[2]) for c in commands]
123 123 for (i, (name, command)) in enumerate(commands):
124 124 if i:
125 125 yield ipipe.Fields(fields, key="", description="")
126 126
127 127 description = command.__doc__
128 128 if description is None:
129 129 lines = []
130 130 else:
131 131 lines = [l.strip() for l in description.splitlines() if l.strip()]
132 132 description = "\n".join(lines)
133 133 lines = textwrap.wrap(description, 60)
134 134 keys = allkeys.get(name, [])
135 135
136 136 yield ipipe.Fields(fields, key="", description=astyle.Text((self.style_header, name)))
137 137 for i in xrange(max(len(keys), len(lines))):
138 138 try:
139 139 key = self.browser.keylabel(keys[i])
140 140 except IndexError:
141 141 key = ""
142 142 try:
143 143 line = lines[i]
144 144 except IndexError:
145 145 line = ""
146 146 yield ipipe.Fields(fields, key=key, description=line)
147 147
148 148
149 149 class _BrowserLevel(object):
150 150 # This is used internally to store the state (iterator, fetch items,
151 151 # position of cursor and screen, etc.) of one browser level
152 152 # An ``ibrowse`` object keeps multiple ``_BrowserLevel`` objects in
153 153 # a stack.
154 154 def __init__(self, browser, input, mainsizey, *attrs):
155 155 self.browser = browser
156 156 self.input = input
157 157 self.header = [x for x in ipipe.xrepr(input, "header") if not isinstance(x[0], int)]
158 158 # iterator for the input
159 159 self.iterator = ipipe.xiter(input)
160 160
161 161 # is the iterator exhausted?
162 162 self.exhausted = False
163 163
164 164 # attributes to be display (autodetected if empty)
165 165 self.attrs = attrs
166 166
167 167 # fetched items (+ marked flag)
168 168 self.items = ipipe.deque()
169 169
170 170 # Number of marked objects
171 171 self.marked = 0
172 172
173 173 # Vertical cursor position
174 174 self.cury = 0
175 175
176 176 # Horizontal cursor position
177 177 self.curx = 0
178 178
179 179 # Index of first data column
180 180 self.datastartx = 0
181 181
182 182 # Index of first data line
183 183 self.datastarty = 0
184 184
185 185 # height of the data display area
186 186 self.mainsizey = mainsizey
187 187
188 188 # width of the data display area (changes when scrolling)
189 189 self.mainsizex = 0
190 190
191 191 # Size of row number (changes when scrolling)
192 192 self.numbersizex = 0
193 193
194 194 # Attributes to display (in this order)
195 195 self.displayattrs = []
196 196
197 197 # index and attribute under the cursor
198 198 self.displayattr = (None, ipipe.noitem)
199 199
200 200 # Maps attributes to column widths
201 201 self.colwidths = {}
202 202
203 203 # Set of hidden attributes
204 204 self.hiddenattrs = set()
205 205
206 206 # This takes care of all the caches etc.
207 207 self.moveto(0, 0, refresh=True)
208 208
209 209 def fetch(self, count):
210 210 # Try to fill ``self.items`` with at least ``count`` objects.
211 211 have = len(self.items)
212 212 while not self.exhausted and have < count:
213 213 try:
214 214 item = self.iterator.next()
215 215 except StopIteration:
216 216 self.exhausted = True
217 217 break
218 218 except (KeyboardInterrupt, SystemExit):
219 219 raise
220 220 except Exception, exc:
221 221 have += 1
222 222 self.items.append(_BrowserCachedItem(exc))
223 223 self.exhausted = True
224 224 break
225 225 else:
226 226 have += 1
227 227 self.items.append(_BrowserCachedItem(item))
228 228
229 229 def calcdisplayattrs(self):
230 230 # Calculate which attributes are available from the objects that are
231 231 # currently visible on screen (and store it in ``self.displayattrs``)
232 232
233 233 attrs = set()
234 234 self.displayattrs = []
235 235 if self.attrs:
236 236 # If the browser object specifies a fixed list of attributes,
237 237 # simply use it (removing hidden attributes).
238 238 for attr in self.attrs:
239 239 attr = ipipe.upgradexattr(attr)
240 240 if attr not in attrs and attr not in self.hiddenattrs:
241 241 self.displayattrs.append(attr)
242 242 attrs.add(attr)
243 243 else:
244 244 endy = min(self.datastarty+self.mainsizey, len(self.items))
245 245 for i in xrange(self.datastarty, endy):
246 246 for attr in ipipe.xattrs(self.items[i].item, "default"):
247 247 if attr not in attrs and attr not in self.hiddenattrs:
248 248 self.displayattrs.append(attr)
249 249 attrs.add(attr)
250 250
251 251 def getrow(self, i):
252 252 # Return a dictionary with the attributes for the object
253 253 # ``self.items[i]``. Attribute names are taken from
254 254 # ``self.displayattrs`` so ``calcdisplayattrs()`` must have been
255 255 # called before.
256 256 row = {}
257 257 item = self.items[i].item
258 258 for attr in self.displayattrs:
259 259 try:
260 260 value = attr.value(item)
261 261 except (KeyboardInterrupt, SystemExit):
262 262 raise
263 263 except Exception, exc:
264 264 value = exc
265 265 # only store attribute if it exists (or we got an exception)
266 266 if value is not ipipe.noitem:
267 267 # remember alignment, length and colored text
268 268 row[attr] = ipipe.xformat(value, "cell", self.browser.maxattrlength)
269 269 return row
270 270
271 271 def calcwidths(self):
272 272 # Recalculate the displayed fields and their widths.
273 273 # ``calcdisplayattrs()'' must have been called and the cache
274 274 # for attributes of the objects on screen (``self.displayrows``)
275 275 # must have been filled. This sets ``self.colwidths`` which maps
276 276 # attribute descriptors to widths.
277 277 self.colwidths = {}
278 278 for row in self.displayrows:
279 279 for attr in self.displayattrs:
280 280 try:
281 281 length = row[attr][1]
282 282 except KeyError:
283 283 length = 0
284 284 # always add attribute to colwidths, even if it doesn't exist
285 285 if attr not in self.colwidths:
286 286 self.colwidths[attr] = len(attr.name())
287 287 newwidth = max(self.colwidths[attr], length)
288 288 self.colwidths[attr] = newwidth
289 289
290 290 # How many characters do we need to paint the largest item number?
291 291 self.numbersizex = len(str(self.datastarty+self.mainsizey-1))
292 292 # How must space have we got to display data?
293 293 self.mainsizex = self.browser.scrsizex-self.numbersizex-3
294 294 # width of all columns
295 295 self.datasizex = sum(self.colwidths.itervalues()) + len(self.colwidths)
296 296
297 297 def calcdisplayattr(self):
298 298 # Find out which attribute the cursor is on and store this
299 299 # information in ``self.displayattr``.
300 300 pos = 0
301 301 for (i, attr) in enumerate(self.displayattrs):
302 302 if pos+self.colwidths[attr] >= self.curx:
303 303 self.displayattr = (i, attr)
304 304 break
305 305 pos += self.colwidths[attr]+1
306 306 else:
307 307 self.displayattr = (None, ipipe.noitem)
308 308
309 309 def moveto(self, x, y, refresh=False):
310 310 # Move the cursor to the position ``(x,y)`` (in data coordinates,
311 311 # not in screen coordinates). If ``refresh`` is true, all cached
312 312 # values will be recalculated (e.g. because the list has been
313 313 # resorted, so screen positions etc. are no longer valid).
314 314 olddatastarty = self.datastarty
315 315 oldx = self.curx
316 316 oldy = self.cury
317 317 x = int(x+0.5)
318 318 y = int(y+0.5)
319 319 newx = x # remember where we wanted to move
320 320 newy = y # remember where we wanted to move
321 321
322 322 scrollbordery = min(self.browser.scrollbordery, self.mainsizey//2)
323 323 scrollborderx = min(self.browser.scrollborderx, self.mainsizex//2)
324 324
325 325 # Make sure that the cursor didn't leave the main area vertically
326 326 if y < 0:
327 327 y = 0
328 328 # try to get enough items to fill the screen
329 329 self.fetch(max(y+scrollbordery+1, self.mainsizey))
330 330 if y >= len(self.items):
331 331 y = max(0, len(self.items)-1)
332 332
333 333 # Make sure that the cursor stays on screen vertically
334 334 if y < self.datastarty+scrollbordery:
335 335 self.datastarty = max(0, y-scrollbordery)
336 336 elif y >= self.datastarty+self.mainsizey-scrollbordery:
337 337 self.datastarty = max(0, min(y-self.mainsizey+scrollbordery+1,
338 338 len(self.items)-self.mainsizey))
339 339
340 340 if refresh: # Do we need to refresh the complete display?
341 341 self.calcdisplayattrs()
342 342 endy = min(self.datastarty+self.mainsizey, len(self.items))
343 343 self.displayrows = map(self.getrow, xrange(self.datastarty, endy))
344 344 self.calcwidths()
345 345 # Did we scroll vertically => update displayrows
346 346 # and various other attributes
347 347 elif self.datastarty != olddatastarty:
348 348 # Recalculate which attributes we have to display
349 349 olddisplayattrs = self.displayattrs
350 350 self.calcdisplayattrs()
351 351 # If there are new attributes, recreate the cache
352 352 if self.displayattrs != olddisplayattrs:
353 353 endy = min(self.datastarty+self.mainsizey, len(self.items))
354 354 self.displayrows = map(self.getrow, xrange(self.datastarty, endy))
355 355 elif self.datastarty<olddatastarty: # we did scroll up
356 356 # drop rows from the end
357 357 del self.displayrows[self.datastarty-olddatastarty:]
358 358 # fetch new items
359 359 for i in xrange(min(olddatastarty, self.datastarty+self.mainsizey)-1,
360 360 self.datastarty-1, -1):
361 361 try:
362 362 row = self.getrow(i)
363 363 except IndexError:
364 364 # we didn't have enough objects to fill the screen
365 365 break
366 366 self.displayrows.insert(0, row)
367 367 else: # we did scroll down
368 368 # drop rows from the start
369 369 del self.displayrows[:self.datastarty-olddatastarty]
370 370 # fetch new items
371 371 for i in xrange(max(olddatastarty+self.mainsizey, self.datastarty),
372 372 self.datastarty+self.mainsizey):
373 373 try:
374 374 row = self.getrow(i)
375 375 except IndexError:
376 376 # we didn't have enough objects to fill the screen
377 377 break
378 378 self.displayrows.append(row)
379 379 self.calcwidths()
380 380
381 381 # Make sure that the cursor didn't leave the data area horizontally
382 382 if x < 0:
383 383 x = 0
384 384 elif x >= self.datasizex:
385 385 x = max(0, self.datasizex-1)
386 386
387 387 # Make sure that the cursor stays on screen horizontally
388 388 if x < self.datastartx+scrollborderx:
389 389 self.datastartx = max(0, x-scrollborderx)
390 390 elif x >= self.datastartx+self.mainsizex-scrollborderx:
391 391 self.datastartx = max(0, min(x-self.mainsizex+scrollborderx+1,
392 392 self.datasizex-self.mainsizex))
393 393
394 394 if x == oldx and y == oldy and (x != newx or y != newy): # couldn't move
395 395 self.browser.beep()
396 396 else:
397 397 self.curx = x
398 398 self.cury = y
399 399 self.calcdisplayattr()
400 400
401 401 def sort(self, key, reverse=False):
402 402 """
403 403 Sort the currently list of items using the key function ``key``. If
404 404 ``reverse`` is true the sort order is reversed.
405 405 """
406 406 curitem = self.items[self.cury] # Remember where the cursor is now
407 407
408 408 # Sort items
409 409 def realkey(item):
410 410 return key(item.item)
411 411 self.items = ipipe.deque(sorted(self.items, key=realkey, reverse=reverse))
412 412
413 413 # Find out where the object under the cursor went
414 414 cury = self.cury
415 415 for (i, item) in enumerate(self.items):
416 416 if item is curitem:
417 417 cury = i
418 418 break
419 419
420 420 self.moveto(self.curx, cury, refresh=True)
421 421
422 422 def refresh(self):
423 423 """
424 424 Restart iterating the input.
425 425 """
426 426 self.iterator = ipipe.xiter(self.input)
427 427 self.items.clear()
428 428 self.exhausted = False
429 429 self.datastartx = self.datastarty = 0
430 430 self.moveto(0, 0, refresh=True)
431 431
432 432 def refreshfind(self):
433 433 """
434 434 Restart iterating the input and go back to the same object as before
435 435 (if it can be found in the new iterator).
436 436 """
437 437 try:
438 438 oldobject = self.items[self.cury].item
439 439 except IndexError:
440 440 oldobject = ipipe.noitem
441 441 self.iterator = ipipe.xiter(self.input)
442 442 self.items.clear()
443 443 self.exhausted = False
444 444 while True:
445 445 self.fetch(len(self.items)+1)
446 446 if self.exhausted:
447 447 curses.beep()
448 448 self.datastartx = self.datastarty = 0
449 449 self.moveto(self.curx, 0, refresh=True)
450 450 break
451 451 if self.items[-1].item == oldobject:
452 452 self.datastartx = self.datastarty = 0
453 453 self.moveto(self.curx, len(self.items)-1, refresh=True)
454 454 break
455 455
456 456
457 457 class _CommandInput(object):
458 458 keymap = Keymap()
459 459 keymap.register("left", curses.KEY_LEFT)
460 460 keymap.register("right", curses.KEY_RIGHT)
461 461 keymap.register("home", curses.KEY_HOME, "\x01") # Ctrl-A
462 462 keymap.register("end", curses.KEY_END, "\x05") # Ctrl-E
463 463 # FIXME: What's happening here?
464 464 keymap.register("backspace", curses.KEY_BACKSPACE, "\x08\x7f")
465 465 keymap.register("delete", curses.KEY_DC)
466 466 keymap.register("delend", 0x0b) # Ctrl-K
467 467 keymap.register("execute", "\r\n")
468 468 keymap.register("up", curses.KEY_UP)
469 469 keymap.register("down", curses.KEY_DOWN)
470 470 keymap.register("incsearchup", curses.KEY_PPAGE)
471 471 keymap.register("incsearchdown", curses.KEY_NPAGE)
472 472 keymap.register("exit", "\x18"), # Ctrl-X
473 473
474 474 def __init__(self, prompt):
475 475 self.prompt = prompt
476 476 self.history = []
477 477 self.maxhistory = 100
478 478 self.input = ""
479 479 self.curx = 0
480 480 self.cury = -1 # blank line
481 481
482 482 def start(self):
483 483 self.input = ""
484 484 self.curx = 0
485 485 self.cury = -1 # blank line
486 486
487 487 def handlekey(self, browser, key):
488 488 cmdname = self.keymap.get(key, None)
489 489 if cmdname is not None:
490 490 cmdfunc = getattr(self, "cmd_%s" % cmdname, None)
491 491 if cmdfunc is not None:
492 492 return cmdfunc(browser)
493 493 curses.beep()
494 494 elif key != -1:
495 495 try:
496 496 char = chr(key)
497 497 except ValueError:
498 498 curses.beep()
499 499 else:
500 500 return self.handlechar(browser, char)
501 501
502 502 def handlechar(self, browser, char):
503 503 self.input = self.input[:self.curx] + char + self.input[self.curx:]
504 504 self.curx += 1
505 505 return True
506 506
507 507 def dohistory(self):
508 508 self.history.insert(0, self.input)
509 509 del self.history[:-self.maxhistory]
510 510
511 511 def cmd_backspace(self, browser):
512 512 if self.curx:
513 513 self.input = self.input[:self.curx-1] + self.input[self.curx:]
514 514 self.curx -= 1
515 515 return True
516 516 else:
517 517 curses.beep()
518 518
519 519 def cmd_delete(self, browser):
520 520 if self.curx<len(self.input):
521 521 self.input = self.input[:self.curx] + self.input[self.curx+1:]
522 522 return True
523 523 else:
524 524 curses.beep()
525 525
526 526 def cmd_delend(self, browser):
527 527 if self.curx<len(self.input):
528 528 self.input = self.input[:self.curx]
529 529 return True
530 530
531 531 def cmd_left(self, browser):
532 532 if self.curx:
533 533 self.curx -= 1
534 534 return True
535 535 else:
536 536 curses.beep()
537 537
538 538 def cmd_right(self, browser):
539 539 if self.curx < len(self.input):
540 540 self.curx += 1
541 541 return True
542 542 else:
543 543 curses.beep()
544 544
545 545 def cmd_home(self, browser):
546 546 if self.curx:
547 547 self.curx = 0
548 548 return True
549 549 else:
550 550 curses.beep()
551 551
552 552 def cmd_end(self, browser):
553 553 if self.curx < len(self.input):
554 554 self.curx = len(self.input)
555 555 return True
556 556 else:
557 557 curses.beep()
558 558
559 559 def cmd_up(self, browser):
560 560 if self.cury < len(self.history)-1:
561 561 self.cury += 1
562 562 self.input = self.history[self.cury]
563 563 self.curx = len(self.input)
564 564 return True
565 565 else:
566 566 curses.beep()
567 567
568 568 def cmd_down(self, browser):
569 569 if self.cury >= 0:
570 570 self.cury -= 1
571 571 if self.cury>=0:
572 572 self.input = self.history[self.cury]
573 573 else:
574 574 self.input = ""
575 575 self.curx = len(self.input)
576 576 return True
577 577 else:
578 578 curses.beep()
579 579
580 580 def cmd_incsearchup(self, browser):
581 581 prefix = self.input[:self.curx]
582 582 cury = self.cury
583 583 while True:
584 584 cury += 1
585 585 if cury >= len(self.history):
586 586 break
587 587 if self.history[cury].startswith(prefix):
588 588 self.input = self.history[cury]
589 589 self.cury = cury
590 590 return True
591 591 curses.beep()
592 592
593 593 def cmd_incsearchdown(self, browser):
594 594 prefix = self.input[:self.curx]
595 595 cury = self.cury
596 596 while True:
597 597 cury -= 1
598 598 if cury <= 0:
599 599 break
600 600 if self.history[cury].startswith(prefix):
601 601 self.input = self.history[self.cury]
602 602 self.cury = cury
603 603 return True
604 604 curses.beep()
605 605
606 606 def cmd_exit(self, browser):
607 607 browser.mode = "default"
608 608 return True
609 609
610 610 def cmd_execute(self, browser):
611 611 raise NotImplementedError
612 612
613 613
614 614 class _CommandGoto(_CommandInput):
615 615 def __init__(self):
616 616 _CommandInput.__init__(self, "goto object #")
617 617
618 618 def handlechar(self, browser, char):
619 619 # Only accept digits
620 620 if not "0" <= char <= "9":
621 621 curses.beep()
622 622 else:
623 623 return _CommandInput.handlechar(self, browser, char)
624 624
625 625 def cmd_execute(self, browser):
626 626 level = browser.levels[-1]
627 627 if self.input:
628 628 self.dohistory()
629 629 level.moveto(level.curx, int(self.input))
630 630 browser.mode = "default"
631 631 return True
632 632
633 633
634 634 class _CommandFind(_CommandInput):
635 635 def __init__(self):
636 636 _CommandInput.__init__(self, "find expression")
637 637
638 638 def cmd_execute(self, browser):
639 639 level = browser.levels[-1]
640 640 if self.input:
641 641 self.dohistory()
642 642 while True:
643 643 cury = level.cury
644 644 level.moveto(level.curx, cury+1)
645 645 if cury == level.cury:
646 646 curses.beep()
647 647 break # hit end
648 648 item = level.items[level.cury].item
649 649 try:
650 650 globals = ipipe.getglobals(None)
651 651 if eval(self.input, globals, ipipe.AttrNamespace(item)):
652 652 break # found something
653 653 except (KeyboardInterrupt, SystemExit):
654 654 raise
655 655 except Exception, exc:
656 656 browser.report(exc)
657 657 curses.beep()
658 658 break # break on error
659 659 browser.mode = "default"
660 660 return True
661 661
662 662
663 663 class _CommandFindBackwards(_CommandInput):
664 664 def __init__(self):
665 665 _CommandInput.__init__(self, "find backwards expression")
666 666
667 667 def cmd_execute(self, browser):
668 668 level = browser.levels[-1]
669 669 if self.input:
670 670 self.dohistory()
671 671 while level.cury:
672 672 level.moveto(level.curx, level.cury-1)
673 673 item = level.items[level.cury].item
674 674 try:
675 675 globals = ipipe.getglobals(None)
676 676 if eval(self.input, globals, ipipe.AttrNamespace(item)):
677 677 break # found something
678 678 except (KeyboardInterrupt, SystemExit):
679 679 raise
680 680 except Exception, exc:
681 681 browser.report(exc)
682 682 curses.beep()
683 683 break # break on error
684 684 else:
685 685 curses.beep()
686 686 browser.mode = "default"
687 687 return True
688 688
689 689
690 690 class ibrowse(ipipe.Display):
691 691 # Show this many lines from the previous screen when paging horizontally
692 692 pageoverlapx = 1
693 693
694 694 # Show this many lines from the previous screen when paging vertically
695 695 pageoverlapy = 1
696 696
697 697 # Start scrolling when the cursor is less than this number of columns
698 698 # away from the left or right screen edge
699 699 scrollborderx = 10
700 700
701 701 # Start scrolling when the cursor is less than this number of lines
702 702 # away from the top or bottom screen edge
703 703 scrollbordery = 5
704 704
705 705 # Accelerate by this factor when scrolling horizontally
706 706 acceleratex = 1.05
707 707
708 708 # Accelerate by this factor when scrolling vertically
709 709 acceleratey = 1.05
710 710
711 711 # The maximum horizontal scroll speed
712 712 # (as a factor of the screen width (i.e. 0.5 == half a screen width)
713 713 maxspeedx = 0.5
714 714
715 715 # The maximum vertical scroll speed
716 716 # (as a factor of the screen height (i.e. 0.5 == half a screen height)
717 717 maxspeedy = 0.5
718 718
719 719 # The maximum number of header lines for browser level
720 720 # if the nesting is deeper, only the innermost levels are displayed
721 721 maxheaders = 5
722 722
723 723 # The approximate maximum length of a column entry
724 724 maxattrlength = 200
725 725
726 726 # Styles for various parts of the GUI
727 727 style_objheadertext = astyle.Style.fromstr("white:black:bold|reverse")
728 728 style_objheadernumber = astyle.Style.fromstr("white:blue:bold|reverse")
729 729 style_objheaderobject = astyle.Style.fromstr("white:black:reverse")
730 730 style_colheader = astyle.Style.fromstr("blue:white:reverse")
731 731 style_colheaderhere = astyle.Style.fromstr("green:black:bold|reverse")
732 732 style_colheadersep = astyle.Style.fromstr("blue:black:reverse")
733 733 style_number = astyle.Style.fromstr("blue:white:reverse")
734 734 style_numberhere = astyle.Style.fromstr("green:black:bold|reverse")
735 735 style_sep = astyle.Style.fromstr("blue:black")
736 736 style_data = astyle.Style.fromstr("white:black")
737 737 style_datapad = astyle.Style.fromstr("blue:black:bold")
738 738 style_footer = astyle.Style.fromstr("black:white")
739 739 style_report = astyle.Style.fromstr("white:black")
740 740
741 741 # Column separator in header
742 742 headersepchar = "|"
743 743
744 744 # Character for padding data cell entries
745 745 datapadchar = "."
746 746
747 747 # Column separator in data area
748 748 datasepchar = "|"
749 749
750 750 # Character to use for "empty" cell (i.e. for non-existing attributes)
751 751 nodatachar = "-"
752 752
753 753 # Prompts for modes that require keyboard input
754 754 prompts = {
755 755 "goto": _CommandGoto(),
756 756 "find": _CommandFind(),
757 757 "findbackwards": _CommandFindBackwards()
758 758 }
759 759
760 760 # Maps curses key codes to "function" names
761 761 keymap = Keymap()
762 762 keymap.register("quit", "q")
763 763 keymap.register("up", curses.KEY_UP)
764 764 keymap.register("down", curses.KEY_DOWN)
765 765 keymap.register("pageup", curses.KEY_PPAGE)
766 766 keymap.register("pagedown", curses.KEY_NPAGE)
767 767 keymap.register("left", curses.KEY_LEFT)
768 768 keymap.register("right", curses.KEY_RIGHT)
769 769 keymap.register("home", curses.KEY_HOME, "\x01")
770 770 keymap.register("end", curses.KEY_END, "\x05")
771 771 keymap.register("prevattr", "<\x1b")
772 772 keymap.register("nextattr", ">\t")
773 773 keymap.register("pick", "p")
774 774 keymap.register("pickattr", "P")
775 775 keymap.register("pickallattrs", "C")
776 776 keymap.register("pickmarked", "m")
777 777 keymap.register("pickmarkedattr", "M")
778 778 keymap.register("pickinput", "i")
779 779 keymap.register("pickinputattr", "I")
780 780 keymap.register("hideattr", "h")
781 781 keymap.register("unhideattrs", "H")
782 782 keymap.register("help", "?")
783 783 keymap.register("enter", "\r\n")
784 784 keymap.register("enterattr", "E")
785 785 # FIXME: What's happening here?
786 786 keymap.register("leave", curses.KEY_BACKSPACE, "x\x08\x7f")
787 787 keymap.register("detail", "d")
788 788 keymap.register("detailattr", "D")
789 789 keymap.register("tooglemark", " ")
790 790 keymap.register("markrange", "%")
791 791 keymap.register("sortattrasc", "v")
792 792 keymap.register("sortattrdesc", "V")
793 793 keymap.register("goto", "g")
794 794 keymap.register("find", "f")
795 795 keymap.register("findbackwards", "b")
796 796 keymap.register("refresh", "r")
797 797 keymap.register("refreshfind", "R")
798 798
799 def __init__(self, *attrs):
799 def __init__(self, input=None, attrs=None):
800 800 """
801 801 Create a new browser. If ``attrs`` is not empty, it is the list
802 802 of attributes that will be displayed in the browser, otherwise
803 803 these will be determined by the objects on screen.
804 804 """
805 self.input = input
806
807 if attrs is None:
808 attrs = ()
805 809 self.attrs = attrs
806 810
807 811 # Stack of browser levels
808 812 self.levels = []
809 813 # how many colums to scroll (Changes when accelerating)
810 814 self.stepx = 1.
811 815
812 816 # how many rows to scroll (Changes when accelerating)
813 817 self.stepy = 1.
814 818
815 819 # Beep on the edges of the data area? (Will be set to ``False``
816 820 # once the cursor hits the edge of the screen, so we don't get
817 821 # multiple beeps).
818 822 self._dobeep = True
819 823
820 824 # Cache for registered ``curses`` colors and styles.
821 825 self._styles = {}
822 826 self._colors = {}
823 827 self._maxcolor = 1
824 828
825 829 # How many header lines do we want to paint (the numbers of levels
826 830 # we have, but with an upper bound)
827 831 self._headerlines = 1
828 832
829 833 # Index of first header line
830 834 self._firstheaderline = 0
831 835
832 836 # curses window
833 837 self.scr = None
834 838 # report in the footer line (error, executed command etc.)
835 839 self._report = None
836 840
837 841 # value to be returned to the caller (set by commands)
838 842 self.returnvalue = None
839 843
840 844 # The mode the browser is in
841 845 # e.g. normal browsing or entering an argument for a command
842 846 self.mode = "default"
843 847
844 848 # set by the SIGWINCH signal handler
845 849 self.resized = False
846 850
847 851 def nextstepx(self, step):
848 852 """
849 853 Accelerate horizontally.
850 854 """
851 855 return max(1., min(step*self.acceleratex,
852 856 self.maxspeedx*self.levels[-1].mainsizex))
853 857
854 858 def nextstepy(self, step):
855 859 """
856 860 Accelerate vertically.
857 861 """
858 862 return max(1., min(step*self.acceleratey,
859 863 self.maxspeedy*self.levels[-1].mainsizey))
860 864
861 865 def getstyle(self, style):
862 866 """
863 867 Register the ``style`` with ``curses`` or get it from the cache,
864 868 if it has been registered before.
865 869 """
866 870 try:
867 871 return self._styles[style.fg, style.bg, style.attrs]
868 872 except KeyError:
869 873 attrs = 0
870 874 for b in astyle.A2CURSES:
871 875 if style.attrs & b:
872 876 attrs |= astyle.A2CURSES[b]
873 877 try:
874 878 color = self._colors[style.fg, style.bg]
875 879 except KeyError:
876 880 curses.init_pair(
877 881 self._maxcolor,
878 882 astyle.COLOR2CURSES[style.fg],
879 883 astyle.COLOR2CURSES[style.bg]
880 884 )
881 885 color = curses.color_pair(self._maxcolor)
882 886 self._colors[style.fg, style.bg] = color
883 887 self._maxcolor += 1
884 888 c = color | attrs
885 889 self._styles[style.fg, style.bg, style.attrs] = c
886 890 return c
887 891
888 892 def addstr(self, y, x, begx, endx, text, style):
889 893 """
890 894 A version of ``curses.addstr()`` that can handle ``x`` coordinates
891 895 that are outside the screen.
892 896 """
893 897 text2 = text[max(0, begx-x):max(0, endx-x)]
894 898 if text2:
895 899 self.scr.addstr(y, max(x, begx), text2, self.getstyle(style))
896 900 return len(text)
897 901
898 902 def addchr(self, y, x, begx, endx, c, l, style):
899 903 x0 = max(x, begx)
900 904 x1 = min(x+l, endx)
901 905 if x1>x0:
902 906 self.scr.addstr(y, x0, c*(x1-x0), self.getstyle(style))
903 907 return l
904 908
905 909 def _calcheaderlines(self, levels):
906 910 # Calculate how many headerlines do we have to display, if we have
907 911 # ``levels`` browser levels
908 912 if levels is None:
909 913 levels = len(self.levels)
910 914 self._headerlines = min(self.maxheaders, levels)
911 915 self._firstheaderline = levels-self._headerlines
912 916
913 917 def getstylehere(self, style):
914 918 """
915 919 Return a style for displaying the original style ``style``
916 920 in the row the cursor is on.
917 921 """
918 922 return astyle.Style(style.fg, astyle.COLOR_BLUE, style.attrs | astyle.A_BOLD)
919 923
920 924 def report(self, msg):
921 925 """
922 926 Store the message ``msg`` for display below the footer line. This
923 927 will be displayed as soon as the screen is redrawn.
924 928 """
925 929 self._report = msg
926 930
927 931 def enter(self, item, *attrs):
928 932 """
929 933 Enter the object ``item``. If ``attrs`` is specified, it will be used
930 934 as a fixed list of attributes to display.
931 935 """
932 936 if self.levels and item is self.levels[-1].input:
933 937 curses.beep()
934 938 self.report(CommandError("Recursion on input object"))
935 939 else:
936 940 oldlevels = len(self.levels)
937 941 self._calcheaderlines(oldlevels+1)
938 942 try:
939 943 level = _BrowserLevel(
940 944 self,
941 945 item,
942 946 self.scrsizey-1-self._headerlines-2,
943 947 *attrs
944 948 )
945 949 except (KeyboardInterrupt, SystemExit):
946 950 raise
947 951 except Exception, exc:
948 952 if not self.levels:
949 953 raise
950 954 self._calcheaderlines(oldlevels)
951 955 curses.beep()
952 956 self.report(exc)
953 957 else:
954 958 self.levels.append(level)
955 959
956 960 def startkeyboardinput(self, mode):
957 961 """
958 962 Enter mode ``mode``, which requires keyboard input.
959 963 """
960 964 self.mode = mode
961 965 self.prompts[mode].start()
962 966
963 967 def keylabel(self, keycode):
964 968 """
965 969 Return a pretty name for the ``curses`` key ``keycode`` (used in the
966 970 help screen and in reports about unassigned keys).
967 971 """
968 972 if keycode <= 0xff:
969 973 specialsnames = {
970 974 ord("\n"): "RETURN",
971 975 ord(" "): "SPACE",
972 976 ord("\t"): "TAB",
973 977 ord("\x7f"): "DELETE",
974 978 ord("\x08"): "BACKSPACE",
975 979 }
976 980 if keycode in specialsnames:
977 981 return specialsnames[keycode]
978 982 elif 0x00 < keycode < 0x20:
979 983 return "CTRL-%s" % chr(keycode + 64)
980 984 return repr(chr(keycode))
981 985 for name in dir(curses):
982 986 if name.startswith("KEY_") and getattr(curses, name) == keycode:
983 987 return name
984 988 return str(keycode)
985 989
986 990 def beep(self, force=False):
987 991 if force or self._dobeep:
988 992 curses.beep()
989 993 # don't beep again (as long as the same key is pressed)
990 994 self._dobeep = False
991 995
992 996 def cmd_up(self):
993 997 """
994 998 Move the cursor to the previous row.
995 999 """
996 1000 level = self.levels[-1]
997 1001 self.report("up")
998 1002 level.moveto(level.curx, level.cury-self.stepy)
999 1003
1000 1004 def cmd_down(self):
1001 1005 """
1002 1006 Move the cursor to the next row.
1003 1007 """
1004 1008 level = self.levels[-1]
1005 1009 self.report("down")
1006 1010 level.moveto(level.curx, level.cury+self.stepy)
1007 1011
1008 1012 def cmd_pageup(self):
1009 1013 """
1010 1014 Move the cursor up one page.
1011 1015 """
1012 1016 level = self.levels[-1]
1013 1017 self.report("page up")
1014 1018 level.moveto(level.curx, level.cury-level.mainsizey+self.pageoverlapy)
1015 1019
1016 1020 def cmd_pagedown(self):
1017 1021 """
1018 1022 Move the cursor down one page.
1019 1023 """
1020 1024 level = self.levels[-1]
1021 1025 self.report("page down")
1022 1026 level.moveto(level.curx, level.cury+level.mainsizey-self.pageoverlapy)
1023 1027
1024 1028 def cmd_left(self):
1025 1029 """
1026 1030 Move the cursor left.
1027 1031 """
1028 1032 level = self.levels[-1]
1029 1033 self.report("left")
1030 1034 level.moveto(level.curx-self.stepx, level.cury)
1031 1035
1032 1036 def cmd_right(self):
1033 1037 """
1034 1038 Move the cursor right.
1035 1039 """
1036 1040 level = self.levels[-1]
1037 1041 self.report("right")
1038 1042 level.moveto(level.curx+self.stepx, level.cury)
1039 1043
1040 1044 def cmd_home(self):
1041 1045 """
1042 1046 Move the cursor to the first column.
1043 1047 """
1044 1048 level = self.levels[-1]
1045 1049 self.report("home")
1046 1050 level.moveto(0, level.cury)
1047 1051
1048 1052 def cmd_end(self):
1049 1053 """
1050 1054 Move the cursor to the last column.
1051 1055 """
1052 1056 level = self.levels[-1]
1053 1057 self.report("end")
1054 1058 level.moveto(level.datasizex+level.mainsizey-self.pageoverlapx, level.cury)
1055 1059
1056 1060 def cmd_prevattr(self):
1057 1061 """
1058 1062 Move the cursor one attribute column to the left.
1059 1063 """
1060 1064 level = self.levels[-1]
1061 1065 if level.displayattr[0] is None or level.displayattr[0] == 0:
1062 1066 self.beep()
1063 1067 else:
1064 1068 self.report("prevattr")
1065 1069 pos = 0
1066 1070 for (i, attrname) in enumerate(level.displayattrs):
1067 1071 if i == level.displayattr[0]-1:
1068 1072 break
1069 1073 pos += level.colwidths[attrname] + 1
1070 1074 level.moveto(pos, level.cury)
1071 1075
1072 1076 def cmd_nextattr(self):
1073 1077 """
1074 1078 Move the cursor one attribute column to the right.
1075 1079 """
1076 1080 level = self.levels[-1]
1077 1081 if level.displayattr[0] is None or level.displayattr[0] == len(level.displayattrs)-1:
1078 1082 self.beep()
1079 1083 else:
1080 1084 self.report("nextattr")
1081 1085 pos = 0
1082 1086 for (i, attrname) in enumerate(level.displayattrs):
1083 1087 if i == level.displayattr[0]+1:
1084 1088 break
1085 1089 pos += level.colwidths[attrname] + 1
1086 1090 level.moveto(pos, level.cury)
1087 1091
1088 1092 def cmd_pick(self):
1089 1093 """
1090 1094 'Pick' the object under the cursor (i.e. the row the cursor is on).
1091 1095 This leaves the browser and returns the picked object to the caller.
1092 1096 (In IPython this object will be available as the ``_`` variable.)
1093 1097 """
1094 1098 level = self.levels[-1]
1095 1099 self.returnvalue = level.items[level.cury].item
1096 1100 return True
1097 1101
1098 1102 def cmd_pickattr(self):
1099 1103 """
1100 1104 'Pick' the attribute under the cursor (i.e. the row/column the
1101 1105 cursor is on).
1102 1106 """
1103 1107 level = self.levels[-1]
1104 1108 attr = level.displayattr[1]
1105 1109 if attr is ipipe.noitem:
1106 1110 curses.beep()
1107 1111 self.report(CommandError("no column under cursor"))
1108 1112 return
1109 1113 value = attr.value(level.items[level.cury].item)
1110 1114 if value is ipipe.noitem:
1111 1115 curses.beep()
1112 1116 self.report(AttributeError(attr.name()))
1113 1117 else:
1114 1118 self.returnvalue = value
1115 1119 return True
1116 1120
1117 1121 def cmd_pickallattrs(self):
1118 1122 """
1119 1123 Pick' the complete column under the cursor (i.e. the attribute under
1120 1124 the cursor) from all currently fetched objects. These attributes
1121 1125 will be returned as a list.
1122 1126 """
1123 1127 level = self.levels[-1]
1124 1128 attr = level.displayattr[1]
1125 1129 if attr is ipipe.noitem:
1126 1130 curses.beep()
1127 1131 self.report(CommandError("no column under cursor"))
1128 1132 return
1129 1133 result = []
1130 1134 for cache in level.items:
1131 1135 value = attr.value(cache.item)
1132 1136 if value is not ipipe.noitem:
1133 1137 result.append(value)
1134 1138 self.returnvalue = result
1135 1139 return True
1136 1140
1137 1141 def cmd_pickmarked(self):
1138 1142 """
1139 1143 'Pick' marked objects. Marked objects will be returned as a list.
1140 1144 """
1141 1145 level = self.levels[-1]
1142 1146 self.returnvalue = [cache.item for cache in level.items if cache.marked]
1143 1147 return True
1144 1148
1145 1149 def cmd_pickmarkedattr(self):
1146 1150 """
1147 1151 'Pick' the attribute under the cursor from all marked objects
1148 1152 (This returns a list).
1149 1153 """
1150 1154
1151 1155 level = self.levels[-1]
1152 1156 attr = level.displayattr[1]
1153 1157 if attr is ipipe.noitem:
1154 1158 curses.beep()
1155 1159 self.report(CommandError("no column under cursor"))
1156 1160 return
1157 1161 result = []
1158 1162 for cache in level.items:
1159 1163 if cache.marked:
1160 1164 value = attr.value(cache.item)
1161 1165 if value is not ipipe.noitem:
1162 1166 result.append(value)
1163 1167 self.returnvalue = result
1164 1168 return True
1165 1169
1166 1170 def cmd_pickinput(self):
1167 1171 """
1168 1172 Use the object under the cursor (i.e. the row the cursor is on) as
1169 1173 the next input line. This leaves the browser and puts the picked object
1170 1174 in the input.
1171 1175 """
1172 1176 level = self.levels[-1]
1173 1177 value = level.items[level.cury].item
1174 1178 self.returnvalue = None
1175 1179 api = ipapi.get()
1176 1180 api.set_next_input(str(value))
1177 1181 return True
1178 1182
1179 1183 def cmd_pickinputattr(self):
1180 1184 """
1181 1185 Use the attribute under the cursor i.e. the row/column the cursor is on)
1182 1186 as the next input line. This leaves the browser and puts the picked
1183 1187 object in the input.
1184 1188 """
1185 1189 level = self.levels[-1]
1186 1190 attr = level.displayattr[1]
1187 1191 if attr is ipipe.noitem:
1188 1192 curses.beep()
1189 1193 self.report(CommandError("no column under cursor"))
1190 1194 return
1191 1195 value = attr.value(level.items[level.cury].item)
1192 1196 if value is ipipe.noitem:
1193 1197 curses.beep()
1194 1198 self.report(AttributeError(attr.name()))
1195 1199 self.returnvalue = None
1196 1200 api = ipapi.get()
1197 1201 api.set_next_input(str(value))
1198 1202 return True
1199 1203
1200 1204 def cmd_markrange(self):
1201 1205 """
1202 1206 Mark all objects from the last marked object before the current cursor
1203 1207 position to the cursor position.
1204 1208 """
1205 1209 level = self.levels[-1]
1206 1210 self.report("markrange")
1207 1211 start = None
1208 1212 if level.items:
1209 1213 for i in xrange(level.cury, -1, -1):
1210 1214 if level.items[i].marked:
1211 1215 start = i
1212 1216 break
1213 1217 if start is None:
1214 1218 self.report(CommandError("no mark before cursor"))
1215 1219 curses.beep()
1216 1220 else:
1217 1221 for i in xrange(start, level.cury+1):
1218 1222 cache = level.items[i]
1219 1223 if not cache.marked:
1220 1224 cache.marked = True
1221 1225 level.marked += 1
1222 1226
1223 1227 def cmd_enter(self):
1224 1228 """
1225 1229 Enter the object under the cursor. (what this mean depends on the object
1226 1230 itself (i.e. how it implements iteration). This opens a new browser 'level'.
1227 1231 """
1228 1232 level = self.levels[-1]
1229 1233 try:
1230 1234 item = level.items[level.cury].item
1231 1235 except IndexError:
1232 1236 self.report(CommandError("No object"))
1233 1237 curses.beep()
1234 1238 else:
1235 1239 self.report("entering object...")
1236 1240 self.enter(item)
1237 1241
1238 1242 def cmd_leave(self):
1239 1243 """
1240 1244 Leave the current browser level and go back to the previous one.
1241 1245 """
1242 1246 self.report("leave")
1243 1247 if len(self.levels) > 1:
1244 1248 self._calcheaderlines(len(self.levels)-1)
1245 1249 self.levels.pop(-1)
1246 1250 else:
1247 1251 self.report(CommandError("This is the last level"))
1248 1252 curses.beep()
1249 1253
1250 1254 def cmd_enterattr(self):
1251 1255 """
1252 1256 Enter the attribute under the cursor.
1253 1257 """
1254 1258 level = self.levels[-1]
1255 1259 attr = level.displayattr[1]
1256 1260 if attr is ipipe.noitem:
1257 1261 curses.beep()
1258 1262 self.report(CommandError("no column under cursor"))
1259 1263 return
1260 1264 try:
1261 1265 item = level.items[level.cury].item
1262 1266 except IndexError:
1263 1267 self.report(CommandError("No object"))
1264 1268 curses.beep()
1265 1269 else:
1266 1270 value = attr.value(item)
1267 1271 name = attr.name()
1268 1272 if value is ipipe.noitem:
1269 1273 self.report(AttributeError(name))
1270 1274 else:
1271 1275 self.report("entering object attribute %s..." % name)
1272 1276 self.enter(value)
1273 1277
1274 1278 def cmd_detail(self):
1275 1279 """
1276 1280 Show a detail view of the object under the cursor. This shows the
1277 1281 name, type, doc string and value of the object attributes (and it
1278 1282 might show more attributes than in the list view, depending on
1279 1283 the object).
1280 1284 """
1281 1285 level = self.levels[-1]
1282 1286 try:
1283 1287 item = level.items[level.cury].item
1284 1288 except IndexError:
1285 1289 self.report(CommandError("No object"))
1286 1290 curses.beep()
1287 1291 else:
1288 1292 self.report("entering detail view for object...")
1289 1293 attrs = [ipipe.AttributeDetail(item, attr) for attr in ipipe.xattrs(item, "detail")]
1290 1294 self.enter(attrs)
1291 1295
1292 1296 def cmd_detailattr(self):
1293 1297 """
1294 1298 Show a detail view of the attribute under the cursor.
1295 1299 """
1296 1300 level = self.levels[-1]
1297 1301 attr = level.displayattr[1]
1298 1302 if attr is ipipe.noitem:
1299 1303 curses.beep()
1300 1304 self.report(CommandError("no attribute"))
1301 1305 return
1302 1306 try:
1303 1307 item = level.items[level.cury].item
1304 1308 except IndexError:
1305 1309 self.report(CommandError("No object"))
1306 1310 curses.beep()
1307 1311 else:
1308 1312 try:
1309 1313 item = attr.value(item)
1310 1314 except (KeyboardInterrupt, SystemExit):
1311 1315 raise
1312 1316 except Exception, exc:
1313 1317 self.report(exc)
1314 1318 else:
1315 1319 self.report("entering detail view for attribute %s..." % attr.name())
1316 1320 attrs = [ipipe.AttributeDetail(item, attr) for attr in ipipe.xattrs(item, "detail")]
1317 1321 self.enter(attrs)
1318 1322
1319 1323 def cmd_tooglemark(self):
1320 1324 """
1321 1325 Mark/unmark the object under the cursor. Marked objects have a '!'
1322 1326 after the row number).
1323 1327 """
1324 1328 level = self.levels[-1]
1325 1329 self.report("toggle mark")
1326 1330 try:
1327 1331 item = level.items[level.cury]
1328 1332 except IndexError: # no items?
1329 1333 pass
1330 1334 else:
1331 1335 if item.marked:
1332 1336 item.marked = False
1333 1337 level.marked -= 1
1334 1338 else:
1335 1339 item.marked = True
1336 1340 level.marked += 1
1337 1341
1338 1342 def cmd_sortattrasc(self):
1339 1343 """
1340 1344 Sort the objects (in ascending order) using the attribute under
1341 1345 the cursor as the sort key.
1342 1346 """
1343 1347 level = self.levels[-1]
1344 1348 attr = level.displayattr[1]
1345 1349 if attr is ipipe.noitem:
1346 1350 curses.beep()
1347 1351 self.report(CommandError("no column under cursor"))
1348 1352 return
1349 1353 self.report("sort by %s (ascending)" % attr.name())
1350 1354 def key(item):
1351 1355 try:
1352 1356 return attr.value(item)
1353 1357 except (KeyboardInterrupt, SystemExit):
1354 1358 raise
1355 1359 except Exception:
1356 1360 return None
1357 1361 level.sort(key)
1358 1362
1359 1363 def cmd_sortattrdesc(self):
1360 1364 """
1361 1365 Sort the objects (in descending order) using the attribute under
1362 1366 the cursor as the sort key.
1363 1367 """
1364 1368 level = self.levels[-1]
1365 1369 attr = level.displayattr[1]
1366 1370 if attr is ipipe.noitem:
1367 1371 curses.beep()
1368 1372 self.report(CommandError("no column under cursor"))
1369 1373 return
1370 1374 self.report("sort by %s (descending)" % attr.name())
1371 1375 def key(item):
1372 1376 try:
1373 1377 return attr.value(item)
1374 1378 except (KeyboardInterrupt, SystemExit):
1375 1379 raise
1376 1380 except Exception:
1377 1381 return None
1378 1382 level.sort(key, reverse=True)
1379 1383
1380 1384 def cmd_hideattr(self):
1381 1385 """
1382 1386 Hide the attribute under the cursor.
1383 1387 """
1384 1388 level = self.levels[-1]
1385 1389 if level.displayattr[0] is None:
1386 1390 self.beep()
1387 1391 else:
1388 1392 self.report("hideattr")
1389 1393 level.hiddenattrs.add(level.displayattr[1])
1390 1394 level.moveto(level.curx, level.cury, refresh=True)
1391 1395
1392 1396 def cmd_unhideattrs(self):
1393 1397 """
1394 1398 Make all attributes visible again.
1395 1399 """
1396 1400 level = self.levels[-1]
1397 1401 self.report("unhideattrs")
1398 1402 level.hiddenattrs.clear()
1399 1403 level.moveto(level.curx, level.cury, refresh=True)
1400 1404
1401 1405 def cmd_goto(self):
1402 1406 """
1403 1407 Jump to a row. The row number can be entered at the
1404 1408 bottom of the screen.
1405 1409 """
1406 1410 self.startkeyboardinput("goto")
1407 1411
1408 1412 def cmd_find(self):
1409 1413 """
1410 1414 Search forward for a row. The search condition can be entered at the
1411 1415 bottom of the screen.
1412 1416 """
1413 1417 self.startkeyboardinput("find")
1414 1418
1415 1419 def cmd_findbackwards(self):
1416 1420 """
1417 1421 Search backward for a row. The search condition can be entered at the
1418 1422 bottom of the screen.
1419 1423 """
1420 1424 self.startkeyboardinput("findbackwards")
1421 1425
1422 1426 def cmd_refresh(self):
1423 1427 """
1424 1428 Refreshes the display by restarting the iterator.
1425 1429 """
1426 1430 level = self.levels[-1]
1427 1431 self.report("refresh")
1428 1432 level.refresh()
1429 1433
1430 1434 def cmd_refreshfind(self):
1431 1435 """
1432 1436 Refreshes the display by restarting the iterator and goes back to the
1433 1437 same object the cursor was on before restarting (if this object can't be
1434 1438 found the cursor jumps back to the first object).
1435 1439 """
1436 1440 level = self.levels[-1]
1437 1441 self.report("refreshfind")
1438 1442 level.refreshfind()
1439 1443
1440 1444 def cmd_help(self):
1441 1445 """
1442 1446 Opens the help screen as a new browser level, describing keyboard
1443 1447 shortcuts.
1444 1448 """
1445 1449 for level in self.levels:
1446 1450 if isinstance(level.input, _BrowserHelp):
1447 1451 curses.beep()
1448 1452 self.report(CommandError("help already active"))
1449 1453 return
1450 1454
1451 1455 self.enter(_BrowserHelp(self))
1452 1456
1453 1457 def cmd_quit(self):
1454 1458 """
1455 1459 Quit the browser and return to the IPython prompt.
1456 1460 """
1457 1461 self.returnvalue = None
1458 1462 return True
1459 1463
1460 1464 def sigwinchhandler(self, signal, frame):
1461 1465 self.resized = True
1462 1466
1463 1467 def _dodisplay(self, scr):
1464 1468 """
1465 1469 This method is the workhorse of the browser. It handles screen
1466 1470 drawing and the keyboard.
1467 1471 """
1468 1472 self.scr = scr
1469 1473 curses.halfdelay(1)
1470 1474 footery = 2
1471 1475
1472 1476 keys = []
1473 1477 for cmd in ("quit", "help"):
1474 1478 key = self.keymap.findkey(cmd, None)
1475 1479 if key is not None:
1476 1480 keys.append("%s=%s" % (self.keylabel(key), cmd))
1477 1481 helpmsg = " | %s" % " ".join(keys)
1478 1482
1479 1483 scr.clear()
1480 1484 msg = "Fetching first batch of objects..."
1481 1485 (self.scrsizey, self.scrsizex) = scr.getmaxyx()
1482 1486 scr.addstr(self.scrsizey//2, (self.scrsizex-len(msg))//2, msg)
1483 1487 scr.refresh()
1484 1488
1485 1489 lastc = -1
1486 1490
1487 1491 self.levels = []
1488 1492 # enter the first level
1489 1493 self.enter(self.input, *self.attrs)
1490 1494
1491 1495 self._calcheaderlines(None)
1492 1496
1493 1497 while True:
1494 1498 level = self.levels[-1]
1495 1499 (self.scrsizey, self.scrsizex) = scr.getmaxyx()
1496 1500 level.mainsizey = self.scrsizey-1-self._headerlines-footery
1497 1501
1498 1502 # Paint object header
1499 1503 for i in xrange(self._firstheaderline, self._firstheaderline+self._headerlines):
1500 1504 lv = self.levels[i]
1501 1505 posx = 0
1502 1506 posy = i-self._firstheaderline
1503 1507 endx = self.scrsizex
1504 1508 if i: # not the first level
1505 1509 msg = " (%d/%d" % (self.levels[i-1].cury, len(self.levels[i-1].items))
1506 1510 if not self.levels[i-1].exhausted:
1507 1511 msg += "+"
1508 1512 msg += ") "
1509 1513 endx -= len(msg)+1
1510 1514 posx += self.addstr(posy, posx, 0, endx, " ibrowse #%d: " % i, self.style_objheadertext)
1511 1515 for (style, text) in lv.header:
1512 1516 posx += self.addstr(posy, posx, 0, endx, text, self.style_objheaderobject)
1513 1517 if posx >= endx:
1514 1518 break
1515 1519 if i:
1516 1520 posx += self.addstr(posy, posx, 0, self.scrsizex, msg, self.style_objheadernumber)
1517 1521 posx += self.addchr(posy, posx, 0, self.scrsizex, " ", self.scrsizex-posx, self.style_objheadernumber)
1518 1522
1519 1523 if not level.items:
1520 1524 self.addchr(self._headerlines, 0, 0, self.scrsizex, " ", self.scrsizex, self.style_colheader)
1521 1525 self.addstr(self._headerlines+1, 0, 0, self.scrsizex, " <empty>", astyle.style_error)
1522 1526 scr.clrtobot()
1523 1527 else:
1524 1528 # Paint column headers
1525 1529 scr.move(self._headerlines, 0)
1526 1530 scr.addstr(" %*s " % (level.numbersizex, "#"), self.getstyle(self.style_colheader))
1527 1531 scr.addstr(self.headersepchar, self.getstyle(self.style_colheadersep))
1528 1532 begx = level.numbersizex+3
1529 1533 posx = begx-level.datastartx
1530 1534 for attr in level.displayattrs:
1531 1535 attrname = attr.name()
1532 1536 cwidth = level.colwidths[attr]
1533 1537 header = attrname.ljust(cwidth)
1534 1538 if attr is level.displayattr[1]:
1535 1539 style = self.style_colheaderhere
1536 1540 else:
1537 1541 style = self.style_colheader
1538 1542 posx += self.addstr(self._headerlines, posx, begx, self.scrsizex, header, style)
1539 1543 posx += self.addstr(self._headerlines, posx, begx, self.scrsizex, self.headersepchar, self.style_colheadersep)
1540 1544 if posx >= self.scrsizex:
1541 1545 break
1542 1546 else:
1543 1547 scr.addstr(" "*(self.scrsizex-posx), self.getstyle(self.style_colheader))
1544 1548
1545 1549 # Paint rows
1546 1550 posy = self._headerlines+1+level.datastarty
1547 1551 for i in xrange(level.datastarty, min(level.datastarty+level.mainsizey, len(level.items))):
1548 1552 cache = level.items[i]
1549 1553 if i == level.cury:
1550 1554 style = self.style_numberhere
1551 1555 else:
1552 1556 style = self.style_number
1553 1557
1554 1558 posy = self._headerlines+1+i-level.datastarty
1555 1559 posx = begx-level.datastartx
1556 1560
1557 1561 scr.move(posy, 0)
1558 1562 scr.addstr(" %*d%s" % (level.numbersizex, i, " !"[cache.marked]), self.getstyle(style))
1559 1563 scr.addstr(self.headersepchar, self.getstyle(self.style_sep))
1560 1564
1561 1565 for attrname in level.displayattrs:
1562 1566 cwidth = level.colwidths[attrname]
1563 1567 try:
1564 1568 (align, length, parts) = level.displayrows[i-level.datastarty][attrname]
1565 1569 except KeyError:
1566 1570 align = 2
1567 1571 style = astyle.style_nodata
1568 1572 if i == level.cury:
1569 1573 style = self.getstylehere(style)
1570 1574 padstyle = self.style_datapad
1571 1575 sepstyle = self.style_sep
1572 1576 if i == level.cury:
1573 1577 padstyle = self.getstylehere(padstyle)
1574 1578 sepstyle = self.getstylehere(sepstyle)
1575 1579 if align == 2:
1576 1580 posx += self.addchr(posy, posx, begx, self.scrsizex, self.nodatachar, cwidth, style)
1577 1581 else:
1578 1582 if align == 1:
1579 1583 posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, cwidth-length, padstyle)
1580 1584 elif align == 0:
1581 1585 pad1 = (cwidth-length)//2
1582 1586 pad2 = cwidth-length-len(pad1)
1583 1587 posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, pad1, padstyle)
1584 1588 for (style, text) in parts:
1585 1589 if i == level.cury:
1586 1590 style = self.getstylehere(style)
1587 1591 posx += self.addstr(posy, posx, begx, self.scrsizex, text, style)
1588 1592 if posx >= self.scrsizex:
1589 1593 break
1590 1594 if align == -1:
1591 1595 posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, cwidth-length, padstyle)
1592 1596 elif align == 0:
1593 1597 posx += self.addchr(posy, posx, begx, self.scrsizex, self.datapadchar, pad2, padstyle)
1594 1598 posx += self.addstr(posy, posx, begx, self.scrsizex, self.datasepchar, sepstyle)
1595 1599 else:
1596 1600 scr.clrtoeol()
1597 1601
1598 1602 # Add blank row headers for the rest of the screen
1599 1603 for posy in xrange(posy+1, self.scrsizey-2):
1600 1604 scr.addstr(posy, 0, " " * (level.numbersizex+2), self.getstyle(self.style_colheader))
1601 1605 scr.clrtoeol()
1602 1606
1603 1607 posy = self.scrsizey-footery
1604 1608 # Display footer
1605 1609 scr.addstr(posy, 0, " "*self.scrsizex, self.getstyle(self.style_footer))
1606 1610
1607 1611 if level.exhausted:
1608 1612 flag = ""
1609 1613 else:
1610 1614 flag = "+"
1611 1615
1612 1616 endx = self.scrsizex-len(helpmsg)-1
1613 1617 scr.addstr(posy, endx, helpmsg, self.getstyle(self.style_footer))
1614 1618
1615 1619 posx = 0
1616 1620 msg = " %d%s objects (%d marked): " % (len(level.items), flag, level.marked)
1617 1621 posx += self.addstr(posy, posx, 0, endx, msg, self.style_footer)
1618 1622 try:
1619 1623 item = level.items[level.cury].item
1620 1624 except IndexError: # empty
1621 1625 pass
1622 1626 else:
1623 1627 for (nostyle, text) in ipipe.xrepr(item, "footer"):
1624 1628 if not isinstance(nostyle, int):
1625 1629 posx += self.addstr(posy, posx, 0, endx, text, self.style_footer)
1626 1630 if posx >= endx:
1627 1631 break
1628 1632
1629 1633 attrstyle = [(astyle.style_default, "no attribute")]
1630 1634 attr = level.displayattr[1]
1631 1635 if attr is not ipipe.noitem and not isinstance(attr, ipipe.SelfDescriptor):
1632 1636 posx += self.addstr(posy, posx, 0, endx, " | ", self.style_footer)
1633 1637 posx += self.addstr(posy, posx, 0, endx, attr.name(), self.style_footer)
1634 1638 posx += self.addstr(posy, posx, 0, endx, ": ", self.style_footer)
1635 1639 try:
1636 1640 value = attr.value(item)
1637 1641 except (SystemExit, KeyboardInterrupt):
1638 1642 raise
1639 1643 except Exception, exc:
1640 1644 value = exc
1641 1645 if value is not ipipe.noitem:
1642 1646 attrstyle = ipipe.xrepr(value, "footer")
1643 1647 for (nostyle, text) in attrstyle:
1644 1648 if not isinstance(nostyle, int):
1645 1649 posx += self.addstr(posy, posx, 0, endx, text, self.style_footer)
1646 1650 if posx >= endx:
1647 1651 break
1648 1652
1649 1653 try:
1650 1654 # Display input prompt
1651 1655 if self.mode in self.prompts:
1652 1656 history = self.prompts[self.mode]
1653 1657 posx = 0
1654 1658 posy = self.scrsizey-1
1655 1659 posx += self.addstr(posy, posx, 0, endx, history.prompt, astyle.style_default)
1656 1660 posx += self.addstr(posy, posx, 0, endx, " [", astyle.style_default)
1657 1661 if history.cury==-1:
1658 1662 text = "new"
1659 1663 else:
1660 1664 text = str(history.cury+1)
1661 1665 posx += self.addstr(posy, posx, 0, endx, text, astyle.style_type_number)
1662 1666 if history.history:
1663 1667 posx += self.addstr(posy, posx, 0, endx, "/", astyle.style_default)
1664 1668 posx += self.addstr(posy, posx, 0, endx, str(len(history.history)), astyle.style_type_number)
1665 1669 posx += self.addstr(posy, posx, 0, endx, "]: ", astyle.style_default)
1666 1670 inputstartx = posx
1667 1671 posx += self.addstr(posy, posx, 0, endx, history.input, astyle.style_default)
1668 1672 # Display report
1669 1673 else:
1670 1674 if self._report is not None:
1671 1675 if isinstance(self._report, Exception):
1672 1676 style = self.getstyle(astyle.style_error)
1673 1677 if self._report.__class__.__module__ == "exceptions":
1674 1678 msg = "%s: %s" % \
1675 1679 (self._report.__class__.__name__, self._report)
1676 1680 else:
1677 1681 msg = "%s.%s: %s" % \
1678 1682 (self._report.__class__.__module__,
1679 1683 self._report.__class__.__name__, self._report)
1680 1684 else:
1681 1685 style = self.getstyle(self.style_report)
1682 1686 msg = self._report
1683 1687 scr.addstr(self.scrsizey-1, 0, msg[:self.scrsizex], style)
1684 1688 self._report = None
1685 1689 else:
1686 1690 scr.move(self.scrsizey-1, 0)
1687 1691 except curses.error:
1688 1692 # Protect against errors from writing to the last line
1689 1693 pass
1690 1694 scr.clrtoeol()
1691 1695
1692 1696 # Position cursor
1693 1697 if self.mode in self.prompts:
1694 1698 history = self.prompts[self.mode]
1695 1699 scr.move(self.scrsizey-1, inputstartx+history.curx)
1696 1700 else:
1697 1701 scr.move(
1698 1702 1+self._headerlines+level.cury-level.datastarty,
1699 1703 level.numbersizex+3+level.curx-level.datastartx
1700 1704 )
1701 1705 scr.refresh()
1702 1706
1703 1707 # Check keyboard
1704 1708 while True:
1705 1709 c = scr.getch()
1706 1710 if self.resized:
1707 1711 size = fcntl.ioctl(0, tty.TIOCGWINSZ, "12345678")
1708 1712 size = struct.unpack("4H", size)
1709 1713 oldsize = scr.getmaxyx()
1710 1714 scr.erase()
1711 1715 curses.resize_term(size[0], size[1])
1712 1716 newsize = scr.getmaxyx()
1713 1717 scr.erase()
1714 1718 for l in self.levels:
1715 1719 l.mainsizey += newsize[0]-oldsize[0]
1716 1720 l.moveto(l.curx, l.cury, refresh=True)
1717 1721 scr.refresh()
1718 1722 self.resized = False
1719 1723 break # Redisplay
1720 1724 if self.mode in self.prompts:
1721 1725 if self.prompts[self.mode].handlekey(self, c):
1722 1726 break # Redisplay
1723 1727 else:
1724 1728 # if no key is pressed slow down and beep again
1725 1729 if c == -1:
1726 1730 self.stepx = 1.
1727 1731 self.stepy = 1.
1728 1732 self._dobeep = True
1729 1733 else:
1730 1734 # if a different key was pressed slow down and beep too
1731 1735 if c != lastc:
1732 1736 lastc = c
1733 1737 self.stepx = 1.
1734 1738 self.stepy = 1.
1735 1739 self._dobeep = True
1736 1740 cmdname = self.keymap.get(c, None)
1737 1741 if cmdname is None:
1738 1742 self.report(
1739 1743 UnassignedKeyError("Unassigned key %s" %
1740 1744 self.keylabel(c)))
1741 1745 else:
1742 1746 cmdfunc = getattr(self, "cmd_%s" % cmdname, None)
1743 1747 if cmdfunc is None:
1744 1748 self.report(
1745 1749 UnknownCommandError("Unknown command %r" %
1746 1750 (cmdname,)))
1747 1751 elif cmdfunc():
1748 1752 returnvalue = self.returnvalue
1749 1753 self.returnvalue = None
1750 1754 return returnvalue
1751 1755 self.stepx = self.nextstepx(self.stepx)
1752 1756 self.stepy = self.nextstepy(self.stepy)
1753 1757 curses.flushinp() # get rid of type ahead
1754 1758 break # Redisplay
1755 1759 self.scr = None
1756 1760
1757 1761 def display(self):
1758 1762 if hasattr(curses, "resize_term"):
1759 1763 oldhandler = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1760 1764 try:
1761 1765 return curses.wrapper(self._dodisplay)
1762 1766 finally:
1763 1767 signal.signal(signal.SIGWINCH, oldhandler)
1764 1768 else:
1765 1769 return curses.wrapper(self._dodisplay)
@@ -1,1125 +1,1129 b''
1 1 # -*- coding: iso-8859-1 -*-
2 2
3 3 import ipipe, os, webbrowser, urllib
4 4 from IPython import ipapi
5 5 import wx
6 6 import wx.grid, wx.html
7 7
8 8 try:
9 9 sorted
10 10 except NameError:
11 11 from ipipe import sorted
12 12 try:
13 13 set
14 14 except:
15 15 from sets import Set as set
16 16
17 17
18 18 __all__ = ["igrid"]
19 19
20 20
21 21 help = """
22 22 <?xml version='1.0' encoding='iso-8859-1'?>
23 23 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
24 24 <html>
25 25 <head>
26 26 <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
27 27 <link rel="stylesheet" href="igrid_help.css" type="text/css" />
28 28 <title>igrid help</title>
29 29 </head>
30 30 <body>
31 31 <h1>igrid help</h1>
32 32
33 33
34 34 <h2>Commands</h2>
35 35
36 36
37 37 <h3>pick (P)</h3>
38 38 <p>Pick the whole row (object is available as "_")</p>
39 39
40 40 <h3>pickattr (Shift-P)</h3>
41 41 <p>Pick the attribute under the cursor</p>
42 42
43 43 <h3>pickallattrs (Shift-C)</h3>
44 44 <p>Pick the complete column under the cursor (i.e. the attribute under the
45 45 cursor) from all currently fetched objects. These attributes will be returned
46 46 as a list.</p>
47 47
48 48 <h3>pickinput (I)</h3>
49 49 <p>Pick the current row as next input line in IPython. Additionally the row is stored as "_"</p>
50 50
51 51 <h3>pickinputattr (Shift-I)</h3>
52 52 <p>Pick the attribute under the cursor as next input line in IPython. Additionally the row is stored as "_"</p>
53 53
54 54 <h3>enter (E)</h3>
55 55 <p>Enter the object under the cursor. (what this mean depends on the object
56 56 itself, i.e. how it implements iteration). This opens a new browser 'level'.</p>
57 57
58 58 <h3>enterattr (Shift-E)</h3>
59 59 <p>Enter the attribute under the cursor.</p>
60 60
61 61 <h3>detail (D)</h3>
62 62 <p>Show a detail view of the object under the cursor. This shows the name,
63 63 type, doc string and value of the object attributes (and it might show more
64 64 attributes than in the list view, depending on the object).</p>
65 65
66 66 <h3>detailattr (Shift-D)</h3>
67 67 <p>Show a detail view of the attribute under the cursor.</p>
68 68
69 69 <h3>pickrows (M)</h3>
70 70 <p>Pick multiple selected rows (M)</p>
71 71
72 72 <h3>pickrowsattr (CTRL-M)</h3>
73 73 <p>From multiple selected rows pick the cells matching the attribute the cursor is in (CTRL-M)</p>
74 74
75 75 <h3>find (CTRL-F)</h3>
76 76 <p>Find text</p>
77 77
78 78 <h3>find_expression (CTRL-Shift-F)</h3>
79 79 <p>Find entries matching an expression</p>
80 80
81 81 <h3>find_next (F3)</h3>
82 82 <p>Find next occurrence</p>
83 83
84 84 <h3>find_previous (Shift-F3)</h3>
85 85 <p>Find previous occurrence</p>
86 86
87 87 <h3>sortattrasc (V)</h3>
88 88 <p>Sort the objects (in ascending order) using the attribute under the cursor as the sort key.</p>
89 89
90 90 <h3>sortattrdesc (Shift-V)</h3>
91 91 <p>Sort the objects (in descending order) using the attribute under the cursor as the sort key.</p>
92 92
93 93 <h3>refresh_once (R, F5)</h3>
94 94 <p>Refreshes the display by restarting the iterator</p>
95 95
96 96 <h3>refresh_every_second</h3>
97 97 <p>Refreshes the display by restarting the iterator every second until stopped by stop_refresh.</p>
98 98
99 99 <h3>refresh_interval</h3>
100 100 <p>Refreshes the display by restarting the iterator every X ms (X is a custom interval set by the user) until stopped by stop_refresh.</p>
101 101
102 102 <h3>stop_refresh</h3>
103 103 <p>Stops all refresh timers.</p>
104 104
105 105 <h3>leave (Backspace, DEL, X)</h3>
106 106 <p>Close current tab (and all the tabs to the right of the current one).</h3>
107 107
108 108 <h3>quit (ESC,Q)</h3>
109 109 <p>Quit igrid and return to the IPython prompt.</p>
110 110
111 111
112 112 <h2>Navigation</h2>
113 113
114 114
115 115 <h3>Jump to the last column of the current row (END, CTRL-E, CTRL-Right)</h3>
116 116
117 117 <h3>Jump to the first column of the current row (HOME, CTRL-A, CTRL-Left)</h3>
118 118
119 119 <h3>Move the cursor one column to the left (&lt;)</h3>
120 120
121 121 <h3>Move the cursor one column to the right (&gt;)</h3>
122 122
123 123 <h3>Jump to the first row in the current column (CTRL-Up)</h3>
124 124
125 125 <h3>Jump to the last row in the current column (CTRL-Down)</h3>
126 126
127 127 </body>
128 128 </html>
129 129
130 130 """
131 131
132 132
133 133 class IGridRenderer(wx.grid.PyGridCellRenderer):
134 134 """
135 135 This is a custom renderer for our IGridGrid
136 136 """
137 137 def __init__(self, table):
138 138 self.maxchars = 200
139 139 self.table = table
140 140 self.colormap = (
141 141 ( 0, 0, 0),
142 142 (174, 0, 0),
143 143 ( 0, 174, 0),
144 144 (174, 174, 0),
145 145 ( 0, 0, 174),
146 146 (174, 0, 174),
147 147 ( 0, 174, 174),
148 148 ( 64, 64, 64)
149 149 )
150 150
151 151 wx.grid.PyGridCellRenderer.__init__(self)
152 152
153 153 def _getvalue(self, row, col):
154 154 try:
155 155 value = self.table._displayattrs[col].value(self.table.items[row])
156 156 (align, width, text) = ipipe.xformat(value, "cell", self.maxchars)
157 157 except Exception, exc:
158 158 (align, width, text) = ipipe.xformat(exc, "cell", self.maxchars)
159 159 return (align, text)
160 160
161 161 def GetBestSize(self, grid, attr, dc, row, col):
162 162 text = grid.GetCellValue(row, col)
163 163 (align, text) = self._getvalue(row, col)
164 164 dc.SetFont(attr.GetFont())
165 165 (w, h) = dc.GetTextExtent(str(text))
166 166 return wx.Size(min(w+2, 600), h+2) # add border
167 167
168 168 def Draw(self, grid, attr, dc, rect, row, col, isSelected):
169 169 """
170 170 Takes care of drawing everything in the cell; aligns the text
171 171 """
172 172 text = grid.GetCellValue(row, col)
173 173 (align, text) = self._getvalue(row, col)
174 174 if isSelected:
175 175 bg = grid.GetSelectionBackground()
176 176 else:
177 177 bg = ["white", (240, 240, 240)][row%2]
178 178 dc.SetTextBackground(bg)
179 179 dc.SetBrush(wx.Brush(bg, wx.SOLID))
180 180 dc.SetPen(wx.TRANSPARENT_PEN)
181 181 dc.SetFont(attr.GetFont())
182 182 dc.DrawRectangleRect(rect)
183 183 dc.SetClippingRect(rect)
184 184 # Format the text
185 185 if align == -1: # left alignment
186 186 (width, height) = dc.GetTextExtent(str(text))
187 187 x = rect[0]+1
188 188 y = rect[1]+0.5*(rect[3]-height)
189 189
190 190 for (style, part) in text:
191 191 if isSelected:
192 192 fg = grid.GetSelectionForeground()
193 193 else:
194 194 fg = self.colormap[style.fg]
195 195 dc.SetTextForeground(fg)
196 196 (w, h) = dc.GetTextExtent(part)
197 197 dc.DrawText(part, x, y)
198 198 x += w
199 199 elif align == 0: # center alignment
200 200 (width, height) = dc.GetTextExtent(str(text))
201 201 x = rect[0]+0.5*(rect[2]-width)
202 202 y = rect[1]+0.5*(rect[3]-height)
203 203 for (style, part) in text:
204 204 if isSelected:
205 205 fg = grid.GetSelectionForeground()
206 206 else:
207 207 fg = self.colormap[style.fg]
208 208 dc.SetTextForeground(fg)
209 209 (w, h) = dc.GetTextExtent(part)
210 210 dc.DrawText(part, x, y)
211 211 x += w
212 212 else: # right alignment
213 213 (width, height) = dc.GetTextExtent(str(text))
214 214 x = rect[0]+rect[2]-1
215 215 y = rect[1]+0.5*(rect[3]-height)
216 216 for (style, part) in reversed(text):
217 217 (w, h) = dc.GetTextExtent(part)
218 218 x -= w
219 219 if isSelected:
220 220 fg = grid.GetSelectionForeground()
221 221 else:
222 222 fg = self.colormap[style.fg]
223 223 dc.SetTextForeground(fg)
224 224 dc.DrawText(part, x, y)
225 225 dc.DestroyClippingRegion()
226 226
227 227 def Clone(self):
228 228 return IGridRenderer(self.table)
229 229
230 230
231 231 class IGridTable(wx.grid.PyGridTableBase):
232 232 # The data table for the ``IGridGrid``. Some dirty tricks were used here:
233 233 # ``GetValue()`` does not get any values (or at least it does not return
234 234 # anything, accessing the values is done by the renderer)
235 235 # but rather tries to fetch the objects which were requested into the table.
236 236 # General behaviour is: Fetch the first X objects. If the user scrolls down
237 237 # to the last object another bunch of X objects is fetched (if possible)
238 238 def __init__(self, input, fontsize, *attrs):
239 239 wx.grid.PyGridTableBase.__init__(self)
240 240 self.input = input
241 241 self.iterator = ipipe.xiter(input)
242 242 self.items = []
243 243 self.attrs = [ipipe.upgradexattr(attr) for attr in attrs]
244 244 self._displayattrs = self.attrs[:]
245 245 self._displayattrset = set(self.attrs)
246 246 self.fontsize = fontsize
247 247 self._fetch(1)
248 248 self.timer = wx.Timer()
249 249 self.timer.Bind(wx.EVT_TIMER, self.refresh_content)
250 250
251 251 def GetAttr(self, *args):
252 252 attr = wx.grid.GridCellAttr()
253 253 attr.SetFont(wx.Font(self.fontsize, wx.TELETYPE, wx.NORMAL, wx.NORMAL))
254 254 return attr
255 255
256 256 def GetNumberRows(self):
257 257 return len(self.items)
258 258
259 259 def GetNumberCols(self):
260 260 return len(self._displayattrs)
261 261
262 262 def GetColLabelValue(self, col):
263 263 if col < len(self._displayattrs):
264 264 return self._displayattrs[col].name()
265 265 else:
266 266 return ""
267 267
268 268 def GetRowLabelValue(self, row):
269 269 return str(row)
270 270
271 271 def IsEmptyCell(self, row, col):
272 272 return False
273 273
274 274 def _append(self, item):
275 275 self.items.append(item)
276 276 # Nothing to do if the set of attributes has been fixed by the user
277 277 if not self.attrs:
278 278 for attr in ipipe.xattrs(item):
279 279 attr = ipipe.upgradexattr(attr)
280 280 if attr not in self._displayattrset:
281 281 self._displayattrs.append(attr)
282 282 self._displayattrset.add(attr)
283 283
284 284 def _fetch(self, count):
285 285 # Try to fill ``self.items`` with at least ``count`` objects.
286 286 have = len(self.items)
287 287 while self.iterator is not None and have < count:
288 288 try:
289 289 item = self.iterator.next()
290 290 except StopIteration:
291 291 self.iterator = None
292 292 break
293 293 except (KeyboardInterrupt, SystemExit):
294 294 raise
295 295 except Exception, exc:
296 296 have += 1
297 297 self._append(exc)
298 298 self.iterator = None
299 299 break
300 300 else:
301 301 have += 1
302 302 self._append(item)
303 303
304 304 def GetValue(self, row, col):
305 305 # some kind of dummy-function: does not return anything but "";
306 306 # (The value isn't use anyway)
307 307 # its main task is to trigger the fetch of new objects
308 308 sizing_needed = False
309 309 had_cols = len(self._displayattrs)
310 310 had_rows = len(self.items)
311 311 if row == had_rows - 1 and self.iterator is not None:
312 312 self._fetch(row + 20)
313 313 sizing_needed = True
314 314 have_rows = len(self.items)
315 315 have_cols = len(self._displayattrs)
316 316 if have_rows > had_rows:
317 317 msg = wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED, have_rows - had_rows)
318 318 self.GetView().ProcessTableMessage(msg)
319 319 sizing_needed = True
320 320 if row >= have_rows:
321 321 return ""
322 322 if have_cols != had_cols:
323 323 msg = wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_COLS_APPENDED, have_cols - had_cols)
324 324 self.GetView().ProcessTableMessage(msg)
325 325 sizing_needed = True
326 326 if sizing_needed:
327 327 self.GetView().AutoSizeColumns(False)
328 328 return ""
329 329
330 330 def SetValue(self, row, col, value):
331 331 pass
332 332
333 333 def refresh_content(self, event):
334 334 msg = wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_ROWS_DELETED, 0, self.GetNumberRows())
335 335 self.GetView().ProcessTableMessage(msg)
336 336 self.iterator = ipipe.xiter(self.input)
337 337 self.items = []
338 338 self.attrs = [] # _append will calculate new displayattrs
339 339 self._fetch(1) # fetch one...
340 340 if self.items:
341 341 msg = wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED, 1)
342 342 self.GetView().ProcessTableMessage(msg)
343 343 self.GetValue(0, 0) # and trigger "fetch next 20"
344 344 item = self.items[0]
345 345 self.GetView().AutoSizeColumns(False)
346 346 panel = self.GetView().GetParent()
347 347 nb = panel.GetParent()
348 348 current = nb.GetSelection()
349 349 if nb.GetPage(current) == panel:
350 350 self.GetView().set_footer(item)
351 351
352 352 class IGridGrid(wx.grid.Grid):
353 353 # The actual grid
354 354 # all methods for selecting/sorting/picking/... data are implemented here
355 355 def __init__(self, panel, input, *attrs):
356 356 wx.grid.Grid.__init__(self, panel)
357 357 fontsize = 9
358 358 self.input = input
359 359 self.table = IGridTable(self.input, fontsize, *attrs)
360 360 self.SetTable(self.table, True)
361 361 self.SetSelectionMode(wx.grid.Grid.wxGridSelectRows)
362 362 self.SetDefaultRenderer(IGridRenderer(self.table))
363 363 self.EnableEditing(False)
364 364 self.Bind(wx.EVT_KEY_DOWN, self.key_pressed)
365 365 self.Bind(wx.grid.EVT_GRID_CELL_LEFT_DCLICK, self.cell_doubleclicked)
366 366 self.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.cell_leftclicked)
367 367 self.Bind(wx.grid.EVT_GRID_LABEL_LEFT_DCLICK, self.label_doubleclicked)
368 368 self.Bind(wx.grid.EVT_GRID_LABEL_LEFT_CLICK, self.on_label_leftclick)
369 369 self.Bind(wx.grid.EVT_GRID_RANGE_SELECT, self._on_selected_range)
370 370 self.Bind(wx.grid.EVT_GRID_SELECT_CELL, self._on_selected_cell)
371 371 self.current_selection = set()
372 372 self.maxchars = 200
373 373
374 374 def on_label_leftclick(self, event):
375 375 event.Skip()
376 376
377 377 def error_output(self, text):
378 378 wx.Bell()
379 379 frame = self.GetParent().GetParent().GetParent()
380 380 frame.SetStatusText(str(text))
381 381
382 382 def _on_selected_range(self, event):
383 383 # Internal update to the selection tracking lists
384 384 if event.Selecting():
385 385 # adding to the list...
386 386 self.current_selection.update(xrange(event.GetTopRow(), event.GetBottomRow()+1))
387 387 else:
388 388 # removal from list
389 389 for index in xrange(event.GetTopRow(), event.GetBottomRow()+1):
390 390 self.current_selection.discard(index)
391 391 event.Skip()
392 392
393 393 def _on_selected_cell(self, event):
394 394 # Internal update to the selection tracking list
395 395 self.current_selection = set([event.GetRow()])
396 396 event.Skip()
397 397
398 398 def sort(self, key, reverse=False):
399 399 """
400 400 Sort the current list of items using the key function ``key``. If
401 401 ``reverse`` is true the sort order is reversed.
402 402 """
403 403 row = self.GetGridCursorRow()
404 404 col = self.GetGridCursorCol()
405 405 curitem = self.table.items[row] # Remember where the cursor is now
406 406 # Sort items
407 407 def realkey(item):
408 408 try:
409 409 return key(item)
410 410 except (KeyboardInterrupt, SystemExit):
411 411 raise
412 412 except Exception:
413 413 return None
414 414 try:
415 415 self.table.items = ipipe.deque(sorted(self.table.items, key=realkey, reverse=reverse))
416 416 except TypeError, exc:
417 417 self.error_output("Exception encountered: %s" % exc)
418 418 return
419 419 # Find out where the object under the cursor went
420 420 for (i, item) in enumerate(self.table.items):
421 421 if item is curitem:
422 422 self.SetGridCursor(i,col)
423 423 self.MakeCellVisible(i,col)
424 424 self.Refresh()
425 425
426 426 def sortattrasc(self):
427 427 """
428 428 Sort in ascending order; sorting criteria is the current attribute
429 429 """
430 430 col = self.GetGridCursorCol()
431 431 attr = self.table._displayattrs[col]
432 432 frame = self.GetParent().GetParent().GetParent()
433 433 if attr is ipipe.noitem:
434 434 self.error_output("no column under cursor")
435 435 return
436 436 frame.SetStatusText("sort by %s (ascending)" % attr.name())
437 437 def key(item):
438 438 try:
439 439 return attr.value(item)
440 440 except (KeyboardInterrupt, SystemExit):
441 441 raise
442 442 except Exception:
443 443 return None
444 444 self.sort(key)
445 445
446 446 def sortattrdesc(self):
447 447 """
448 448 Sort in descending order; sorting criteria is the current attribute
449 449 """
450 450 col = self.GetGridCursorCol()
451 451 attr = self.table._displayattrs[col]
452 452 frame = self.GetParent().GetParent().GetParent()
453 453 if attr is ipipe.noitem:
454 454 self.error_output("no column under cursor")
455 455 return
456 456 frame.SetStatusText("sort by %s (descending)" % attr.name())
457 457 def key(item):
458 458 try:
459 459 return attr.value(item)
460 460 except (KeyboardInterrupt, SystemExit):
461 461 raise
462 462 except Exception:
463 463 return None
464 464 self.sort(key, reverse=True)
465 465
466 466 def label_doubleclicked(self, event):
467 467 row = event.GetRow()
468 468 col = event.GetCol()
469 469 if col == -1:
470 470 self.enter(row)
471 471
472 472 def _getvalue(self, row, col):
473 473 """
474 474 Gets the text which is displayed at ``(row, col)``
475 475 """
476 476 try:
477 477 value = self.table._displayattrs[col].value(self.table.items[row])
478 478 (align, width, text) = ipipe.xformat(value, "cell", self.maxchars)
479 479 except IndexError:
480 480 raise IndexError
481 481 except Exception, exc:
482 482 (align, width, text) = ipipe.xformat(exc, "cell", self.maxchars)
483 483 return text
484 484
485 485 def searchexpression(self, searchexp, startrow=None, search_forward=True ):
486 486 """
487 487 Find by expression
488 488 """
489 489 frame = self.GetParent().GetParent().GetParent()
490 490 if searchexp:
491 491 if search_forward:
492 492 if not startrow:
493 493 row = self.GetGridCursorRow()+1
494 494 else:
495 495 row = startrow + 1
496 496 while True:
497 497 try:
498 498 foo = self.table.GetValue(row, 0)
499 499 item = self.table.items[row]
500 500 try:
501 501 globals = ipipe.getglobals(None)
502 502 if eval(searchexp, globals, ipipe.AttrNamespace(item)):
503 503 self.SetGridCursor(row, 0) # found something
504 504 self.MakeCellVisible(row, 0)
505 505 break
506 506 except (KeyboardInterrupt, SystemExit):
507 507 raise
508 508 except Exception, exc:
509 509 frame.SetStatusText(str(exc))
510 510 wx.Bell()
511 511 break # break on error
512 512 except IndexError:
513 513 return
514 514 row += 1
515 515 else:
516 516 if not startrow:
517 517 row = self.GetGridCursorRow() - 1
518 518 else:
519 519 row = startrow - 1
520 520 while True:
521 521 try:
522 522 foo = self.table.GetValue(row, 0)
523 523 item = self.table.items[row]
524 524 try:
525 525 globals = ipipe.getglobals(None)
526 526 if eval(searchexp, globals, ipipe.AttrNamespace(item)):
527 527 self.SetGridCursor(row, 0) # found something
528 528 self.MakeCellVisible(row, 0)
529 529 break
530 530 except (KeyboardInterrupt, SystemExit):
531 531 raise
532 532 except Exception, exc:
533 533 frame.SetStatusText(str(exc))
534 534 wx.Bell()
535 535 break # break on error
536 536 except IndexError:
537 537 return
538 538 row -= 1
539 539
540 540
541 541 def search(self, searchtext, startrow=None, startcol=None, search_forward=True):
542 542 """
543 543 search for ``searchtext``, starting in ``(startrow, startcol)``;
544 544 if ``search_forward`` is true the direction is "forward"
545 545 """
546 546 searchtext = searchtext.lower()
547 547 if search_forward:
548 548 if startrow is not None and startcol is not None:
549 549 row = startrow
550 550 else:
551 551 startcol = self.GetGridCursorCol() + 1
552 552 row = self.GetGridCursorRow()
553 553 if startcol >= self.GetNumberCols():
554 554 startcol = 0
555 555 row += 1
556 556 while True:
557 557 for col in xrange(startcol, self.table.GetNumberCols()):
558 558 try:
559 559 foo = self.table.GetValue(row, col)
560 560 text = self._getvalue(row, col)
561 561 if searchtext in text.string().lower():
562 562 self.SetGridCursor(row, col)
563 563 self.MakeCellVisible(row, col)
564 564 return
565 565 except IndexError:
566 566 return
567 567 startcol = 0
568 568 row += 1
569 569 else:
570 570 if startrow is not None and startcol is not None:
571 571 row = startrow
572 572 else:
573 573 startcol = self.GetGridCursorCol() - 1
574 574 row = self.GetGridCursorRow()
575 575 if startcol < 0:
576 576 startcol = self.GetNumberCols() - 1
577 577 row -= 1
578 578 while True:
579 579 for col in xrange(startcol, -1, -1):
580 580 try:
581 581 foo = self.table.GetValue(row, col)
582 582 text = self._getvalue(row, col)
583 583 if searchtext in text.string().lower():
584 584 self.SetGridCursor(row, col)
585 585 self.MakeCellVisible(row, col)
586 586 return
587 587 except IndexError:
588 588 return
589 589 startcol = self.table.GetNumberCols()-1
590 590 row -= 1
591 591
592 592 def key_pressed(self, event):
593 593 """
594 594 Maps pressed keys to functions
595 595 """
596 596 frame = self.GetParent().GetParent().GetParent()
597 597 frame.SetStatusText("")
598 598 sh = event.ShiftDown()
599 599 ctrl = event.ControlDown()
600 600
601 601 keycode = event.GetKeyCode()
602 602 if keycode == ord("P"):
603 603 row = self.GetGridCursorRow()
604 604 if sh:
605 605 col = self.GetGridCursorCol()
606 606 self.pickattr(row, col)
607 607 else:
608 608 self.pick(row)
609 609 elif keycode == ord("M"):
610 610 if ctrl:
611 611 col = self.GetGridCursorCol()
612 612 self.pickrowsattr(sorted(self.current_selection), col)
613 613 else:
614 614 self.pickrows(sorted(self.current_selection))
615 615 elif keycode in (wx.WXK_BACK, wx.WXK_DELETE, ord("X")) and not (ctrl or sh):
616 616 self.delete_current_notebook()
617 617 elif keycode in (ord("E"), ord("\r")):
618 618 row = self.GetGridCursorRow()
619 619 if sh:
620 620 col = self.GetGridCursorCol()
621 621 self.enterattr(row, col)
622 622 else:
623 623 self.enter(row)
624 624 elif keycode == ord("E") and ctrl:
625 625 row = self.GetGridCursorRow()
626 626 self.SetGridCursor(row, self.GetNumberCols()-1)
627 627 elif keycode == wx.WXK_HOME or (keycode == ord("A") and ctrl):
628 628 row = self.GetGridCursorRow()
629 629 self.SetGridCursor(row, 0)
630 630 elif keycode == ord("C") and sh:
631 631 col = self.GetGridCursorCol()
632 632 attr = self.table._displayattrs[col]
633 633 result = []
634 634 for i in xrange(self.GetNumberRows()):
635 635 result.append(self.table._displayattrs[col].value(self.table.items[i]))
636 636 self.quit(result)
637 637 elif keycode in (wx.WXK_ESCAPE, ord("Q")) and not (ctrl or sh):
638 638 self.quit()
639 639 elif keycode == ord("<"):
640 640 row = self.GetGridCursorRow()
641 641 col = self.GetGridCursorCol()
642 642 if not event.ShiftDown():
643 643 newcol = col - 1
644 644 if newcol >= 0:
645 645 self.SetGridCursor(row, col - 1)
646 646 else:
647 647 newcol = col + 1
648 648 if newcol < self.GetNumberCols():
649 649 self.SetGridCursor(row, col + 1)
650 650 elif keycode == ord("D"):
651 651 col = self.GetGridCursorCol()
652 652 row = self.GetGridCursorRow()
653 653 if not sh:
654 654 self.detail(row, col)
655 655 else:
656 656 self.detail_attr(row, col)
657 657 elif keycode == ord("F") and ctrl:
658 658 if sh:
659 659 frame.enter_searchexpression(event)
660 660 else:
661 661 frame.enter_searchtext(event)
662 662 elif keycode == wx.WXK_F3:
663 663 if sh:
664 664 frame.find_previous(event)
665 665 else:
666 666 frame.find_next(event)
667 667 elif keycode == ord("V"):
668 668 if sh:
669 669 self.sortattrdesc()
670 670 else:
671 671 self.sortattrasc()
672 672 elif keycode == wx.WXK_DOWN:
673 673 row = self.GetGridCursorRow()
674 674 try:
675 675 item = self.table.items[row+1]
676 676 except IndexError:
677 677 item = self.table.items[row]
678 678 self.set_footer(item)
679 679 event.Skip()
680 680 elif keycode == wx.WXK_UP:
681 681 row = self.GetGridCursorRow()
682 682 if row >= 1:
683 683 item = self.table.items[row-1]
684 684 else:
685 685 item = self.table.items[row]
686 686 self.set_footer(item)
687 687 event.Skip()
688 688 elif keycode == wx.WXK_RIGHT:
689 689 row = self.GetGridCursorRow()
690 690 item = self.table.items[row]
691 691 self.set_footer(item)
692 692 event.Skip()
693 693 elif keycode == wx.WXK_LEFT:
694 694 row = self.GetGridCursorRow()
695 695 item = self.table.items[row]
696 696 self.set_footer(item)
697 697 event.Skip()
698 698 elif keycode == ord("R") or keycode == wx.WXK_F5:
699 699 self.table.refresh_content(event)
700 700 elif keycode == ord("I"):
701 701 row = self.GetGridCursorRow()
702 702 if not sh:
703 703 self.pickinput(row)
704 704 else:
705 705 col = self.GetGridCursorCol()
706 706 self.pickinputattr(row, col)
707 707 else:
708 708 event.Skip()
709 709
710 710 def delete_current_notebook(self):
711 711 """
712 712 deletes the current notebook tab
713 713 """
714 714 panel = self.GetParent()
715 715 nb = panel.GetParent()
716 716 current = nb.GetSelection()
717 717 count = nb.GetPageCount()
718 718 if count > 1:
719 719 for i in xrange(count-1, current-1, -1):
720 720 nb.DeletePage(i)
721 721 nb.GetCurrentPage().grid.SetFocus()
722 722 else:
723 723 frame = nb.GetParent()
724 724 frame.SetStatusText("This is the last level!")
725 725
726 726 def _doenter(self, value, *attrs):
727 727 """
728 728 "enter" a special item resulting in a new notebook tab
729 729 """
730 730 panel = self.GetParent()
731 731 nb = panel.GetParent()
732 732 frame = nb.GetParent()
733 733 current = nb.GetSelection()
734 734 count = nb.GetPageCount()
735 735 try: # if we want to enter something non-iterable, e.g. a function
736 736 if current + 1 == count and value is not self.input: # we have an event in the last tab
737 737 frame._add_notebook(value, *attrs)
738 738 elif value != self.input: # we have to delete all tabs newer than [panel] first
739 739 for i in xrange(count-1, current, -1): # some tabs don't close if we don't close in *reverse* order
740 740 nb.DeletePage(i)
741 741 frame._add_notebook(value)
742 742 except TypeError, exc:
743 743 if exc.__class__.__module__ == "exceptions":
744 744 msg = "%s: %s" % (exc.__class__.__name__, exc)
745 745 else:
746 746 msg = "%s.%s: %s" % (exc.__class__.__module__, exc.__class__.__name__, exc)
747 747 frame.SetStatusText(msg)
748 748
749 749 def enterattr(self, row, col):
750 750 try:
751 751 attr = self.table._displayattrs[col]
752 752 value = attr.value(self.table.items[row])
753 753 except Exception, exc:
754 754 self.error_output(str(exc))
755 755 else:
756 756 self._doenter(value)
757 757
758 758 def set_footer(self, item):
759 759 frame = self.GetParent().GetParent().GetParent()
760 760 frame.SetStatusText(" ".join([str(text) for (style, text) in ipipe.xformat(item, "footer", 20)[2]]), 0)
761 761
762 762 def enter(self, row):
763 763 try:
764 764 value = self.table.items[row]
765 765 except Exception, exc:
766 766 self.error_output(str(exc))
767 767 else:
768 768 self._doenter(value)
769 769
770 770 def detail(self, row, col):
771 771 """
772 772 shows a detail-view of the current cell
773 773 """
774 774 try:
775 775 attr = self.table._displayattrs[col]
776 776 item = self.table.items[row]
777 777 except Exception, exc:
778 778 self.error_output(str(exc))
779 779 else:
780 780 attrs = [ipipe.AttributeDetail(item, attr) for attr in ipipe.xattrs(item, "detail")]
781 781 self._doenter(attrs)
782 782
783 783 def detail_attr(self, row, col):
784 784 try:
785 785 attr = self.table._displayattrs[col]
786 786 item = attr.value(self.table.items[row])
787 787 except Exception, exc:
788 788 self.error_output(str(exc))
789 789 else:
790 790 attrs = [ipipe.AttributeDetail(item, attr) for attr in ipipe.xattrs(item, "detail")]
791 791 self._doenter(attrs)
792 792
793 793 def quit(self, result=None):
794 794 """
795 795 quit
796 796 """
797 797 frame = self.GetParent().GetParent().GetParent()
798 798 if frame.helpdialog:
799 799 frame.helpdialog.Destroy()
800 800 app = frame.parent
801 801 if app is not None:
802 802 app.result = result
803 803 frame.Close()
804 804 frame.Destroy()
805 805
806 806 def cell_doubleclicked(self, event):
807 807 self.enterattr(event.GetRow(), event.GetCol())
808 808 event.Skip()
809 809
810 810 def cell_leftclicked(self, event):
811 811 row = event.GetRow()
812 812 item = self.table.items[row]
813 813 self.set_footer(item)
814 814 event.Skip()
815 815
816 816 def pick(self, row):
817 817 """
818 818 pick a single row and return to the IPython prompt
819 819 """
820 820 try:
821 821 value = self.table.items[row]
822 822 except Exception, exc:
823 823 self.error_output(str(exc))
824 824 else:
825 825 self.quit(value)
826 826
827 827 def pickinput(self, row):
828 828 try:
829 829 value = self.table.items[row]
830 830 except Exception, exc:
831 831 self.error_output(str(exc))
832 832 else:
833 833 api = ipapi.get()
834 834 api.set_next_input(str(value))
835 835 self.quit(value)
836 836
837 837 def pickinputattr(self, row, col):
838 838 try:
839 839 attr = self.table._displayattrs[col]
840 840 value = attr.value(self.table.items[row])
841 841 except Exception, exc:
842 842 self.error_output(str(exc))
843 843 else:
844 844 api = ipapi.get()
845 845 api.set_next_input(str(value))
846 846 self.quit(value)
847 847
848 848 def pickrows(self, rows):
849 849 """
850 850 pick multiple rows and return to the IPython prompt
851 851 """
852 852 try:
853 853 value = [self.table.items[row] for row in rows]
854 854 except Exception, exc:
855 855 self.error_output(str(exc))
856 856 else:
857 857 self.quit(value)
858 858
859 859 def pickrowsattr(self, rows, col):
860 860 """"
861 861 pick one column from multiple rows
862 862 """
863 863 values = []
864 864 try:
865 865 attr = self.table._displayattrs[col]
866 866 for row in rows:
867 867 try:
868 868 values.append(attr.value(self.table.items[row]))
869 869 except (SystemExit, KeyboardInterrupt):
870 870 raise
871 871 except Exception:
872 872 raise #pass
873 873 except Exception, exc:
874 874 self.error_output(str(exc))
875 875 else:
876 876 self.quit(values)
877 877
878 878 def pickattr(self, row, col):
879 879 try:
880 880 attr = self.table._displayattrs[col]
881 881 value = attr.value(self.table.items[row])
882 882 except Exception, exc:
883 883 self.error_output(str(exc))
884 884 else:
885 885 self.quit(value)
886 886
887 887
888 888 class IGridPanel(wx.Panel):
889 889 # Each IGridPanel contains an IGridGrid
890 890 def __init__(self, parent, input, *attrs):
891 891 wx.Panel.__init__(self, parent, -1)
892 892 self.grid = IGridGrid(self, input, *attrs)
893 893 self.grid.FitInside()
894 894 sizer = wx.BoxSizer(wx.VERTICAL)
895 895 sizer.Add(self.grid, proportion=1, flag=wx.EXPAND | wx.ALL, border=10)
896 896 self.SetSizer(sizer)
897 897 sizer.Fit(self)
898 898 sizer.SetSizeHints(self)
899 899
900 900
901 901 class IGridHTMLHelp(wx.Frame):
902 902 def __init__(self, parent, title, size):
903 903 wx.Frame.__init__(self, parent, -1, title, size=size)
904 904 html = wx.html.HtmlWindow(self)
905 905 if "gtk2" in wx.PlatformInfo:
906 906 html.SetStandardFonts()
907 907 html.SetPage(help)
908 908
909 909
910 910 class IGridFrame(wx.Frame):
911 911 maxtitlelen = 30
912 912
913 913 def __init__(self, parent, input):
914 914 title = " ".join([str(text) for (style, text) in ipipe.xformat(input, "header", 20)[2]])
915 915 wx.Frame.__init__(self, None, title=title, size=(640, 480))
916 916 self.menubar = wx.MenuBar()
917 917 self.menucounter = 100
918 918 self.m_help = wx.Menu()
919 919 self.m_search = wx.Menu()
920 920 self.m_sort = wx.Menu()
921 921 self.m_refresh = wx.Menu()
922 922 self.notebook = wx.Notebook(self, -1, style=0)
923 923 self.statusbar = self.CreateStatusBar(1, wx.ST_SIZEGRIP)
924 924 self.statusbar.SetFieldsCount(2)
925 925 self.SetStatusWidths([-1, 200])
926 926 self.parent = parent
927 927 self._add_notebook(input)
928 928 self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
929 929 self.makemenu(self.m_sort, "&Sort (asc)\tV", "Sort ascending", self.sortasc)
930 930 self.makemenu(self.m_sort, "Sort (&desc)\tShift-V", "Sort descending", self.sortdesc)
931 931 self.makemenu(self.m_help, "&Help\tF1", "Help", self.display_help)
932 932 # self.makemenu(self.m_help, "&Show help in browser", "Show help in browser", self.display_help_in_browser)
933 933 self.makemenu(self.m_search, "&Find text\tCTRL-F", "Find text", self.enter_searchtext)
934 934 self.makemenu(self.m_search, "Find by &expression\tCTRL-Shift-F", "Find by expression", self.enter_searchexpression)
935 935 self.makemenu(self.m_search, "Find &next\tF3", "Find next", self.find_next)
936 936 self.makemenu(self.m_search, "Find &previous\tShift-F3", "Find previous", self.find_previous)
937 937 self.makemenu(self.m_refresh, "&Refresh once \tF5", "Refresh once", self.refresh_once)
938 938 self.makemenu(self.m_refresh, "Refresh every &1s", "Refresh every second", self.refresh_every_second)
939 939 self.makemenu(self.m_refresh, "Refresh every &X seconds", "Refresh every X seconds", self.refresh_interval)
940 940 self.makemenu(self.m_refresh, "&Stop all refresh timers", "Stop refresh timers", self.stop_refresh)
941 941 self.menubar.Append(self.m_search, "&Find")
942 942 self.menubar.Append(self.m_sort, "&Sort")
943 943 self.menubar.Append(self.m_refresh, "&Refresh")
944 944 self.menubar.Append(self.m_help, "&Help")
945 945 self.SetMenuBar(self.menubar)
946 946 self.searchtext = ""
947 947 self.searchexpression = ""
948 948 self.helpdialog = None
949 949 self.refresh_interval = 1000
950 950 self.SetStatusText("Refreshing inactive", 1)
951 951
952 952 def refresh_once(self, event):
953 953 table = self.notebook.GetPage(self.notebook.GetSelection()).grid.table
954 954 table.refresh_content(event)
955 955
956 956 def refresh_interval(self, event):
957 957 table = self.notebook.GetPage(self.notebook.GetSelection()).grid.table
958 958 dlg = wx.TextEntryDialog(self, "Enter refresh interval (milliseconds):", "Refresh timer:", defaultValue=str(self.refresh_interval))
959 959 if dlg.ShowModal() == wx.ID_OK:
960 960 try:
961 961 milliseconds = int(dlg.GetValue())
962 962 except ValueError, exc:
963 963 self.SetStatusText(str(exc))
964 964 else:
965 965 table.timer.Start(milliseconds=milliseconds, oneShot=False)
966 966 self.SetStatusText("Refresh timer set to %s ms" % milliseconds)
967 967 self.SetStatusText("Refresh interval: %s ms" % milliseconds, 1)
968 968 self.refresh_interval = milliseconds
969 969 dlg.Destroy()
970 970
971 971 def stop_refresh(self, event):
972 972 for i in xrange(self.notebook.GetPageCount()):
973 973 nb = self.notebook.GetPage(i)
974 974 nb.grid.table.timer.Stop()
975 975 self.SetStatusText("Refreshing inactive", 1)
976 976
977 977 def refresh_every_second(self, event):
978 978 table = self.notebook.GetPage(self.notebook.GetSelection()).grid.table
979 979 table.timer.Start(milliseconds=1000, oneShot=False)
980 980 self.SetStatusText("Refresh interval: 1000 ms", 1)
981 981
982 982 def sortasc(self, event):
983 983 grid = self.notebook.GetPage(self.notebook.GetSelection()).grid
984 984 grid.sortattrasc()
985 985
986 986 def sortdesc(self, event):
987 987 grid = self.notebook.GetPage(self.notebook.GetSelection()).grid
988 988 grid.sortattrdesc()
989 989
990 990 def find_previous(self, event):
991 991 """
992 992 find previous occurrences
993 993 """
994 994 grid = self.notebook.GetPage(self.notebook.GetSelection()).grid
995 995 if self.searchtext:
996 996 row = grid.GetGridCursorRow()
997 997 col = grid.GetGridCursorCol()
998 998 self.SetStatusText('Search mode: text; looking for %s' % self.searchtext)
999 999 if col-1 >= 0:
1000 1000 grid.search(self.searchtext, row, col-1, False)
1001 1001 else:
1002 1002 grid.search(self.searchtext, row-1, grid.table.GetNumberCols()-1, False)
1003 1003 elif self.searchexpression:
1004 1004 self.SetStatusText("Search mode: expression; looking for %s" % repr(self.searchexpression)[2:-1])
1005 1005 grid.searchexpression(searchexp=self.searchexpression, search_forward=False)
1006 1006 else:
1007 1007 self.SetStatusText("No search yet: please enter search-text or -expression")
1008 1008
1009 1009 def find_next(self, event):
1010 1010 """
1011 1011 find the next occurrence
1012 1012 """
1013 1013 grid = self.notebook.GetPage(self.notebook.GetSelection()).grid
1014 1014 if self.searchtext != "":
1015 1015 row = grid.GetGridCursorRow()
1016 1016 col = grid.GetGridCursorCol()
1017 1017 self.SetStatusText('Search mode: text; looking for %s' % self.searchtext)
1018 1018 if col+1 < grid.table.GetNumberCols():
1019 1019 grid.search(self.searchtext, row, col+1)
1020 1020 else:
1021 1021 grid.search(self.searchtext, row+1, 0)
1022 1022 elif self.searchexpression != "":
1023 1023 self.SetStatusText('Search mode: expression; looking for %s' % repr(self.searchexpression)[2:-1])
1024 1024 grid.searchexpression(searchexp=self.searchexpression)
1025 1025 else:
1026 1026 self.SetStatusText("No search yet: please enter search-text or -expression")
1027 1027
1028 1028 def display_help(self, event):
1029 1029 """
1030 1030 Display a help dialog
1031 1031 """
1032 1032 if self.helpdialog:
1033 1033 self.helpdialog.Destroy()
1034 1034 self.helpdialog = IGridHTMLHelp(None, title="Help", size=wx.Size(600,400))
1035 1035 self.helpdialog.Show()
1036 1036
1037 1037 def display_help_in_browser(self, event):
1038 1038 """
1039 1039 Show the help-HTML in a browser (as a ``HtmlWindow`` does not understand
1040 1040 CSS this looks better)
1041 1041 """
1042 1042 filename = urllib.pathname2url(os.path.abspath(os.path.join(os.path.dirname(__file__), "igrid_help.html")))
1043 1043 if not filename.startswith("file"):
1044 1044 filename = "file:" + filename
1045 1045 webbrowser.open(filename, new=1, autoraise=True)
1046 1046
1047 1047 def enter_searchexpression(self, event):
1048 1048 dlg = wx.TextEntryDialog(self, "Find:", "Find matching expression:", defaultValue=self.searchexpression)
1049 1049 if dlg.ShowModal() == wx.ID_OK:
1050 1050 self.searchexpression = dlg.GetValue()
1051 1051 self.searchtext = ""
1052 1052 self.SetStatusText('Search mode: expression; looking for %s' % repr(self.searchexpression)[2:-1])
1053 1053 self.notebook.GetPage(self.notebook.GetSelection()).grid.searchexpression(self.searchexpression)
1054 1054 dlg.Destroy()
1055 1055
1056 1056 def makemenu(self, menu, label, help, cmd):
1057 1057 menu.Append(self.menucounter, label, help)
1058 1058 self.Bind(wx.EVT_MENU, cmd, id=self.menucounter)
1059 1059 self.menucounter += 1
1060 1060
1061 1061 def _add_notebook(self, input, *attrs):
1062 1062 # Adds another notebook which has the starting object ``input``
1063 1063 panel = IGridPanel(self.notebook, input, *attrs)
1064 1064 text = str(ipipe.xformat(input, "header", self.maxtitlelen)[2])
1065 1065 if len(text) >= self.maxtitlelen:
1066 1066 text = text[:self.maxtitlelen].rstrip(".") + "..."
1067 1067 self.notebook.AddPage(panel, text, True)
1068 1068 panel.grid.SetFocus()
1069 1069 self.Layout()
1070 1070
1071 1071 def OnCloseWindow(self, event):
1072 1072 self.Destroy()
1073 1073
1074 1074 def enter_searchtext(self, event):
1075 1075 # Displays a dialog asking for the searchtext
1076 1076 dlg = wx.TextEntryDialog(self, "Find:", "Find in list", defaultValue=self.searchtext)
1077 1077 if dlg.ShowModal() == wx.ID_OK:
1078 1078 self.searchtext = dlg.GetValue()
1079 1079 self.searchexpression = ""
1080 1080 self.SetStatusText('Search mode: text; looking for %s' % self.searchtext)
1081 1081 self.notebook.GetPage(self.notebook.GetSelection()).grid.search(self.searchtext)
1082 1082 dlg.Destroy()
1083 1083
1084 1084
1085 1085 class App(wx.App):
1086 1086 def __init__(self, input):
1087 1087 self.input = input
1088 1088 self.result = None # Result to be returned to IPython. Set by quit().
1089 1089 wx.App.__init__(self)
1090 1090
1091 1091 def OnInit(self):
1092 1092 frame = IGridFrame(self, self.input)
1093 1093 frame.Show()
1094 1094 self.SetTopWindow(frame)
1095 1095 frame.Raise()
1096 1096 return True
1097 1097
1098 1098
1099 1099 class igrid(ipipe.Display):
1100 1100 """
1101 1101 This is a wx-based display object that can be used instead of ``ibrowse``
1102 1102 (which is curses-based) or ``idump`` (which simply does a print).
1103 1103 """
1104
1105 def __init__(self, input=None):
1106 self.input = input
1107
1104 1108 if wx.VERSION < (2, 7):
1105 1109 def display(self):
1106 1110 try:
1107 1111 # Try to create a "standalone" from. If this works we're probably
1108 1112 # running with -wthread.
1109 1113 # Note that this sets the parent of the frame to None, but we can't
1110 1114 # pass a result object back to the shell anyway.
1111 1115 frame = IGridFrame(None, self.input)
1112 1116 frame.Show()
1113 1117 frame.Raise()
1114 1118 except wx.PyNoAppError:
1115 1119 # There's no wx application yet => create one.
1116 1120 app = App(self.input)
1117 1121 app.MainLoop()
1118 1122 return app.result
1119 1123 else:
1120 1124 # With wx 2.7 it gets simpler.
1121 1125 def display(self):
1122 1126 app = App(self.input)
1123 1127 app.MainLoop()
1124 1128 return app.result
1125 1129
General Comments 0
You need to be logged in to leave comments. Login now