blob: a6c8c2181edcecc358629acddea666361d9d31d1 [file] [log] [blame]
Guido van Rossum69ccfcc2002-10-28 01:06:37 +00001"""SS1 -- a spreadsheet."""
2
3import os
4import re
5import sys
6import cgi
Guido van Rossum69ccfcc2002-10-28 01:06:37 +00007from xml.parsers import expat
8
9LEFT, CENTER, RIGHT = "LEFT", "CENTER", "RIGHT"
10
11def ljust(x, n):
12 return x.ljust(n)
13def center(x, n):
14 return x.center(n)
15def rjust(x, n):
16 return x.rjust(n)
17align2action = {LEFT: ljust, CENTER: center, RIGHT: rjust}
18
19align2xml = {LEFT: "left", CENTER: "center", RIGHT: "right"}
20xml2align = {"left": LEFT, "center": CENTER, "right": RIGHT}
21
22align2anchor = {LEFT: "w", CENTER: "center", RIGHT: "e"}
23
24def sum(seq):
25 total = 0
26 for x in seq:
27 if x is not None:
28 total += x
29 return total
30
31class Sheet:
32
33 def __init__(self):
34 self.cells = {} # {(x, y): cell, ...}
Georg Brandla52bae72010-08-21 23:20:01 +000035 self.ns = dict(
36 cell = self.cellvalue,
37 cells = self.multicellvalue,
38 sum = sum,
39 )
Guido van Rossum69ccfcc2002-10-28 01:06:37 +000040
41 def cellvalue(self, x, y):
42 cell = self.getcell(x, y)
43 if hasattr(cell, 'recalc'):
Georg Brandla52bae72010-08-21 23:20:01 +000044 return cell.recalc(self.ns)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +000045 else:
46 return cell
47
48 def multicellvalue(self, x1, y1, x2, y2):
49 if x1 > x2:
50 x1, x2 = x2, x1
51 if y1 > y2:
52 y1, y2 = y2, y1
53 seq = []
54 for y in range(y1, y2+1):
55 for x in range(x1, x2+1):
56 seq.append(self.cellvalue(x, y))
57 return seq
58
59 def getcell(self, x, y):
60 return self.cells.get((x, y))
61
62 def setcell(self, x, y, cell):
63 assert x > 0 and y > 0
64 assert isinstance(cell, BaseCell)
65 self.cells[x, y] = cell
66
67 def clearcell(self, x, y):
68 try:
69 del self.cells[x, y]
70 except KeyError:
71 pass
72
73 def clearcells(self, x1, y1, x2, y2):
74 for xy in self.selectcells(x1, y1, x2, y2):
75 del self.cells[xy]
76
77 def clearrows(self, y1, y2):
78 self.clearcells(0, y1, sys.maxint, y2)
79
80 def clearcolumns(self, x1, x2):
81 self.clearcells(x1, 0, x2, sys.maxint)
82
83 def selectcells(self, x1, y1, x2, y2):
84 if x1 > x2:
85 x1, x2 = x2, x1
86 if y1 > y2:
87 y1, y2 = y2, y1
88 return [(x, y) for x, y in self.cells
89 if x1 <= x <= x2 and y1 <= y <= y2]
90
91 def movecells(self, x1, y1, x2, y2, dx, dy):
92 if dx == 0 and dy == 0:
93 return
94 if x1 > x2:
95 x1, x2 = x2, x1
96 if y1 > y2:
97 y1, y2 = y2, y1
98 assert x1+dx > 0 and y1+dy > 0
99 new = {}
100 for x, y in self.cells:
101 cell = self.cells[x, y]
102 if hasattr(cell, 'renumber'):
103 cell = cell.renumber(x1, y1, x2, y2, dx, dy)
104 if x1 <= x <= x2 and y1 <= y <= y2:
105 x += dx
106 y += dy
107 new[x, y] = cell
108 self.cells = new
109
110 def insertrows(self, y, n):
111 assert n > 0
112 self.movecells(0, y, sys.maxint, sys.maxint, 0, n)
113
114 def deleterows(self, y1, y2):
115 if y1 > y2:
116 y1, y2 = y2, y1
117 self.clearrows(y1, y2)
118 self.movecells(0, y2+1, sys.maxint, sys.maxint, 0, y1-y2-1)
119
120 def insertcolumns(self, x, n):
121 assert n > 0
122 self.movecells(x, 0, sys.maxint, sys.maxint, n, 0)
123
124 def deletecolumns(self, x1, x2):
125 if x1 > x2:
126 x1, x2 = x2, x1
127 self.clearcells(x1, x2)
128 self.movecells(x2+1, 0, sys.maxint, sys.maxint, x1-x2-1, 0)
129
130 def getsize(self):
131 maxx = maxy = 0
132 for x, y in self.cells:
133 maxx = max(maxx, x)
134 maxy = max(maxy, y)
135 return maxx, maxy
136
137 def reset(self):
Collin Winter6f2df4d2007-07-17 20:59:35 +0000138 for cell in self.cells.values():
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000139 if hasattr(cell, 'reset'):
140 cell.reset()
141
142 def recalc(self):
143 self.reset()
Collin Winter6f2df4d2007-07-17 20:59:35 +0000144 for cell in self.cells.values():
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000145 if hasattr(cell, 'recalc'):
Georg Brandla52bae72010-08-21 23:20:01 +0000146 cell.recalc(self.ns)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000147
148 def display(self):
149 maxx, maxy = self.getsize()
150 width, height = maxx+1, maxy+1
151 colwidth = [1] * width
152 full = {}
153 # Add column heading labels in row 0
154 for x in range(1, width):
155 full[x, 0] = text, alignment = colnum2name(x), RIGHT
156 colwidth[x] = max(colwidth[x], len(text))
157 # Add row labels in column 0
158 for y in range(1, height):
159 full[0, y] = text, alignment = str(y), RIGHT
160 colwidth[0] = max(colwidth[0], len(text))
161 # Add sheet cells in columns with x>0 and y>0
Collin Winter6f2df4d2007-07-17 20:59:35 +0000162 for (x, y), cell in self.cells.items():
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000163 if x <= 0 or y <= 0:
164 continue
165 if hasattr(cell, 'recalc'):
Georg Brandla52bae72010-08-21 23:20:01 +0000166 cell.recalc(self.ns)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000167 if hasattr(cell, 'format'):
168 text, alignment = cell.format()
169 assert isinstance(text, str)
170 assert alignment in (LEFT, CENTER, RIGHT)
171 else:
172 text = str(cell)
173 if isinstance(cell, str):
174 alignment = LEFT
175 else:
176 alignment = RIGHT
177 full[x, y] = (text, alignment)
178 colwidth[x] = max(colwidth[x], len(text))
179 # Calculate the horizontal separator line (dashes and dots)
180 sep = ""
181 for x in range(width):
182 if sep:
183 sep += "+"
184 sep += "-"*colwidth[x]
185 # Now print The full grid
186 for y in range(height):
187 line = ""
188 for x in range(width):
189 text, alignment = full.get((x, y)) or ("", LEFT)
190 text = align2action[alignment](text, colwidth[x])
191 if line:
192 line += '|'
193 line += text
Collin Winter6f2df4d2007-07-17 20:59:35 +0000194 print(line)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000195 if y == 0:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000196 print(sep)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000197
198 def xml(self):
199 out = ['<spreadsheet>']
Collin Winter6f2df4d2007-07-17 20:59:35 +0000200 for (x, y), cell in self.cells.items():
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000201 if hasattr(cell, 'xml'):
202 cellxml = cell.xml()
203 else:
204 cellxml = '<value>%s</value>' % cgi.escape(cell)
205 out.append('<cell row="%s" col="%s">\n %s\n</cell>' %
206 (y, x, cellxml))
207 out.append('</spreadsheet>')
208 return '\n'.join(out)
209
210 def save(self, filename):
211 text = self.xml()
212 f = open(filename, "w")
213 f.write(text)
214 if text and not text.endswith('\n'):
215 f.write('\n')
216 f.close()
217
218 def load(self, filename):
219 f = open(filename, 'r')
220 SheetParser(self).parsefile(f)
221 f.close()
222
223class SheetParser:
224
225 def __init__(self, sheet):
226 self.sheet = sheet
227
228 def parsefile(self, f):
229 parser = expat.ParserCreate()
230 parser.StartElementHandler = self.startelement
231 parser.EndElementHandler = self.endelement
232 parser.CharacterDataHandler = self.data
233 parser.ParseFile(f)
234
235 def startelement(self, tag, attrs):
236 method = getattr(self, 'start_'+tag, None)
237 if method:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000238 for key, value in attrs.items():
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000239 attrs[key] = str(value) # XXX Convert Unicode to 8-bit
240 method(attrs)
241 self.texts = []
242
243 def data(self, text):
244 text = str(text) # XXX Convert Unicode to 8-bit
245 self.texts.append(text)
246
247 def endelement(self, tag):
248 method = getattr(self, 'end_'+tag, None)
249 if method:
250 method("".join(self.texts))
251
252 def start_cell(self, attrs):
253 self.y = int(attrs.get("row"))
254 self.x = int(attrs.get("col"))
255
256 def start_value(self, attrs):
257 self.fmt = attrs.get('format')
258 self.alignment = xml2align.get(attrs.get('align'))
259
260 start_formula = start_value
261
262 def end_int(self, text):
263 try:
264 self.value = int(text)
265 except:
266 self.value = None
267
268 def end_long(self, text):
269 try:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000270 self.value = int(text)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000271 except:
272 self.value = None
273
274 def end_double(self, text):
275 try:
276 self.value = float(text)
277 except:
278 self.value = None
279
280 def end_complex(self, text):
281 try:
282 self.value = complex(text)
283 except:
284 self.value = None
285
286 def end_string(self, text):
287 try:
288 self.value = text
289 except:
290 self.value = None
291
292 def end_value(self, text):
293 if isinstance(self.value, BaseCell):
294 self.cell = self.value
295 elif isinstance(self.value, str):
296 self.cell = StringCell(self.value,
297 self.fmt or "%s",
298 self.alignment or LEFT)
299 else:
300 self.cell = NumericCell(self.value,
301 self.fmt or "%s",
302 self.alignment or RIGHT)
303
304 def end_formula(self, text):
305 self.cell = FormulaCell(text,
306 self.fmt or "%s",
307 self.alignment or RIGHT)
308
309 def end_cell(self, text):
310 self.sheet.setcell(self.x, self.y, self.cell)
311
312class BaseCell:
313 __init__ = None # Must provide
314 """Abstract base class for sheet cells.
315
316 Subclasses may but needn't provide the following APIs:
Tim Peters182b5ac2004-07-18 06:16:08 +0000317
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000318 cell.reset() -- prepare for recalculation
Georg Brandla52bae72010-08-21 23:20:01 +0000319 cell.recalc(ns) -> value -- recalculate formula
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000320 cell.format() -> (value, alignment) -- return formatted value
321 cell.xml() -> string -- return XML
322 """
323
324class NumericCell(BaseCell):
325
326 def __init__(self, value, fmt="%s", alignment=RIGHT):
Collin Winter6f2df4d2007-07-17 20:59:35 +0000327 assert isinstance(value, (int, int, float, complex))
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000328 assert alignment in (LEFT, CENTER, RIGHT)
329 self.value = value
330 self.fmt = fmt
331 self.alignment = alignment
332
Georg Brandla52bae72010-08-21 23:20:01 +0000333 def recalc(self, ns):
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000334 return self.value
335
336 def format(self):
337 try:
338 text = self.fmt % self.value
339 except:
340 text = str(self.value)
341 return text, self.alignment
342
343 def xml(self):
344 method = getattr(self, '_xml_' + type(self.value).__name__)
345 return '<value align="%s" format="%s">%s</value>' % (
346 align2xml[self.alignment],
347 self.fmt,
348 method())
349
350 def _xml_int(self):
351 if -2**31 <= self.value < 2**31:
352 return '<int>%s</int>' % self.value
353 else:
354 return self._xml_long()
355
356 def _xml_long(self):
357 return '<long>%s</long>' % self.value
358
359 def _xml_float(self):
360 return '<double>%s</double>' % repr(self.value)
361
362 def _xml_complex(self):
363 return '<complex>%s</double>' % repr(self.value)
364
365class StringCell(BaseCell):
366
367 def __init__(self, text, fmt="%s", alignment=LEFT):
Collin Winter6f2df4d2007-07-17 20:59:35 +0000368 assert isinstance(text, (str, str))
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000369 assert alignment in (LEFT, CENTER, RIGHT)
370 self.text = text
371 self.fmt = fmt
372 self.alignment = alignment
373
Georg Brandla52bae72010-08-21 23:20:01 +0000374 def recalc(self, ns):
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000375 return self.text
376
377 def format(self):
378 return self.text, self.alignment
379
380 def xml(self):
381 s = '<value align="%s" format="%s"><string>%s</string></value>'
382 return s % (
383 align2xml[self.alignment],
384 self.fmt,
385 cgi.escape(self.text))
386
387class FormulaCell(BaseCell):
388
389 def __init__(self, formula, fmt="%s", alignment=RIGHT):
390 assert alignment in (LEFT, CENTER, RIGHT)
391 self.formula = formula
392 self.translated = translate(self.formula)
393 self.fmt = fmt
394 self.alignment = alignment
395 self.reset()
396
397 def reset(self):
398 self.value = None
399
Georg Brandla52bae72010-08-21 23:20:01 +0000400 def recalc(self, ns):
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000401 if self.value is None:
402 try:
Guido van Rossum36692422002-11-02 06:25:51 +0000403 # A hack to evaluate expressions using true division
Georg Brandla52bae72010-08-21 23:20:01 +0000404 self.value = eval(self.translated, ns)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000405 except:
406 exc = sys.exc_info()[0]
407 if hasattr(exc, "__name__"):
408 self.value = exc.__name__
409 else:
410 self.value = str(exc)
411 return self.value
412
413 def format(self):
414 try:
415 text = self.fmt % self.value
416 except:
417 text = str(self.value)
418 return text, self.alignment
419
420 def xml(self):
421 return '<formula align="%s" format="%s">%s</formula>' % (
422 align2xml[self.alignment],
423 self.fmt,
424 self.formula)
425
426 def renumber(self, x1, y1, x2, y2, dx, dy):
427 out = []
428 for part in re.split('(\w+)', self.formula):
429 m = re.match('^([A-Z]+)([1-9][0-9]*)$', part)
430 if m is not None:
431 sx, sy = m.groups()
432 x = colname2num(sx)
433 y = int(sy)
434 if x1 <= x <= x2 and y1 <= y <= y2:
435 part = cellname(x+dx, y+dy)
436 out.append(part)
437 return FormulaCell("".join(out), self.fmt, self.alignment)
438
439def translate(formula):
440 """Translate a formula containing fancy cell names to valid Python code.
441
442 Examples:
443 B4 -> cell(2, 4)
444 B4:Z100 -> cells(2, 4, 26, 100)
445 """
446 out = []
447 for part in re.split(r"(\w+(?::\w+)?)", formula):
448 m = re.match(r"^([A-Z]+)([1-9][0-9]*)(?::([A-Z]+)([1-9][0-9]*))?$", part)
449 if m is None:
450 out.append(part)
451 else:
452 x1, y1, x2, y2 = m.groups()
453 x1 = colname2num(x1)
454 if x2 is None:
455 s = "cell(%s, %s)" % (x1, y1)
456 else:
457 x2 = colname2num(x2)
458 s = "cells(%s, %s, %s, %s)" % (x1, y1, x2, y2)
459 out.append(s)
460 return "".join(out)
461
462def cellname(x, y):
463 "Translate a cell coordinate to a fancy cell name (e.g. (1, 1)->'A1')."
464 assert x > 0 # Column 0 has an empty name, so can't use that
465 return colnum2name(x) + str(y)
466
467def colname2num(s):
468 "Translate a column name to number (e.g. 'A'->1, 'Z'->26, 'AA'->27)."
469 s = s.upper()
470 n = 0
471 for c in s:
472 assert 'A' <= c <= 'Z'
473 n = n*26 + ord(c) - ord('A') + 1
474 return n
475
476def colnum2name(n):
477 "Translate a column number to name (e.g. 1->'A', etc.)."
478 assert n > 0
479 s = ""
480 while n:
481 n, m = divmod(n-1, 26)
482 s = chr(m+ord('A')) + s
483 return s
484
Benjamin Petersond6d63f52009-01-04 18:53:28 +0000485import tkinter as Tk
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000486
487class SheetGUI:
488
489 """Beginnings of a GUI for a spreadsheet.
490
491 TO DO:
492 - clear multiple cells
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000493 - Insert, clear, remove rows or columns
494 - Show new contents while typing
495 - Scroll bars
496 - Grow grid when window is grown
497 - Proper menus
498 - Undo, redo
499 - Cut, copy and paste
500 - Formatting and alignment
501 """
502
503 def __init__(self, filename="sheet1.xml", rows=10, columns=5):
504 """Constructor.
505
506 Load the sheet from the filename argument.
507 Set up the Tk widget tree.
508 """
509 # Create and load the sheet
510 self.filename = filename
511 self.sheet = Sheet()
512 if os.path.isfile(filename):
513 self.sheet.load(filename)
514 # Calculate the needed grid size
515 maxx, maxy = self.sheet.getsize()
516 rows = max(rows, maxy)
517 columns = max(columns, maxx)
518 # Create the widgets
519 self.root = Tk.Tk()
520 self.root.wm_title("Spreadsheet: %s" % self.filename)
521 self.beacon = Tk.Label(self.root, text="A1",
522 font=('helvetica', 16, 'bold'))
523 self.entry = Tk.Entry(self.root)
524 self.savebutton = Tk.Button(self.root, text="Save",
525 command=self.save)
526 self.cellgrid = Tk.Frame(self.root)
527 # Configure the widget lay-out
528 self.cellgrid.pack(side="bottom", expand=1, fill="both")
529 self.beacon.pack(side="left")
530 self.savebutton.pack(side="right")
531 self.entry.pack(side="left", expand=1, fill="x")
532 # Bind some events
533 self.entry.bind("<Return>", self.return_event)
534 self.entry.bind("<Shift-Return>", self.shift_return_event)
535 self.entry.bind("<Tab>", self.tab_event)
536 self.entry.bind("<Shift-Tab>", self.shift_tab_event)
Guido van Rossum36692422002-11-02 06:25:51 +0000537 self.entry.bind("<Delete>", self.delete_event)
Guido van Rossumbfcd6532002-11-02 06:50:05 +0000538 self.entry.bind("<Escape>", self.escape_event)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000539 # Now create the cell grid
540 self.makegrid(rows, columns)
541 # Select the top-left cell
542 self.currentxy = None
543 self.cornerxy = None
544 self.setcurrent(1, 1)
545 # Copy the sheet cells to the GUI cells
546 self.sync()
547
Guido van Rossum36692422002-11-02 06:25:51 +0000548 def delete_event(self, event):
549 if self.cornerxy != self.currentxy and self.cornerxy is not None:
550 self.sheet.clearcells(*(self.currentxy + self.cornerxy))
551 else:
552 self.sheet.clearcell(*self.currentxy)
553 self.sync()
554 self.entry.delete(0, 'end')
555 return "break"
556
Guido van Rossumbfcd6532002-11-02 06:50:05 +0000557 def escape_event(self, event):
558 x, y = self.currentxy
559 self.load_entry(x, y)
560
561 def load_entry(self, x, y):
562 cell = self.sheet.getcell(x, y)
563 if cell is None:
564 text = ""
565 elif isinstance(cell, FormulaCell):
566 text = '=' + cell.formula
567 else:
568 text, alignment = cell.format()
569 self.entry.delete(0, 'end')
570 self.entry.insert(0, text)
571 self.entry.selection_range(0, 'end')
572
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000573 def makegrid(self, rows, columns):
574 """Helper to create the grid of GUI cells.
575
576 The edge (x==0 or y==0) is filled with labels; the rest is real cells.
577 """
Guido van Rossum36692422002-11-02 06:25:51 +0000578 self.rows = rows
579 self.columns = columns
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000580 self.gridcells = {}
Guido van Rossum065627e2002-11-02 22:18:46 +0000581 # Create the top left corner cell (which selects all)
582 cell = Tk.Label(self.cellgrid, relief='raised')
583 cell.grid_configure(column=0, row=0, sticky='NSWE')
584 cell.bind("<ButtonPress-1>", self.selectall)
585 # Create the top row of labels, and confiure the grid columns
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000586 for x in range(1, columns+1):
587 self.cellgrid.grid_columnconfigure(x, minsize=64)
588 cell = Tk.Label(self.cellgrid, text=colnum2name(x), relief='raised')
589 cell.grid_configure(column=x, row=0, sticky='WE')
590 self.gridcells[x, 0] = cell
Guido van Rossum36692422002-11-02 06:25:51 +0000591 cell.__x = x
592 cell.__y = 0
593 cell.bind("<ButtonPress-1>", self.selectcolumn)
594 cell.bind("<B1-Motion>", self.extendcolumn)
595 cell.bind("<ButtonRelease-1>", self.extendcolumn)
596 cell.bind("<Shift-Button-1>", self.extendcolumn)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000597 # Create the leftmost column of labels
598 for y in range(1, rows+1):
599 cell = Tk.Label(self.cellgrid, text=str(y), relief='raised')
600 cell.grid_configure(column=0, row=y, sticky='WE')
601 self.gridcells[0, y] = cell
Guido van Rossum36692422002-11-02 06:25:51 +0000602 cell.__x = 0
603 cell.__y = y
604 cell.bind("<ButtonPress-1>", self.selectrow)
605 cell.bind("<B1-Motion>", self.extendrow)
606 cell.bind("<ButtonRelease-1>", self.extendrow)
607 cell.bind("<Shift-Button-1>", self.extendrow)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000608 # Create the real cells
609 for x in range(1, columns+1):
610 for y in range(1, rows+1):
611 cell = Tk.Label(self.cellgrid, relief='sunken',
612 bg='white', fg='black')
Guido van Rossum065627e2002-11-02 22:18:46 +0000613 cell.grid_configure(column=x, row=y, sticky='NSWE')
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000614 self.gridcells[x, y] = cell
Guido van Rossum36692422002-11-02 06:25:51 +0000615 cell.__x = x
616 cell.__y = y
617 # Bind mouse events
618 cell.bind("<ButtonPress-1>", self.press)
619 cell.bind("<B1-Motion>", self.motion)
620 cell.bind("<ButtonRelease-1>", self.release)
621 cell.bind("<Shift-Button-1>", self.release)
622
Guido van Rossum065627e2002-11-02 22:18:46 +0000623 def selectall(self, event):
624 self.setcurrent(1, 1)
625 self.setcorner(sys.maxint, sys.maxint)
626
Guido van Rossum36692422002-11-02 06:25:51 +0000627 def selectcolumn(self, event):
628 x, y = self.whichxy(event)
629 self.setcurrent(x, 1)
Guido van Rossum065627e2002-11-02 22:18:46 +0000630 self.setcorner(x, sys.maxint)
Guido van Rossum36692422002-11-02 06:25:51 +0000631
632 def extendcolumn(self, event):
633 x, y = self.whichxy(event)
634 if x > 0:
635 self.setcurrent(self.currentxy[0], 1)
Guido van Rossum065627e2002-11-02 22:18:46 +0000636 self.setcorner(x, sys.maxint)
Guido van Rossum36692422002-11-02 06:25:51 +0000637
638 def selectrow(self, event):
639 x, y = self.whichxy(event)
640 self.setcurrent(1, y)
Guido van Rossum065627e2002-11-02 22:18:46 +0000641 self.setcorner(sys.maxint, y)
Guido van Rossum36692422002-11-02 06:25:51 +0000642
643 def extendrow(self, event):
644 x, y = self.whichxy(event)
645 if y > 0:
646 self.setcurrent(1, self.currentxy[1])
Guido van Rossum065627e2002-11-02 22:18:46 +0000647 self.setcorner(sys.maxint, y)
Guido van Rossum36692422002-11-02 06:25:51 +0000648
649 def press(self, event):
650 x, y = self.whichxy(event)
651 if x > 0 and y > 0:
652 self.setcurrent(x, y)
653
654 def motion(self, event):
655 x, y = self.whichxy(event)
656 if x > 0 and y > 0:
657 self.setcorner(x, y)
658
659 release = motion
660
661 def whichxy(self, event):
662 w = self.cellgrid.winfo_containing(event.x_root, event.y_root)
663 if w is not None and isinstance(w, Tk.Label):
664 try:
665 return w.__x, w.__y
666 except AttributeError:
667 pass
668 return 0, 0
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000669
670 def save(self):
671 self.sheet.save(self.filename)
672
673 def setcurrent(self, x, y):
674 "Make (x, y) the current cell."
675 if self.currentxy is not None:
676 self.change_cell()
677 self.clearfocus()
Guido van Rossumbfcd6532002-11-02 06:50:05 +0000678 self.beacon['text'] = cellname(x, y)
679 self.load_entry(x, y)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000680 self.entry.focus_set()
681 self.currentxy = x, y
682 self.cornerxy = None
683 gridcell = self.gridcells.get(self.currentxy)
684 if gridcell is not None:
Guido van Rossum36692422002-11-02 06:25:51 +0000685 gridcell['bg'] = 'yellow'
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000686
687 def setcorner(self, x, y):
688 if self.currentxy is None or self.currentxy == (x, y):
689 self.setcurrent(x, y)
690 return
691 self.clearfocus()
692 self.cornerxy = x, y
693 x1, y1 = self.currentxy
694 x2, y2 = self.cornerxy or self.currentxy
695 if x1 > x2:
696 x1, x2 = x2, x1
697 if y1 > y2:
698 y1, y2 = y2, y1
Collin Winter6f2df4d2007-07-17 20:59:35 +0000699 for (x, y), cell in self.gridcells.items():
Guido van Rossum065627e2002-11-02 22:18:46 +0000700 if x1 <= x <= x2 and y1 <= y <= y2:
701 cell['bg'] = 'lightBlue'
Guido van Rossum36692422002-11-02 06:25:51 +0000702 gridcell = self.gridcells.get(self.currentxy)
703 if gridcell is not None:
704 gridcell['bg'] = 'yellow'
Guido van Rossum065627e2002-11-02 22:18:46 +0000705 self.setbeacon(x1, y1, x2, y2)
706
707 def setbeacon(self, x1, y1, x2, y2):
708 if x1 == y1 == 1 and x2 == y2 == sys.maxint:
709 name = ":"
710 elif (x1, x2) == (1, sys.maxint):
711 if y1 == y2:
712 name = "%d" % y1
713 else:
714 name = "%d:%d" % (y1, y2)
715 elif (y1, y2) == (1, sys.maxint):
716 if x1 == x2:
717 name = "%s" % colnum2name(x1)
718 else:
719 name = "%s:%s" % (colnum2name(x1), colnum2name(x2))
720 else:
721 name1 = cellname(*self.currentxy)
722 name2 = cellname(*self.cornerxy)
723 name = "%s:%s" % (name1, name2)
724 self.beacon['text'] = name
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000725
726
727 def clearfocus(self):
728 if self.currentxy is not None:
729 x1, y1 = self.currentxy
730 x2, y2 = self.cornerxy or self.currentxy
731 if x1 > x2:
732 x1, x2 = x2, x1
733 if y1 > y2:
734 y1, y2 = y2, y1
Collin Winter6f2df4d2007-07-17 20:59:35 +0000735 for (x, y), cell in self.gridcells.items():
Guido van Rossum065627e2002-11-02 22:18:46 +0000736 if x1 <= x <= x2 and y1 <= y <= y2:
737 cell['bg'] = 'white'
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000738
739 def return_event(self, event):
740 "Callback for the Return key."
741 self.change_cell()
742 x, y = self.currentxy
743 self.setcurrent(x, y+1)
744 return "break"
745
746 def shift_return_event(self, event):
747 "Callback for the Return key with Shift modifier."
748 self.change_cell()
749 x, y = self.currentxy
750 self.setcurrent(x, max(1, y-1))
751 return "break"
752
753 def tab_event(self, event):
754 "Callback for the Tab key."
755 self.change_cell()
756 x, y = self.currentxy
757 self.setcurrent(x+1, y)
758 return "break"
759
760 def shift_tab_event(self, event):
761 "Callback for the Tab key with Shift modifier."
762 self.change_cell()
763 x, y = self.currentxy
764 self.setcurrent(max(1, x-1), y)
765 return "break"
766
767 def change_cell(self):
768 "Set the current cell from the entry widget."
769 x, y = self.currentxy
770 text = self.entry.get()
771 cell = None
772 if text.startswith('='):
773 cell = FormulaCell(text[1:])
774 else:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000775 for cls in int, int, float, complex:
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000776 try:
777 value = cls(text)
778 except:
779 continue
780 else:
781 cell = NumericCell(value)
782 break
783 if cell is None and text:
784 cell = StringCell(text)
785 if cell is None:
786 self.sheet.clearcell(x, y)
787 else:
788 self.sheet.setcell(x, y, cell)
789 self.sync()
790
791 def sync(self):
792 "Fill the GUI cells from the sheet cells."
793 self.sheet.recalc()
Collin Winter6f2df4d2007-07-17 20:59:35 +0000794 for (x, y), gridcell in self.gridcells.items():
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000795 if x == 0 or y == 0:
796 continue
797 cell = self.sheet.getcell(x, y)
798 if cell is None:
799 gridcell['text'] = ""
800 else:
801 if hasattr(cell, 'format'):
802 text, alignment = cell.format()
803 else:
804 text, alignment = str(cell), LEFT
805 gridcell['text'] = text
806 gridcell['anchor'] = align2anchor[alignment]
807
808
809def test_basic():
810 "Basic non-gui self-test."
811 import os
812 a = Sheet()
813 for x in range(1, 11):
814 for y in range(1, 11):
815 if x == 1:
816 cell = NumericCell(y)
817 elif y == 1:
818 cell = NumericCell(x)
819 else:
820 c1 = cellname(x, 1)
821 c2 = cellname(1, y)
822 formula = "%s*%s" % (c1, c2)
823 cell = FormulaCell(formula)
824 a.setcell(x, y, cell)
825## if os.path.isfile("sheet1.xml"):
826## print "Loading from sheet1.xml"
827## a.load("sheet1.xml")
828 a.display()
829 a.save("sheet1.xml")
830
831def test_gui():
832 "GUI test."
Guido van Rossum36692422002-11-02 06:25:51 +0000833 if sys.argv[1:]:
834 filename = sys.argv[1]
835 else:
836 filename = "sheet1.xml"
837 g = SheetGUI(filename)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000838 g.root.mainloop()
839
840if __name__ == '__main__':
841 #test_basic()
842 test_gui()