/Demo/tkinter/guido/ss1.py

http://unladen-swallow.googlecode.com/ · Python · 845 lines · 760 code · 52 blank · 33 comment · 80 complexity · 465a8d320b3143446a12ac15c79669db MD5 · raw file

  1. """SS1 -- a spreadsheet."""
  2. import os
  3. import re
  4. import sys
  5. import cgi
  6. import rexec
  7. from xml.parsers import expat
  8. LEFT, CENTER, RIGHT = "LEFT", "CENTER", "RIGHT"
  9. def ljust(x, n):
  10. return x.ljust(n)
  11. def center(x, n):
  12. return x.center(n)
  13. def rjust(x, n):
  14. return x.rjust(n)
  15. align2action = {LEFT: ljust, CENTER: center, RIGHT: rjust}
  16. align2xml = {LEFT: "left", CENTER: "center", RIGHT: "right"}
  17. xml2align = {"left": LEFT, "center": CENTER, "right": RIGHT}
  18. align2anchor = {LEFT: "w", CENTER: "center", RIGHT: "e"}
  19. def sum(seq):
  20. total = 0
  21. for x in seq:
  22. if x is not None:
  23. total += x
  24. return total
  25. class Sheet:
  26. def __init__(self):
  27. self.cells = {} # {(x, y): cell, ...}
  28. self.rexec = rexec.RExec()
  29. m = self.rexec.add_module('__main__')
  30. m.cell = self.cellvalue
  31. m.cells = self.multicellvalue
  32. m.sum = sum
  33. def cellvalue(self, x, y):
  34. cell = self.getcell(x, y)
  35. if hasattr(cell, 'recalc'):
  36. return cell.recalc(self.rexec)
  37. else:
  38. return cell
  39. def multicellvalue(self, x1, y1, x2, y2):
  40. if x1 > x2:
  41. x1, x2 = x2, x1
  42. if y1 > y2:
  43. y1, y2 = y2, y1
  44. seq = []
  45. for y in range(y1, y2+1):
  46. for x in range(x1, x2+1):
  47. seq.append(self.cellvalue(x, y))
  48. return seq
  49. def getcell(self, x, y):
  50. return self.cells.get((x, y))
  51. def setcell(self, x, y, cell):
  52. assert x > 0 and y > 0
  53. assert isinstance(cell, BaseCell)
  54. self.cells[x, y] = cell
  55. def clearcell(self, x, y):
  56. try:
  57. del self.cells[x, y]
  58. except KeyError:
  59. pass
  60. def clearcells(self, x1, y1, x2, y2):
  61. for xy in self.selectcells(x1, y1, x2, y2):
  62. del self.cells[xy]
  63. def clearrows(self, y1, y2):
  64. self.clearcells(0, y1, sys.maxint, y2)
  65. def clearcolumns(self, x1, x2):
  66. self.clearcells(x1, 0, x2, sys.maxint)
  67. def selectcells(self, x1, y1, x2, y2):
  68. if x1 > x2:
  69. x1, x2 = x2, x1
  70. if y1 > y2:
  71. y1, y2 = y2, y1
  72. return [(x, y) for x, y in self.cells
  73. if x1 <= x <= x2 and y1 <= y <= y2]
  74. def movecells(self, x1, y1, x2, y2, dx, dy):
  75. if dx == 0 and dy == 0:
  76. return
  77. if x1 > x2:
  78. x1, x2 = x2, x1
  79. if y1 > y2:
  80. y1, y2 = y2, y1
  81. assert x1+dx > 0 and y1+dy > 0
  82. new = {}
  83. for x, y in self.cells:
  84. cell = self.cells[x, y]
  85. if hasattr(cell, 'renumber'):
  86. cell = cell.renumber(x1, y1, x2, y2, dx, dy)
  87. if x1 <= x <= x2 and y1 <= y <= y2:
  88. x += dx
  89. y += dy
  90. new[x, y] = cell
  91. self.cells = new
  92. def insertrows(self, y, n):
  93. assert n > 0
  94. self.movecells(0, y, sys.maxint, sys.maxint, 0, n)
  95. def deleterows(self, y1, y2):
  96. if y1 > y2:
  97. y1, y2 = y2, y1
  98. self.clearrows(y1, y2)
  99. self.movecells(0, y2+1, sys.maxint, sys.maxint, 0, y1-y2-1)
  100. def insertcolumns(self, x, n):
  101. assert n > 0
  102. self.movecells(x, 0, sys.maxint, sys.maxint, n, 0)
  103. def deletecolumns(self, x1, x2):
  104. if x1 > x2:
  105. x1, x2 = x2, x1
  106. self.clearcells(x1, x2)
  107. self.movecells(x2+1, 0, sys.maxint, sys.maxint, x1-x2-1, 0)
  108. def getsize(self):
  109. maxx = maxy = 0
  110. for x, y in self.cells:
  111. maxx = max(maxx, x)
  112. maxy = max(maxy, y)
  113. return maxx, maxy
  114. def reset(self):
  115. for cell in self.cells.itervalues():
  116. if hasattr(cell, 'reset'):
  117. cell.reset()
  118. def recalc(self):
  119. self.reset()
  120. for cell in self.cells.itervalues():
  121. if hasattr(cell, 'recalc'):
  122. cell.recalc(self.rexec)
  123. def display(self):
  124. maxx, maxy = self.getsize()
  125. width, height = maxx+1, maxy+1
  126. colwidth = [1] * width
  127. full = {}
  128. # Add column heading labels in row 0
  129. for x in range(1, width):
  130. full[x, 0] = text, alignment = colnum2name(x), RIGHT
  131. colwidth[x] = max(colwidth[x], len(text))
  132. # Add row labels in column 0
  133. for y in range(1, height):
  134. full[0, y] = text, alignment = str(y), RIGHT
  135. colwidth[0] = max(colwidth[0], len(text))
  136. # Add sheet cells in columns with x>0 and y>0
  137. for (x, y), cell in self.cells.iteritems():
  138. if x <= 0 or y <= 0:
  139. continue
  140. if hasattr(cell, 'recalc'):
  141. cell.recalc(self.rexec)
  142. if hasattr(cell, 'format'):
  143. text, alignment = cell.format()
  144. assert isinstance(text, str)
  145. assert alignment in (LEFT, CENTER, RIGHT)
  146. else:
  147. text = str(cell)
  148. if isinstance(cell, str):
  149. alignment = LEFT
  150. else:
  151. alignment = RIGHT
  152. full[x, y] = (text, alignment)
  153. colwidth[x] = max(colwidth[x], len(text))
  154. # Calculate the horizontal separator line (dashes and dots)
  155. sep = ""
  156. for x in range(width):
  157. if sep:
  158. sep += "+"
  159. sep += "-"*colwidth[x]
  160. # Now print The full grid
  161. for y in range(height):
  162. line = ""
  163. for x in range(width):
  164. text, alignment = full.get((x, y)) or ("", LEFT)
  165. text = align2action[alignment](text, colwidth[x])
  166. if line:
  167. line += '|'
  168. line += text
  169. print line
  170. if y == 0:
  171. print sep
  172. def xml(self):
  173. out = ['<spreadsheet>']
  174. for (x, y), cell in self.cells.iteritems():
  175. if hasattr(cell, 'xml'):
  176. cellxml = cell.xml()
  177. else:
  178. cellxml = '<value>%s</value>' % cgi.escape(cell)
  179. out.append('<cell row="%s" col="%s">\n %s\n</cell>' %
  180. (y, x, cellxml))
  181. out.append('</spreadsheet>')
  182. return '\n'.join(out)
  183. def save(self, filename):
  184. text = self.xml()
  185. f = open(filename, "w")
  186. f.write(text)
  187. if text and not text.endswith('\n'):
  188. f.write('\n')
  189. f.close()
  190. def load(self, filename):
  191. f = open(filename, 'r')
  192. SheetParser(self).parsefile(f)
  193. f.close()
  194. class SheetParser:
  195. def __init__(self, sheet):
  196. self.sheet = sheet
  197. def parsefile(self, f):
  198. parser = expat.ParserCreate()
  199. parser.StartElementHandler = self.startelement
  200. parser.EndElementHandler = self.endelement
  201. parser.CharacterDataHandler = self.data
  202. parser.ParseFile(f)
  203. def startelement(self, tag, attrs):
  204. method = getattr(self, 'start_'+tag, None)
  205. if method:
  206. for key, value in attrs.iteritems():
  207. attrs[key] = str(value) # XXX Convert Unicode to 8-bit
  208. method(attrs)
  209. self.texts = []
  210. def data(self, text):
  211. text = str(text) # XXX Convert Unicode to 8-bit
  212. self.texts.append(text)
  213. def endelement(self, tag):
  214. method = getattr(self, 'end_'+tag, None)
  215. if method:
  216. method("".join(self.texts))
  217. def start_cell(self, attrs):
  218. self.y = int(attrs.get("row"))
  219. self.x = int(attrs.get("col"))
  220. def start_value(self, attrs):
  221. self.fmt = attrs.get('format')
  222. self.alignment = xml2align.get(attrs.get('align'))
  223. start_formula = start_value
  224. def end_int(self, text):
  225. try:
  226. self.value = int(text)
  227. except:
  228. self.value = None
  229. def end_long(self, text):
  230. try:
  231. self.value = long(text)
  232. except:
  233. self.value = None
  234. def end_double(self, text):
  235. try:
  236. self.value = float(text)
  237. except:
  238. self.value = None
  239. def end_complex(self, text):
  240. try:
  241. self.value = complex(text)
  242. except:
  243. self.value = None
  244. def end_string(self, text):
  245. try:
  246. self.value = text
  247. except:
  248. self.value = None
  249. def end_value(self, text):
  250. if isinstance(self.value, BaseCell):
  251. self.cell = self.value
  252. elif isinstance(self.value, str):
  253. self.cell = StringCell(self.value,
  254. self.fmt or "%s",
  255. self.alignment or LEFT)
  256. else:
  257. self.cell = NumericCell(self.value,
  258. self.fmt or "%s",
  259. self.alignment or RIGHT)
  260. def end_formula(self, text):
  261. self.cell = FormulaCell(text,
  262. self.fmt or "%s",
  263. self.alignment or RIGHT)
  264. def end_cell(self, text):
  265. self.sheet.setcell(self.x, self.y, self.cell)
  266. class BaseCell:
  267. __init__ = None # Must provide
  268. """Abstract base class for sheet cells.
  269. Subclasses may but needn't provide the following APIs:
  270. cell.reset() -- prepare for recalculation
  271. cell.recalc(rexec) -> value -- recalculate formula
  272. cell.format() -> (value, alignment) -- return formatted value
  273. cell.xml() -> string -- return XML
  274. """
  275. class NumericCell(BaseCell):
  276. def __init__(self, value, fmt="%s", alignment=RIGHT):
  277. assert isinstance(value, (int, long, float, complex))
  278. assert alignment in (LEFT, CENTER, RIGHT)
  279. self.value = value
  280. self.fmt = fmt
  281. self.alignment = alignment
  282. def recalc(self, rexec):
  283. return self.value
  284. def format(self):
  285. try:
  286. text = self.fmt % self.value
  287. except:
  288. text = str(self.value)
  289. return text, self.alignment
  290. def xml(self):
  291. method = getattr(self, '_xml_' + type(self.value).__name__)
  292. return '<value align="%s" format="%s">%s</value>' % (
  293. align2xml[self.alignment],
  294. self.fmt,
  295. method())
  296. def _xml_int(self):
  297. if -2**31 <= self.value < 2**31:
  298. return '<int>%s</int>' % self.value
  299. else:
  300. return self._xml_long()
  301. def _xml_long(self):
  302. return '<long>%s</long>' % self.value
  303. def _xml_float(self):
  304. return '<double>%s</double>' % repr(self.value)
  305. def _xml_complex(self):
  306. return '<complex>%s</double>' % repr(self.value)
  307. class StringCell(BaseCell):
  308. def __init__(self, text, fmt="%s", alignment=LEFT):
  309. assert isinstance(text, (str, unicode))
  310. assert alignment in (LEFT, CENTER, RIGHT)
  311. self.text = text
  312. self.fmt = fmt
  313. self.alignment = alignment
  314. def recalc(self, rexec):
  315. return self.text
  316. def format(self):
  317. return self.text, self.alignment
  318. def xml(self):
  319. s = '<value align="%s" format="%s"><string>%s</string></value>'
  320. return s % (
  321. align2xml[self.alignment],
  322. self.fmt,
  323. cgi.escape(self.text))
  324. class FormulaCell(BaseCell):
  325. def __init__(self, formula, fmt="%s", alignment=RIGHT):
  326. assert alignment in (LEFT, CENTER, RIGHT)
  327. self.formula = formula
  328. self.translated = translate(self.formula)
  329. self.fmt = fmt
  330. self.alignment = alignment
  331. self.reset()
  332. def reset(self):
  333. self.value = None
  334. def recalc(self, rexec):
  335. if self.value is None:
  336. try:
  337. # A hack to evaluate expressions using true division
  338. rexec.r_exec("from __future__ import division\n" +
  339. "__value__ = eval(%s)" % repr(self.translated))
  340. self.value = rexec.r_eval("__value__")
  341. except:
  342. exc = sys.exc_info()[0]
  343. if hasattr(exc, "__name__"):
  344. self.value = exc.__name__
  345. else:
  346. self.value = str(exc)
  347. return self.value
  348. def format(self):
  349. try:
  350. text = self.fmt % self.value
  351. except:
  352. text = str(self.value)
  353. return text, self.alignment
  354. def xml(self):
  355. return '<formula align="%s" format="%s">%s</formula>' % (
  356. align2xml[self.alignment],
  357. self.fmt,
  358. self.formula)
  359. def renumber(self, x1, y1, x2, y2, dx, dy):
  360. out = []
  361. for part in re.split('(\w+)', self.formula):
  362. m = re.match('^([A-Z]+)([1-9][0-9]*)$', part)
  363. if m is not None:
  364. sx, sy = m.groups()
  365. x = colname2num(sx)
  366. y = int(sy)
  367. if x1 <= x <= x2 and y1 <= y <= y2:
  368. part = cellname(x+dx, y+dy)
  369. out.append(part)
  370. return FormulaCell("".join(out), self.fmt, self.alignment)
  371. def translate(formula):
  372. """Translate a formula containing fancy cell names to valid Python code.
  373. Examples:
  374. B4 -> cell(2, 4)
  375. B4:Z100 -> cells(2, 4, 26, 100)
  376. """
  377. out = []
  378. for part in re.split(r"(\w+(?::\w+)?)", formula):
  379. m = re.match(r"^([A-Z]+)([1-9][0-9]*)(?::([A-Z]+)([1-9][0-9]*))?$", part)
  380. if m is None:
  381. out.append(part)
  382. else:
  383. x1, y1, x2, y2 = m.groups()
  384. x1 = colname2num(x1)
  385. if x2 is None:
  386. s = "cell(%s, %s)" % (x1, y1)
  387. else:
  388. x2 = colname2num(x2)
  389. s = "cells(%s, %s, %s, %s)" % (x1, y1, x2, y2)
  390. out.append(s)
  391. return "".join(out)
  392. def cellname(x, y):
  393. "Translate a cell coordinate to a fancy cell name (e.g. (1, 1)->'A1')."
  394. assert x > 0 # Column 0 has an empty name, so can't use that
  395. return colnum2name(x) + str(y)
  396. def colname2num(s):
  397. "Translate a column name to number (e.g. 'A'->1, 'Z'->26, 'AA'->27)."
  398. s = s.upper()
  399. n = 0
  400. for c in s:
  401. assert 'A' <= c <= 'Z'
  402. n = n*26 + ord(c) - ord('A') + 1
  403. return n
  404. def colnum2name(n):
  405. "Translate a column number to name (e.g. 1->'A', etc.)."
  406. assert n > 0
  407. s = ""
  408. while n:
  409. n, m = divmod(n-1, 26)
  410. s = chr(m+ord('A')) + s
  411. return s
  412. import Tkinter as Tk
  413. class SheetGUI:
  414. """Beginnings of a GUI for a spreadsheet.
  415. TO DO:
  416. - clear multiple cells
  417. - Insert, clear, remove rows or columns
  418. - Show new contents while typing
  419. - Scroll bars
  420. - Grow grid when window is grown
  421. - Proper menus
  422. - Undo, redo
  423. - Cut, copy and paste
  424. - Formatting and alignment
  425. """
  426. def __init__(self, filename="sheet1.xml", rows=10, columns=5):
  427. """Constructor.
  428. Load the sheet from the filename argument.
  429. Set up the Tk widget tree.
  430. """
  431. # Create and load the sheet
  432. self.filename = filename
  433. self.sheet = Sheet()
  434. if os.path.isfile(filename):
  435. self.sheet.load(filename)
  436. # Calculate the needed grid size
  437. maxx, maxy = self.sheet.getsize()
  438. rows = max(rows, maxy)
  439. columns = max(columns, maxx)
  440. # Create the widgets
  441. self.root = Tk.Tk()
  442. self.root.wm_title("Spreadsheet: %s" % self.filename)
  443. self.beacon = Tk.Label(self.root, text="A1",
  444. font=('helvetica', 16, 'bold'))
  445. self.entry = Tk.Entry(self.root)
  446. self.savebutton = Tk.Button(self.root, text="Save",
  447. command=self.save)
  448. self.cellgrid = Tk.Frame(self.root)
  449. # Configure the widget lay-out
  450. self.cellgrid.pack(side="bottom", expand=1, fill="both")
  451. self.beacon.pack(side="left")
  452. self.savebutton.pack(side="right")
  453. self.entry.pack(side="left", expand=1, fill="x")
  454. # Bind some events
  455. self.entry.bind("<Return>", self.return_event)
  456. self.entry.bind("<Shift-Return>", self.shift_return_event)
  457. self.entry.bind("<Tab>", self.tab_event)
  458. self.entry.bind("<Shift-Tab>", self.shift_tab_event)
  459. self.entry.bind("<Delete>", self.delete_event)
  460. self.entry.bind("<Escape>", self.escape_event)
  461. # Now create the cell grid
  462. self.makegrid(rows, columns)
  463. # Select the top-left cell
  464. self.currentxy = None
  465. self.cornerxy = None
  466. self.setcurrent(1, 1)
  467. # Copy the sheet cells to the GUI cells
  468. self.sync()
  469. def delete_event(self, event):
  470. if self.cornerxy != self.currentxy and self.cornerxy is not None:
  471. self.sheet.clearcells(*(self.currentxy + self.cornerxy))
  472. else:
  473. self.sheet.clearcell(*self.currentxy)
  474. self.sync()
  475. self.entry.delete(0, 'end')
  476. return "break"
  477. def escape_event(self, event):
  478. x, y = self.currentxy
  479. self.load_entry(x, y)
  480. def load_entry(self, x, y):
  481. cell = self.sheet.getcell(x, y)
  482. if cell is None:
  483. text = ""
  484. elif isinstance(cell, FormulaCell):
  485. text = '=' + cell.formula
  486. else:
  487. text, alignment = cell.format()
  488. self.entry.delete(0, 'end')
  489. self.entry.insert(0, text)
  490. self.entry.selection_range(0, 'end')
  491. def makegrid(self, rows, columns):
  492. """Helper to create the grid of GUI cells.
  493. The edge (x==0 or y==0) is filled with labels; the rest is real cells.
  494. """
  495. self.rows = rows
  496. self.columns = columns
  497. self.gridcells = {}
  498. # Create the top left corner cell (which selects all)
  499. cell = Tk.Label(self.cellgrid, relief='raised')
  500. cell.grid_configure(column=0, row=0, sticky='NSWE')
  501. cell.bind("<ButtonPress-1>", self.selectall)
  502. # Create the top row of labels, and confiure the grid columns
  503. for x in range(1, columns+1):
  504. self.cellgrid.grid_columnconfigure(x, minsize=64)
  505. cell = Tk.Label(self.cellgrid, text=colnum2name(x), relief='raised')
  506. cell.grid_configure(column=x, row=0, sticky='WE')
  507. self.gridcells[x, 0] = cell
  508. cell.__x = x
  509. cell.__y = 0
  510. cell.bind("<ButtonPress-1>", self.selectcolumn)
  511. cell.bind("<B1-Motion>", self.extendcolumn)
  512. cell.bind("<ButtonRelease-1>", self.extendcolumn)
  513. cell.bind("<Shift-Button-1>", self.extendcolumn)
  514. # Create the leftmost column of labels
  515. for y in range(1, rows+1):
  516. cell = Tk.Label(self.cellgrid, text=str(y), relief='raised')
  517. cell.grid_configure(column=0, row=y, sticky='WE')
  518. self.gridcells[0, y] = cell
  519. cell.__x = 0
  520. cell.__y = y
  521. cell.bind("<ButtonPress-1>", self.selectrow)
  522. cell.bind("<B1-Motion>", self.extendrow)
  523. cell.bind("<ButtonRelease-1>", self.extendrow)
  524. cell.bind("<Shift-Button-1>", self.extendrow)
  525. # Create the real cells
  526. for x in range(1, columns+1):
  527. for y in range(1, rows+1):
  528. cell = Tk.Label(self.cellgrid, relief='sunken',
  529. bg='white', fg='black')
  530. cell.grid_configure(column=x, row=y, sticky='NSWE')
  531. self.gridcells[x, y] = cell
  532. cell.__x = x
  533. cell.__y = y
  534. # Bind mouse events
  535. cell.bind("<ButtonPress-1>", self.press)
  536. cell.bind("<B1-Motion>", self.motion)
  537. cell.bind("<ButtonRelease-1>", self.release)
  538. cell.bind("<Shift-Button-1>", self.release)
  539. def selectall(self, event):
  540. self.setcurrent(1, 1)
  541. self.setcorner(sys.maxint, sys.maxint)
  542. def selectcolumn(self, event):
  543. x, y = self.whichxy(event)
  544. self.setcurrent(x, 1)
  545. self.setcorner(x, sys.maxint)
  546. def extendcolumn(self, event):
  547. x, y = self.whichxy(event)
  548. if x > 0:
  549. self.setcurrent(self.currentxy[0], 1)
  550. self.setcorner(x, sys.maxint)
  551. def selectrow(self, event):
  552. x, y = self.whichxy(event)
  553. self.setcurrent(1, y)
  554. self.setcorner(sys.maxint, y)
  555. def extendrow(self, event):
  556. x, y = self.whichxy(event)
  557. if y > 0:
  558. self.setcurrent(1, self.currentxy[1])
  559. self.setcorner(sys.maxint, y)
  560. def press(self, event):
  561. x, y = self.whichxy(event)
  562. if x > 0 and y > 0:
  563. self.setcurrent(x, y)
  564. def motion(self, event):
  565. x, y = self.whichxy(event)
  566. if x > 0 and y > 0:
  567. self.setcorner(x, y)
  568. release = motion
  569. def whichxy(self, event):
  570. w = self.cellgrid.winfo_containing(event.x_root, event.y_root)
  571. if w is not None and isinstance(w, Tk.Label):
  572. try:
  573. return w.__x, w.__y
  574. except AttributeError:
  575. pass
  576. return 0, 0
  577. def save(self):
  578. self.sheet.save(self.filename)
  579. def setcurrent(self, x, y):
  580. "Make (x, y) the current cell."
  581. if self.currentxy is not None:
  582. self.change_cell()
  583. self.clearfocus()
  584. self.beacon['text'] = cellname(x, y)
  585. self.load_entry(x, y)
  586. self.entry.focus_set()
  587. self.currentxy = x, y
  588. self.cornerxy = None
  589. gridcell = self.gridcells.get(self.currentxy)
  590. if gridcell is not None:
  591. gridcell['bg'] = 'yellow'
  592. def setcorner(self, x, y):
  593. if self.currentxy is None or self.currentxy == (x, y):
  594. self.setcurrent(x, y)
  595. return
  596. self.clearfocus()
  597. self.cornerxy = x, y
  598. x1, y1 = self.currentxy
  599. x2, y2 = self.cornerxy or self.currentxy
  600. if x1 > x2:
  601. x1, x2 = x2, x1
  602. if y1 > y2:
  603. y1, y2 = y2, y1
  604. for (x, y), cell in self.gridcells.iteritems():
  605. if x1 <= x <= x2 and y1 <= y <= y2:
  606. cell['bg'] = 'lightBlue'
  607. gridcell = self.gridcells.get(self.currentxy)
  608. if gridcell is not None:
  609. gridcell['bg'] = 'yellow'
  610. self.setbeacon(x1, y1, x2, y2)
  611. def setbeacon(self, x1, y1, x2, y2):
  612. if x1 == y1 == 1 and x2 == y2 == sys.maxint:
  613. name = ":"
  614. elif (x1, x2) == (1, sys.maxint):
  615. if y1 == y2:
  616. name = "%d" % y1
  617. else:
  618. name = "%d:%d" % (y1, y2)
  619. elif (y1, y2) == (1, sys.maxint):
  620. if x1 == x2:
  621. name = "%s" % colnum2name(x1)
  622. else:
  623. name = "%s:%s" % (colnum2name(x1), colnum2name(x2))
  624. else:
  625. name1 = cellname(*self.currentxy)
  626. name2 = cellname(*self.cornerxy)
  627. name = "%s:%s" % (name1, name2)
  628. self.beacon['text'] = name
  629. def clearfocus(self):
  630. if self.currentxy is not None:
  631. x1, y1 = self.currentxy
  632. x2, y2 = self.cornerxy or self.currentxy
  633. if x1 > x2:
  634. x1, x2 = x2, x1
  635. if y1 > y2:
  636. y1, y2 = y2, y1
  637. for (x, y), cell in self.gridcells.iteritems():
  638. if x1 <= x <= x2 and y1 <= y <= y2:
  639. cell['bg'] = 'white'
  640. def return_event(self, event):
  641. "Callback for the Return key."
  642. self.change_cell()
  643. x, y = self.currentxy
  644. self.setcurrent(x, y+1)
  645. return "break"
  646. def shift_return_event(self, event):
  647. "Callback for the Return key with Shift modifier."
  648. self.change_cell()
  649. x, y = self.currentxy
  650. self.setcurrent(x, max(1, y-1))
  651. return "break"
  652. def tab_event(self, event):
  653. "Callback for the Tab key."
  654. self.change_cell()
  655. x, y = self.currentxy
  656. self.setcurrent(x+1, y)
  657. return "break"
  658. def shift_tab_event(self, event):
  659. "Callback for the Tab key with Shift modifier."
  660. self.change_cell()
  661. x, y = self.currentxy
  662. self.setcurrent(max(1, x-1), y)
  663. return "break"
  664. def change_cell(self):
  665. "Set the current cell from the entry widget."
  666. x, y = self.currentxy
  667. text = self.entry.get()
  668. cell = None
  669. if text.startswith('='):
  670. cell = FormulaCell(text[1:])
  671. else:
  672. for cls in int, long, float, complex:
  673. try:
  674. value = cls(text)
  675. except:
  676. continue
  677. else:
  678. cell = NumericCell(value)
  679. break
  680. if cell is None and text:
  681. cell = StringCell(text)
  682. if cell is None:
  683. self.sheet.clearcell(x, y)
  684. else:
  685. self.sheet.setcell(x, y, cell)
  686. self.sync()
  687. def sync(self):
  688. "Fill the GUI cells from the sheet cells."
  689. self.sheet.recalc()
  690. for (x, y), gridcell in self.gridcells.iteritems():
  691. if x == 0 or y == 0:
  692. continue
  693. cell = self.sheet.getcell(x, y)
  694. if cell is None:
  695. gridcell['text'] = ""
  696. else:
  697. if hasattr(cell, 'format'):
  698. text, alignment = cell.format()
  699. else:
  700. text, alignment = str(cell), LEFT
  701. gridcell['text'] = text
  702. gridcell['anchor'] = align2anchor[alignment]
  703. def test_basic():
  704. "Basic non-gui self-test."
  705. import os
  706. a = Sheet()
  707. for x in range(1, 11):
  708. for y in range(1, 11):
  709. if x == 1:
  710. cell = NumericCell(y)
  711. elif y == 1:
  712. cell = NumericCell(x)
  713. else:
  714. c1 = cellname(x, 1)
  715. c2 = cellname(1, y)
  716. formula = "%s*%s" % (c1, c2)
  717. cell = FormulaCell(formula)
  718. a.setcell(x, y, cell)
  719. ## if os.path.isfile("sheet1.xml"):
  720. ## print "Loading from sheet1.xml"
  721. ## a.load("sheet1.xml")
  722. a.display()
  723. a.save("sheet1.xml")
  724. def test_gui():
  725. "GUI test."
  726. if sys.argv[1:]:
  727. filename = sys.argv[1]
  728. else:
  729. filename = "sheet1.xml"
  730. g = SheetGUI(filename)
  731. g.root.mainloop()
  732. if __name__ == '__main__':
  733. #test_basic()
  734. test_gui()