ss1.py 25 KB

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