/IPython/deathrow/igrid.py

http://github.com/ipython/ipython · Python · 1126 lines · 945 code · 87 blank · 94 comment · 235 complexity · 84aac1c7c2d6fbb992b03d910fb295c9 MD5 · raw file

  1. # -*- coding: iso-8859-1 -*-
  2. import ipipe, os, webbrowser, urllib
  3. from IPython.core import ipapi
  4. import wx
  5. import wx.grid, wx.html
  6. try:
  7. sorted
  8. except NameError:
  9. from ipipe import sorted
  10. try:
  11. set
  12. except:
  13. from sets import Set as set
  14. __all__ = ["igrid"]
  15. help = """
  16. <?xml version='1.0' encoding='iso-8859-1'?>
  17. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  18. <html>
  19. <head>
  20. <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
  21. <link rel="stylesheet" href="igrid_help.css" type="text/css" />
  22. <title>igrid help</title>
  23. </head>
  24. <body>
  25. <h1>igrid help</h1>
  26. <h2>Commands</h2>
  27. <h3>pick (P)</h3>
  28. <p>Pick the whole row (object is available as "_")</p>
  29. <h3>pickattr (Shift-P)</h3>
  30. <p>Pick the attribute under the cursor</p>
  31. <h3>pickallattrs (Shift-C)</h3>
  32. <p>Pick the complete column under the cursor (i.e. the attribute under the
  33. cursor) from all currently fetched objects. These attributes will be returned
  34. as a list.</p>
  35. <h3>pickinput (I)</h3>
  36. <p>Pick the current row as next input line in IPython. Additionally the row is stored as "_"</p>
  37. <h3>pickinputattr (Shift-I)</h3>
  38. <p>Pick the attribute under the cursor as next input line in IPython. Additionally the row is stored as "_"</p>
  39. <h3>enter (E)</h3>
  40. <p>Enter the object under the cursor. (what this mean depends on the object
  41. itself, i.e. how it implements iteration). This opens a new browser 'level'.</p>
  42. <h3>enterattr (Shift-E)</h3>
  43. <p>Enter the attribute under the cursor.</p>
  44. <h3>detail (D)</h3>
  45. <p>Show a detail view of the object under the cursor. This shows the name,
  46. type, doc string and value of the object attributes (and it might show more
  47. attributes than in the list view, depending on the object).</p>
  48. <h3>detailattr (Shift-D)</h3>
  49. <p>Show a detail view of the attribute under the cursor.</p>
  50. <h3>pickrows (M)</h3>
  51. <p>Pick multiple selected rows (M)</p>
  52. <h3>pickrowsattr (CTRL-M)</h3>
  53. <p>From multiple selected rows pick the cells matching the attribute the cursor is in (CTRL-M)</p>
  54. <h3>find (CTRL-F)</h3>
  55. <p>Find text</p>
  56. <h3>find_expression (CTRL-Shift-F)</h3>
  57. <p>Find entries matching an expression</p>
  58. <h3>find_next (F3)</h3>
  59. <p>Find next occurrence</p>
  60. <h3>find_previous (Shift-F3)</h3>
  61. <p>Find previous occurrence</p>
  62. <h3>sortattrasc (V)</h3>
  63. <p>Sort the objects (in ascending order) using the attribute under the cursor as the sort key.</p>
  64. <h3>sortattrdesc (Shift-V)</h3>
  65. <p>Sort the objects (in descending order) using the attribute under the cursor as the sort key.</p>
  66. <h3>refresh_once (R, F5)</h3>
  67. <p>Refreshes the display by restarting the iterator</p>
  68. <h3>refresh_every_second</h3>
  69. <p>Refreshes the display by restarting the iterator every second until stopped by stop_refresh.</p>
  70. <h3>refresh_interval</h3>
  71. <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>
  72. <h3>stop_refresh</h3>
  73. <p>Stops all refresh timers.</p>
  74. <h3>leave (Backspace, DEL, X)</h3>
  75. <p>Close current tab (and all the tabs to the right of the current one).</h3>
  76. <h3>quit (ESC,Q)</h3>
  77. <p>Quit igrid and return to the IPython prompt.</p>
  78. <h2>Navigation</h2>
  79. <h3>Jump to the last column of the current row (END, CTRL-E, CTRL-Right)</h3>
  80. <h3>Jump to the first column of the current row (HOME, CTRL-A, CTRL-Left)</h3>
  81. <h3>Move the cursor one column to the left (&lt;)</h3>
  82. <h3>Move the cursor one column to the right (&gt;)</h3>
  83. <h3>Jump to the first row in the current column (CTRL-Up)</h3>
  84. <h3>Jump to the last row in the current column (CTRL-Down)</h3>
  85. </body>
  86. </html>
  87. """
  88. class IGridRenderer(wx.grid.PyGridCellRenderer):
  89. """
  90. This is a custom renderer for our IGridGrid
  91. """
  92. def __init__(self, table):
  93. self.maxchars = 200
  94. self.table = table
  95. self.colormap = (
  96. ( 0, 0, 0),
  97. (174, 0, 0),
  98. ( 0, 174, 0),
  99. (174, 174, 0),
  100. ( 0, 0, 174),
  101. (174, 0, 174),
  102. ( 0, 174, 174),
  103. ( 64, 64, 64)
  104. )
  105. wx.grid.PyGridCellRenderer.__init__(self)
  106. def _getvalue(self, row, col):
  107. try:
  108. value = self.table._displayattrs[col].value(self.table.items[row])
  109. (align, width, text) = ipipe.xformat(value, "cell", self.maxchars)
  110. except Exception, exc:
  111. (align, width, text) = ipipe.xformat(exc, "cell", self.maxchars)
  112. return (align, text)
  113. def GetBestSize(self, grid, attr, dc, row, col):
  114. text = grid.GetCellValue(row, col)
  115. (align, text) = self._getvalue(row, col)
  116. dc.SetFont(attr.GetFont())
  117. (w, h) = dc.GetTextExtent(str(text))
  118. return wx.Size(min(w+2, 600), h+2) # add border
  119. def Draw(self, grid, attr, dc, rect, row, col, isSelected):
  120. """
  121. Takes care of drawing everything in the cell; aligns the text
  122. """
  123. text = grid.GetCellValue(row, col)
  124. (align, text) = self._getvalue(row, col)
  125. if isSelected:
  126. bg = grid.GetSelectionBackground()
  127. else:
  128. bg = ["white", (240, 240, 240)][row%2]
  129. dc.SetTextBackground(bg)
  130. dc.SetBrush(wx.Brush(bg, wx.SOLID))
  131. dc.SetPen(wx.TRANSPARENT_PEN)
  132. dc.SetFont(attr.GetFont())
  133. dc.DrawRectangleRect(rect)
  134. dc.SetClippingRect(rect)
  135. # Format the text
  136. if align == -1: # left alignment
  137. (width, height) = dc.GetTextExtent(str(text))
  138. x = rect[0]+1
  139. y = rect[1]+0.5*(rect[3]-height)
  140. for (style, part) in text:
  141. if isSelected:
  142. fg = grid.GetSelectionForeground()
  143. else:
  144. fg = self.colormap[style.fg]
  145. dc.SetTextForeground(fg)
  146. (w, h) = dc.GetTextExtent(part)
  147. dc.DrawText(part, x, y)
  148. x += w
  149. elif align == 0: # center alignment
  150. (width, height) = dc.GetTextExtent(str(text))
  151. x = rect[0]+0.5*(rect[2]-width)
  152. y = rect[1]+0.5*(rect[3]-height)
  153. for (style, part) in text:
  154. if isSelected:
  155. fg = grid.GetSelectionForeground()
  156. else:
  157. fg = self.colormap[style.fg]
  158. dc.SetTextForeground(fg)
  159. (w, h) = dc.GetTextExtent(part)
  160. dc.DrawText(part, x, y)
  161. x += w
  162. else: # right alignment
  163. (width, height) = dc.GetTextExtent(str(text))
  164. x = rect[0]+rect[2]-1
  165. y = rect[1]+0.5*(rect[3]-height)
  166. for (style, part) in reversed(text):
  167. (w, h) = dc.GetTextExtent(part)
  168. x -= w
  169. if isSelected:
  170. fg = grid.GetSelectionForeground()
  171. else:
  172. fg = self.colormap[style.fg]
  173. dc.SetTextForeground(fg)
  174. dc.DrawText(part, x, y)
  175. dc.DestroyClippingRegion()
  176. def Clone(self):
  177. return IGridRenderer(self.table)
  178. class IGridTable(wx.grid.PyGridTableBase):
  179. # The data table for the ``IGridGrid``. Some dirty tricks were used here:
  180. # ``GetValue()`` does not get any values (or at least it does not return
  181. # anything, accessing the values is done by the renderer)
  182. # but rather tries to fetch the objects which were requested into the table.
  183. # General behaviour is: Fetch the first X objects. If the user scrolls down
  184. # to the last object another bunch of X objects is fetched (if possible)
  185. def __init__(self, input, fontsize, *attrs):
  186. wx.grid.PyGridTableBase.__init__(self)
  187. self.input = input
  188. self.iterator = ipipe.xiter(input)
  189. self.items = []
  190. self.attrs = [ipipe.upgradexattr(attr) for attr in attrs]
  191. self._displayattrs = self.attrs[:]
  192. self._displayattrset = set(self.attrs)
  193. self.fontsize = fontsize
  194. self._fetch(1)
  195. self.timer = wx.Timer()
  196. self.timer.Bind(wx.EVT_TIMER, self.refresh_content)
  197. def GetAttr(self, *args):
  198. attr = wx.grid.GridCellAttr()
  199. attr.SetFont(wx.Font(self.fontsize, wx.TELETYPE, wx.NORMAL, wx.NORMAL))
  200. return attr
  201. def GetNumberRows(self):
  202. return len(self.items)
  203. def GetNumberCols(self):
  204. return len(self._displayattrs)
  205. def GetColLabelValue(self, col):
  206. if col < len(self._displayattrs):
  207. return self._displayattrs[col].name()
  208. else:
  209. return ""
  210. def GetRowLabelValue(self, row):
  211. return str(row)
  212. def IsEmptyCell(self, row, col):
  213. return False
  214. def _append(self, item):
  215. self.items.append(item)
  216. # Nothing to do if the set of attributes has been fixed by the user
  217. if not self.attrs:
  218. for attr in ipipe.xattrs(item):
  219. attr = ipipe.upgradexattr(attr)
  220. if attr not in self._displayattrset:
  221. self._displayattrs.append(attr)
  222. self._displayattrset.add(attr)
  223. def _fetch(self, count):
  224. # Try to fill ``self.items`` with at least ``count`` objects.
  225. have = len(self.items)
  226. while self.iterator is not None and have < count:
  227. try:
  228. item = self.iterator.next()
  229. except StopIteration:
  230. self.iterator = None
  231. break
  232. except (KeyboardInterrupt, SystemExit):
  233. raise
  234. except Exception, exc:
  235. have += 1
  236. self._append(exc)
  237. self.iterator = None
  238. break
  239. else:
  240. have += 1
  241. self._append(item)
  242. def GetValue(self, row, col):
  243. # some kind of dummy-function: does not return anything but "";
  244. # (The value isn't use anyway)
  245. # its main task is to trigger the fetch of new objects
  246. sizing_needed = False
  247. had_cols = len(self._displayattrs)
  248. had_rows = len(self.items)
  249. if row == had_rows - 1 and self.iterator is not None:
  250. self._fetch(row + 20)
  251. sizing_needed = True
  252. have_rows = len(self.items)
  253. have_cols = len(self._displayattrs)
  254. if have_rows > had_rows:
  255. msg = wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED, have_rows - had_rows)
  256. self.GetView().ProcessTableMessage(msg)
  257. sizing_needed = True
  258. if row >= have_rows:
  259. return ""
  260. if have_cols != had_cols:
  261. msg = wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_COLS_APPENDED, have_cols - had_cols)
  262. self.GetView().ProcessTableMessage(msg)
  263. sizing_needed = True
  264. if sizing_needed:
  265. self.GetView().AutoSizeColumns(False)
  266. return ""
  267. def SetValue(self, row, col, value):
  268. pass
  269. def refresh_content(self, event):
  270. msg = wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_ROWS_DELETED, 0, self.GetNumberRows())
  271. self.GetView().ProcessTableMessage(msg)
  272. self.iterator = ipipe.xiter(self.input)
  273. self.items = []
  274. self.attrs = [] # _append will calculate new displayattrs
  275. self._fetch(1) # fetch one...
  276. if self.items:
  277. msg = wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED, 1)
  278. self.GetView().ProcessTableMessage(msg)
  279. self.GetValue(0, 0) # and trigger "fetch next 20"
  280. item = self.items[0]
  281. self.GetView().AutoSizeColumns(False)
  282. panel = self.GetView().GetParent()
  283. nb = panel.GetParent()
  284. current = nb.GetSelection()
  285. if nb.GetPage(current) == panel:
  286. self.GetView().set_footer(item)
  287. class IGridGrid(wx.grid.Grid):
  288. # The actual grid
  289. # all methods for selecting/sorting/picking/... data are implemented here
  290. def __init__(self, panel, input, *attrs):
  291. wx.grid.Grid.__init__(self, panel)
  292. fontsize = 9
  293. self.input = input
  294. self.table = IGridTable(self.input, fontsize, *attrs)
  295. self.SetTable(self.table, True)
  296. self.SetSelectionMode(wx.grid.Grid.wxGridSelectRows)
  297. self.SetDefaultRenderer(IGridRenderer(self.table))
  298. self.EnableEditing(False)
  299. self.Bind(wx.EVT_KEY_DOWN, self.key_pressed)
  300. self.Bind(wx.grid.EVT_GRID_CELL_LEFT_DCLICK, self.cell_doubleclicked)
  301. self.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.cell_leftclicked)
  302. self.Bind(wx.grid.EVT_GRID_LABEL_LEFT_DCLICK, self.label_doubleclicked)
  303. self.Bind(wx.grid.EVT_GRID_LABEL_LEFT_CLICK, self.on_label_leftclick)
  304. self.Bind(wx.grid.EVT_GRID_RANGE_SELECT, self._on_selected_range)
  305. self.Bind(wx.grid.EVT_GRID_SELECT_CELL, self._on_selected_cell)
  306. self.current_selection = set()
  307. self.maxchars = 200
  308. def on_label_leftclick(self, event):
  309. event.Skip()
  310. def error_output(self, text):
  311. wx.Bell()
  312. frame = self.GetParent().GetParent().GetParent()
  313. frame.SetStatusText(str(text))
  314. def _on_selected_range(self, event):
  315. # Internal update to the selection tracking lists
  316. if event.Selecting():
  317. # adding to the list...
  318. self.current_selection.update(xrange(event.GetTopRow(), event.GetBottomRow()+1))
  319. else:
  320. # removal from list
  321. for index in xrange(event.GetTopRow(), event.GetBottomRow()+1):
  322. self.current_selection.discard(index)
  323. event.Skip()
  324. def _on_selected_cell(self, event):
  325. # Internal update to the selection tracking list
  326. self.current_selection = set([event.GetRow()])
  327. event.Skip()
  328. def sort(self, key, reverse=False):
  329. """
  330. Sort the current list of items using the key function ``key``. If
  331. ``reverse`` is true the sort order is reversed.
  332. """
  333. row = self.GetGridCursorRow()
  334. col = self.GetGridCursorCol()
  335. curitem = self.table.items[row] # Remember where the cursor is now
  336. # Sort items
  337. def realkey(item):
  338. try:
  339. return key(item)
  340. except (KeyboardInterrupt, SystemExit):
  341. raise
  342. except Exception:
  343. return None
  344. try:
  345. self.table.items = ipipe.deque(sorted(self.table.items, key=realkey, reverse=reverse))
  346. except TypeError, exc:
  347. self.error_output("Exception encountered: %s" % exc)
  348. return
  349. # Find out where the object under the cursor went
  350. for (i, item) in enumerate(self.table.items):
  351. if item is curitem:
  352. self.SetGridCursor(i,col)
  353. self.MakeCellVisible(i,col)
  354. self.Refresh()
  355. def sortattrasc(self):
  356. """
  357. Sort in ascending order; sorting criteria is the current attribute
  358. """
  359. col = self.GetGridCursorCol()
  360. attr = self.table._displayattrs[col]
  361. frame = self.GetParent().GetParent().GetParent()
  362. if attr is ipipe.noitem:
  363. self.error_output("no column under cursor")
  364. return
  365. frame.SetStatusText("sort by %s (ascending)" % attr.name())
  366. def key(item):
  367. try:
  368. return attr.value(item)
  369. except (KeyboardInterrupt, SystemExit):
  370. raise
  371. except Exception:
  372. return None
  373. self.sort(key)
  374. def sortattrdesc(self):
  375. """
  376. Sort in descending order; sorting criteria is the current attribute
  377. """
  378. col = self.GetGridCursorCol()
  379. attr = self.table._displayattrs[col]
  380. frame = self.GetParent().GetParent().GetParent()
  381. if attr is ipipe.noitem:
  382. self.error_output("no column under cursor")
  383. return
  384. frame.SetStatusText("sort by %s (descending)" % attr.name())
  385. def key(item):
  386. try:
  387. return attr.value(item)
  388. except (KeyboardInterrupt, SystemExit):
  389. raise
  390. except Exception:
  391. return None
  392. self.sort(key, reverse=True)
  393. def label_doubleclicked(self, event):
  394. row = event.GetRow()
  395. col = event.GetCol()
  396. if col == -1:
  397. self.enter(row)
  398. def _getvalue(self, row, col):
  399. """
  400. Gets the text which is displayed at ``(row, col)``
  401. """
  402. try:
  403. value = self.table._displayattrs[col].value(self.table.items[row])
  404. (align, width, text) = ipipe.xformat(value, "cell", self.maxchars)
  405. except IndexError:
  406. raise IndexError
  407. except Exception, exc:
  408. (align, width, text) = ipipe.xformat(exc, "cell", self.maxchars)
  409. return text
  410. def searchexpression(self, searchexp, startrow=None, search_forward=True ):
  411. """
  412. Find by expression
  413. """
  414. frame = self.GetParent().GetParent().GetParent()
  415. if searchexp:
  416. if search_forward:
  417. if not startrow:
  418. row = self.GetGridCursorRow()+1
  419. else:
  420. row = startrow + 1
  421. while True:
  422. try:
  423. foo = self.table.GetValue(row, 0)
  424. item = self.table.items[row]
  425. try:
  426. globals = ipipe.getglobals(None)
  427. if eval(searchexp, globals, ipipe.AttrNamespace(item)):
  428. self.SetGridCursor(row, 0) # found something
  429. self.MakeCellVisible(row, 0)
  430. break
  431. except (KeyboardInterrupt, SystemExit):
  432. raise
  433. except Exception, exc:
  434. frame.SetStatusText(str(exc))
  435. wx.Bell()
  436. break # break on error
  437. except IndexError:
  438. return
  439. row += 1
  440. else:
  441. if not startrow:
  442. row = self.GetGridCursorRow() - 1
  443. else:
  444. row = startrow - 1
  445. while True:
  446. try:
  447. foo = self.table.GetValue(row, 0)
  448. item = self.table.items[row]
  449. try:
  450. globals = ipipe.getglobals(None)
  451. if eval(searchexp, globals, ipipe.AttrNamespace(item)):
  452. self.SetGridCursor(row, 0) # found something
  453. self.MakeCellVisible(row, 0)
  454. break
  455. except (KeyboardInterrupt, SystemExit):
  456. raise
  457. except Exception, exc:
  458. frame.SetStatusText(str(exc))
  459. wx.Bell()
  460. break # break on error
  461. except IndexError:
  462. return
  463. row -= 1
  464. def search(self, searchtext, startrow=None, startcol=None, search_forward=True):
  465. """
  466. search for ``searchtext``, starting in ``(startrow, startcol)``;
  467. if ``search_forward`` is true the direction is "forward"
  468. """
  469. searchtext = searchtext.lower()
  470. if search_forward:
  471. if startrow is not None and startcol is not None:
  472. row = startrow
  473. else:
  474. startcol = self.GetGridCursorCol() + 1
  475. row = self.GetGridCursorRow()
  476. if startcol >= self.GetNumberCols():
  477. startcol = 0
  478. row += 1
  479. while True:
  480. for col in xrange(startcol, self.table.GetNumberCols()):
  481. try:
  482. foo = self.table.GetValue(row, col)
  483. text = self._getvalue(row, col)
  484. if searchtext in text.string().lower():
  485. self.SetGridCursor(row, col)
  486. self.MakeCellVisible(row, col)
  487. return
  488. except IndexError:
  489. return
  490. startcol = 0
  491. row += 1
  492. else:
  493. if startrow is not None and startcol is not None:
  494. row = startrow
  495. else:
  496. startcol = self.GetGridCursorCol() - 1
  497. row = self.GetGridCursorRow()
  498. if startcol < 0:
  499. startcol = self.GetNumberCols() - 1
  500. row -= 1
  501. while True:
  502. for col in xrange(startcol, -1, -1):
  503. try:
  504. foo = self.table.GetValue(row, col)
  505. text = self._getvalue(row, col)
  506. if searchtext in text.string().lower():
  507. self.SetGridCursor(row, col)
  508. self.MakeCellVisible(row, col)
  509. return
  510. except IndexError:
  511. return
  512. startcol = self.table.GetNumberCols()-1
  513. row -= 1
  514. def key_pressed(self, event):
  515. """
  516. Maps pressed keys to functions
  517. """
  518. frame = self.GetParent().GetParent().GetParent()
  519. frame.SetStatusText("")
  520. sh = event.ShiftDown()
  521. ctrl = event.ControlDown()
  522. keycode = event.GetKeyCode()
  523. if keycode == ord("P"):
  524. row = self.GetGridCursorRow()
  525. if sh:
  526. col = self.GetGridCursorCol()
  527. self.pickattr(row, col)
  528. else:
  529. self.pick(row)
  530. elif keycode == ord("M"):
  531. if ctrl:
  532. col = self.GetGridCursorCol()
  533. self.pickrowsattr(sorted(self.current_selection), col)
  534. else:
  535. self.pickrows(sorted(self.current_selection))
  536. elif keycode in (wx.WXK_BACK, wx.WXK_DELETE, ord("X")) and not (ctrl or sh):
  537. self.delete_current_notebook()
  538. elif keycode in (ord("E"), ord("\r")):
  539. row = self.GetGridCursorRow()
  540. if sh:
  541. col = self.GetGridCursorCol()
  542. self.enterattr(row, col)
  543. else:
  544. self.enter(row)
  545. elif keycode == ord("E") and ctrl:
  546. row = self.GetGridCursorRow()
  547. self.SetGridCursor(row, self.GetNumberCols()-1)
  548. elif keycode == wx.WXK_HOME or (keycode == ord("A") and ctrl):
  549. row = self.GetGridCursorRow()
  550. self.SetGridCursor(row, 0)
  551. elif keycode == ord("C") and sh:
  552. col = self.GetGridCursorCol()
  553. attr = self.table._displayattrs[col]
  554. result = []
  555. for i in xrange(self.GetNumberRows()):
  556. result.append(self.table._displayattrs[col].value(self.table.items[i]))
  557. self.quit(result)
  558. elif keycode in (wx.WXK_ESCAPE, ord("Q")) and not (ctrl or sh):
  559. self.quit()
  560. elif keycode == ord("<"):
  561. row = self.GetGridCursorRow()
  562. col = self.GetGridCursorCol()
  563. if not event.ShiftDown():
  564. newcol = col - 1
  565. if newcol >= 0:
  566. self.SetGridCursor(row, col - 1)
  567. else:
  568. newcol = col + 1
  569. if newcol < self.GetNumberCols():
  570. self.SetGridCursor(row, col + 1)
  571. elif keycode == ord("D"):
  572. col = self.GetGridCursorCol()
  573. row = self.GetGridCursorRow()
  574. if not sh:
  575. self.detail(row, col)
  576. else:
  577. self.detail_attr(row, col)
  578. elif keycode == ord("F") and ctrl:
  579. if sh:
  580. frame.enter_searchexpression(event)
  581. else:
  582. frame.enter_searchtext(event)
  583. elif keycode == wx.WXK_F3:
  584. if sh:
  585. frame.find_previous(event)
  586. else:
  587. frame.find_next(event)
  588. elif keycode == ord("V"):
  589. if sh:
  590. self.sortattrdesc()
  591. else:
  592. self.sortattrasc()
  593. elif keycode == wx.WXK_DOWN:
  594. row = self.GetGridCursorRow()
  595. try:
  596. item = self.table.items[row+1]
  597. except IndexError:
  598. item = self.table.items[row]
  599. self.set_footer(item)
  600. event.Skip()
  601. elif keycode == wx.WXK_UP:
  602. row = self.GetGridCursorRow()
  603. if row >= 1:
  604. item = self.table.items[row-1]
  605. else:
  606. item = self.table.items[row]
  607. self.set_footer(item)
  608. event.Skip()
  609. elif keycode == wx.WXK_RIGHT:
  610. row = self.GetGridCursorRow()
  611. item = self.table.items[row]
  612. self.set_footer(item)
  613. event.Skip()
  614. elif keycode == wx.WXK_LEFT:
  615. row = self.GetGridCursorRow()
  616. item = self.table.items[row]
  617. self.set_footer(item)
  618. event.Skip()
  619. elif keycode == ord("R") or keycode == wx.WXK_F5:
  620. self.table.refresh_content(event)
  621. elif keycode == ord("I"):
  622. row = self.GetGridCursorRow()
  623. if not sh:
  624. self.pickinput(row)
  625. else:
  626. col = self.GetGridCursorCol()
  627. self.pickinputattr(row, col)
  628. else:
  629. event.Skip()
  630. def delete_current_notebook(self):
  631. """
  632. deletes the current notebook tab
  633. """
  634. panel = self.GetParent()
  635. nb = panel.GetParent()
  636. current = nb.GetSelection()
  637. count = nb.GetPageCount()
  638. if count > 1:
  639. for i in xrange(count-1, current-1, -1):
  640. nb.DeletePage(i)
  641. nb.GetCurrentPage().grid.SetFocus()
  642. else:
  643. frame = nb.GetParent()
  644. frame.SetStatusText("This is the last level!")
  645. def _doenter(self, value, *attrs):
  646. """
  647. "enter" a special item resulting in a new notebook tab
  648. """
  649. panel = self.GetParent()
  650. nb = panel.GetParent()
  651. frame = nb.GetParent()
  652. current = nb.GetSelection()
  653. count = nb.GetPageCount()
  654. try: # if we want to enter something non-iterable, e.g. a function
  655. if current + 1 == count and value is not self.input: # we have an event in the last tab
  656. frame._add_notebook(value, *attrs)
  657. elif value != self.input: # we have to delete all tabs newer than [panel] first
  658. for i in xrange(count-1, current, -1): # some tabs don't close if we don't close in *reverse* order
  659. nb.DeletePage(i)
  660. frame._add_notebook(value)
  661. except TypeError, exc:
  662. if exc.__class__.__module__ == "exceptions":
  663. msg = "%s: %s" % (exc.__class__.__name__, exc)
  664. else:
  665. msg = "%s.%s: %s" % (exc.__class__.__module__, exc.__class__.__name__, exc)
  666. frame.SetStatusText(msg)
  667. def enterattr(self, row, col):
  668. try:
  669. attr = self.table._displayattrs[col]
  670. value = attr.value(self.table.items[row])
  671. except Exception, exc:
  672. self.error_output(str(exc))
  673. else:
  674. self._doenter(value)
  675. def set_footer(self, item):
  676. frame = self.GetParent().GetParent().GetParent()
  677. frame.SetStatusText(" ".join([str(text) for (style, text) in ipipe.xformat(item, "footer", 20)[2]]), 0)
  678. def enter(self, row):
  679. try:
  680. value = self.table.items[row]
  681. except Exception, exc:
  682. self.error_output(str(exc))
  683. else:
  684. self._doenter(value)
  685. def detail(self, row, col):
  686. """
  687. shows a detail-view of the current cell
  688. """
  689. try:
  690. attr = self.table._displayattrs[col]
  691. item = self.table.items[row]
  692. except Exception, exc:
  693. self.error_output(str(exc))
  694. else:
  695. attrs = [ipipe.AttributeDetail(item, attr) for attr in ipipe.xattrs(item, "detail")]
  696. self._doenter(attrs)
  697. def detail_attr(self, row, col):
  698. try:
  699. attr = self.table._displayattrs[col]
  700. item = attr.value(self.table.items[row])
  701. except Exception, exc:
  702. self.error_output(str(exc))
  703. else:
  704. attrs = [ipipe.AttributeDetail(item, attr) for attr in ipipe.xattrs(item, "detail")]
  705. self._doenter(attrs)
  706. def quit(self, result=None):
  707. """
  708. quit
  709. """
  710. frame = self.GetParent().GetParent().GetParent()
  711. if frame.helpdialog:
  712. frame.helpdialog.Destroy()
  713. app = frame.parent
  714. if app is not None:
  715. app.result = result
  716. frame.Close()
  717. frame.Destroy()
  718. def cell_doubleclicked(self, event):
  719. self.enterattr(event.GetRow(), event.GetCol())
  720. event.Skip()
  721. def cell_leftclicked(self, event):
  722. row = event.GetRow()
  723. item = self.table.items[row]
  724. self.set_footer(item)
  725. event.Skip()
  726. def pick(self, row):
  727. """
  728. pick a single row and return to the IPython prompt
  729. """
  730. try:
  731. value = self.table.items[row]
  732. except Exception, exc:
  733. self.error_output(str(exc))
  734. else:
  735. self.quit(value)
  736. def pickinput(self, row):
  737. try:
  738. value = self.table.items[row]
  739. except Exception, exc:
  740. self.error_output(str(exc))
  741. else:
  742. api = ipapi.get()
  743. api.set_next_input(str(value))
  744. self.quit(value)
  745. def pickinputattr(self, row, col):
  746. try:
  747. attr = self.table._displayattrs[col]
  748. value = attr.value(self.table.items[row])
  749. except Exception, exc:
  750. self.error_output(str(exc))
  751. else:
  752. api = ipapi.get()
  753. api.set_next_input(str(value))
  754. self.quit(value)
  755. def pickrows(self, rows):
  756. """
  757. pick multiple rows and return to the IPython prompt
  758. """
  759. try:
  760. value = [self.table.items[row] for row in rows]
  761. except Exception, exc:
  762. self.error_output(str(exc))
  763. else:
  764. self.quit(value)
  765. def pickrowsattr(self, rows, col):
  766. """"
  767. pick one column from multiple rows
  768. """
  769. values = []
  770. try:
  771. attr = self.table._displayattrs[col]
  772. for row in rows:
  773. try:
  774. values.append(attr.value(self.table.items[row]))
  775. except (SystemExit, KeyboardInterrupt):
  776. raise
  777. except Exception:
  778. raise #pass
  779. except Exception, exc:
  780. self.error_output(str(exc))
  781. else:
  782. self.quit(values)
  783. def pickattr(self, row, col):
  784. try:
  785. attr = self.table._displayattrs[col]
  786. value = attr.value(self.table.items[row])
  787. except Exception, exc:
  788. self.error_output(str(exc))
  789. else:
  790. self.quit(value)
  791. class IGridPanel(wx.Panel):
  792. # Each IGridPanel contains an IGridGrid
  793. def __init__(self, parent, input, *attrs):
  794. wx.Panel.__init__(self, parent, -1)
  795. self.grid = IGridGrid(self, input, *attrs)
  796. self.grid.FitInside()
  797. sizer = wx.BoxSizer(wx.VERTICAL)
  798. sizer.Add(self.grid, proportion=1, flag=wx.EXPAND | wx.ALL, border=10)
  799. self.SetSizer(sizer)
  800. sizer.Fit(self)
  801. sizer.SetSizeHints(self)
  802. class IGridHTMLHelp(wx.Frame):
  803. def __init__(self, parent, title, size):
  804. wx.Frame.__init__(self, parent, -1, title, size=size)
  805. html = wx.html.HtmlWindow(self)
  806. if "gtk2" in wx.PlatformInfo:
  807. html.SetStandardFonts()
  808. html.SetPage(help)
  809. class IGridFrame(wx.Frame):
  810. maxtitlelen = 30
  811. def __init__(self, parent, input):
  812. title = " ".join([str(text) for (style, text) in ipipe.xformat(input, "header", 20)[2]])
  813. wx.Frame.__init__(self, None, title=title, size=(640, 480))
  814. self.menubar = wx.MenuBar()
  815. self.menucounter = 100
  816. self.m_help = wx.Menu()
  817. self.m_search = wx.Menu()
  818. self.m_sort = wx.Menu()
  819. self.m_refresh = wx.Menu()
  820. self.notebook = wx.Notebook(self, -1, style=0)
  821. self.statusbar = self.CreateStatusBar(1, wx.ST_SIZEGRIP)
  822. self.statusbar.SetFieldsCount(2)
  823. self.SetStatusWidths([-1, 200])
  824. self.parent = parent
  825. self._add_notebook(input)
  826. self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
  827. self.makemenu(self.m_sort, "&Sort (asc)\tV", "Sort ascending", self.sortasc)
  828. self.makemenu(self.m_sort, "Sort (&desc)\tShift-V", "Sort descending", self.sortdesc)
  829. self.makemenu(self.m_help, "&Help\tF1", "Help", self.display_help)
  830. # self.makemenu(self.m_help, "&Show help in browser", "Show help in browser", self.display_help_in_browser)
  831. self.makemenu(self.m_search, "&Find text\tCTRL-F", "Find text", self.enter_searchtext)
  832. self.makemenu(self.m_search, "Find by &expression\tCTRL-Shift-F", "Find by expression", self.enter_searchexpression)
  833. self.makemenu(self.m_search, "Find &next\tF3", "Find next", self.find_next)
  834. self.makemenu(self.m_search, "Find &previous\tShift-F3", "Find previous", self.find_previous)
  835. self.makemenu(self.m_refresh, "&Refresh once \tF5", "Refresh once", self.refresh_once)
  836. self.makemenu(self.m_refresh, "Refresh every &1s", "Refresh every second", self.refresh_every_second)
  837. self.makemenu(self.m_refresh, "Refresh every &X seconds", "Refresh every X seconds", self.refresh_interval)
  838. self.makemenu(self.m_refresh, "&Stop all refresh timers", "Stop refresh timers", self.stop_refresh)
  839. self.menubar.Append(self.m_search, "&Find")
  840. self.menubar.Append(self.m_sort, "&Sort")
  841. self.menubar.Append(self.m_refresh, "&Refresh")
  842. self.menubar.Append(self.m_help, "&Help")
  843. self.SetMenuBar(self.menubar)
  844. self.searchtext = ""
  845. self.searchexpression = ""
  846. self.helpdialog = None
  847. self.refresh_interval = 1000
  848. self.SetStatusText("Refreshing inactive", 1)
  849. def refresh_once(self, event):
  850. table = self.notebook.GetPage(self.notebook.GetSelection()).grid.table
  851. table.refresh_content(event)
  852. def refresh_interval(self, event):
  853. table = self.notebook.GetPage(self.notebook.GetSelection()).grid.table
  854. dlg = wx.TextEntryDialog(self, "Enter refresh interval (milliseconds):", "Refresh timer:", defaultValue=str(self.refresh_interval))
  855. if dlg.ShowModal() == wx.ID_OK:
  856. try:
  857. milliseconds = int(dlg.GetValue())
  858. except ValueError, exc:
  859. self.SetStatusText(str(exc))
  860. else:
  861. table.timer.Start(milliseconds=milliseconds, oneShot=False)
  862. self.SetStatusText("Refresh timer set to %s ms" % milliseconds)
  863. self.SetStatusText("Refresh interval: %s ms" % milliseconds, 1)
  864. self.refresh_interval = milliseconds
  865. dlg.Destroy()
  866. def stop_refresh(self, event):
  867. for i in xrange(self.notebook.GetPageCount()):
  868. nb = self.notebook.GetPage(i)
  869. nb.grid.table.timer.Stop()
  870. self.SetStatusText("Refreshing inactive", 1)
  871. def refresh_every_second(self, event):
  872. table = self.notebook.GetPage(self.notebook.GetSelection()).grid.table
  873. table.timer.Start(milliseconds=1000, oneShot=False)
  874. self.SetStatusText("Refresh interval: 1000 ms", 1)
  875. def sortasc(self, event):
  876. grid = self.notebook.GetPage(self.notebook.GetSelection()).grid
  877. grid.sortattrasc()
  878. def sortdesc(self, event):
  879. grid = self.notebook.GetPage(self.notebook.GetSelection()).grid
  880. grid.sortattrdesc()
  881. def find_previous(self, event):
  882. """
  883. find previous occurrences
  884. """
  885. grid = self.notebook.GetPage(self.notebook.GetSelection()).grid
  886. if self.searchtext:
  887. row = grid.GetGridCursorRow()
  888. col = grid.GetGridCursorCol()
  889. self.SetStatusText('Search mode: text; looking for %s' % self.searchtext)
  890. if col-1 >= 0:
  891. grid.search(self.searchtext, row, col-1, False)
  892. else:
  893. grid.search(self.searchtext, row-1, grid.table.GetNumberCols()-1, False)
  894. elif self.searchexpression:
  895. self.SetStatusText("Search mode: expression; looking for %s" % repr(self.searchexpression)[2:-1])
  896. grid.searchexpression(searchexp=self.searchexpression, search_forward=False)
  897. else:
  898. self.SetStatusText("No search yet: please enter search-text or -expression")
  899. def find_next(self, event):
  900. """
  901. find the next occurrence
  902. """
  903. grid = self.notebook.GetPage(self.notebook.GetSelection()).grid
  904. if self.searchtext != "":
  905. row = grid.GetGridCursorRow()
  906. col = grid.GetGridCursorCol()
  907. self.SetStatusText('Search mode: text; looking for %s' % self.searchtext)
  908. if col+1 < grid.table.GetNumberCols():
  909. grid.search(self.searchtext, row, col+1)
  910. else:
  911. grid.search(self.searchtext, row+1, 0)
  912. elif self.searchexpression != "":
  913. self.SetStatusText('Search mode: expression; looking for %s' % repr(self.searchexpression)[2:-1])
  914. grid.searchexpression(searchexp=self.searchexpression)
  915. else:
  916. self.SetStatusText("No search yet: please enter search-text or -expression")
  917. def display_help(self, event):
  918. """
  919. Display a help dialog
  920. """
  921. if self.helpdialog:
  922. self.helpdialog.Destroy()
  923. self.helpdialog = IGridHTMLHelp(None, title="Help", size=wx.Size(600,400))
  924. self.helpdialog.Show()
  925. def display_help_in_browser(self, event):
  926. """
  927. Show the help-HTML in a browser (as a ``HtmlWindow`` does not understand
  928. CSS this looks better)
  929. """
  930. filename = urllib.pathname2url(os.path.abspath(os.path.join(os.path.dirname(__file__), "igrid_help.html")))
  931. if not filename.startswith("file"):
  932. filename = "file:" + filename
  933. webbrowser.open(filename, new=1, autoraise=True)
  934. def enter_searchexpression(self, event):
  935. dlg = wx.TextEntryDialog(self, "Find:", "Find matching expression:", defaultValue=self.searchexpression)
  936. if dlg.ShowModal() == wx.ID_OK:
  937. self.searchexpression = dlg.GetValue()
  938. self.searchtext = ""
  939. self.SetStatusText('Search mode: expression; looking for %s' % repr(self.searchexpression)[2:-1])
  940. self.notebook.GetPage(self.notebook.GetSelection()).grid.searchexpression(self.searchexpression)
  941. dlg.Destroy()
  942. def makemenu(self, menu, label, help, cmd):
  943. menu.Append(self.menucounter, label, help)
  944. self.Bind(wx.EVT_MENU, cmd, id=self.menucounter)
  945. self.menucounter += 1
  946. def _add_notebook(self, input, *attrs):
  947. # Adds another notebook which has the starting object ``input``
  948. panel = IGridPanel(self.notebook, input, *attrs)
  949. text = str(ipipe.xformat(input, "header", self.maxtitlelen)[2])
  950. if len(text) >= self.maxtitlelen:
  951. text = text[:self.maxtitlelen].rstrip(".") + "..."
  952. self.notebook.AddPage(panel, text, True)
  953. panel.grid.SetFocus()
  954. self.Layout()
  955. def OnCloseWindow(self, event):
  956. self.Destroy()
  957. def enter_searchtext(self, event):
  958. # Displays a dialog asking for the searchtext
  959. dlg = wx.TextEntryDialog(self, "Find:", "Find in list", defaultValue=self.searchtext)
  960. if dlg.ShowModal() == wx.ID_OK:
  961. self.searchtext = dlg.GetValue()
  962. self.searchexpression = ""
  963. self.SetStatusText('Search mode: text; looking for %s' % self.searchtext)
  964. self.notebook.GetPage(self.notebook.GetSelection()).grid.search(self.searchtext)
  965. dlg.Destroy()
  966. class App(wx.App):
  967. def __init__(self, input):
  968. self.input = input
  969. self.result = None # Result to be returned to IPython. Set by quit().
  970. wx.App.__init__(self)
  971. def OnInit(self):
  972. frame = IGridFrame(self, self.input)
  973. frame.Show()
  974. self.SetTopWindow(frame)
  975. frame.Raise()
  976. return True
  977. class igrid(ipipe.Display):
  978. """
  979. This is a wx-based display object that can be used instead of ``ibrowse``
  980. (which is curses-based) or ``idump`` (which simply does a print).
  981. """
  982. if wx.VERSION < (2, 7):
  983. def display(self):
  984. try:
  985. # Try to create a "standalone" frame. If this works we're probably
  986. # running with -wthread.
  987. # Note that this sets the parent of the frame to None, but we can't
  988. # pass a result object back to the shell anyway.
  989. frame = IGridFrame(None, self.input)
  990. frame.Show()
  991. frame.Raise()
  992. except wx.PyNoAppError:
  993. # There's no wx application yet => create one.
  994. app = App(self.input)
  995. app.MainLoop()
  996. return app.result
  997. else:
  998. # With wx 2.7 it gets simpler.
  999. def display(self):
  1000. app = App(self.input)
  1001. app.MainLoop()
  1002. return app.result