blob: 4cea66717100010fc267748b3898d44054071782 [file] [log] [blame]
Georg Brandl856898b2010-12-30 22:11:50 +00001#!/usr/bin/env python3
2
3"""
4SS1 -- a spreadsheet-like application.
5"""
Guido van Rossum69ccfcc2002-10-28 01:06:37 +00006
7import os
8import re
9import sys
Georg Brandlf19ff1e2010-10-26 10:39:14 +000010import html
Guido van Rossum69ccfcc2002-10-28 01:06:37 +000011from xml.parsers import expat
12
13LEFT, CENTER, RIGHT = "LEFT", "CENTER", "RIGHT"
14
15def ljust(x, n):
16 return x.ljust(n)
17def center(x, n):
18 return x.center(n)
19def rjust(x, n):
20 return x.rjust(n)
21align2action = {LEFT: ljust, CENTER: center, RIGHT: rjust}
22
23align2xml = {LEFT: "left", CENTER: "center", RIGHT: "right"}
24xml2align = {"left": LEFT, "center": CENTER, "right": RIGHT}
25
26align2anchor = {LEFT: "w", CENTER: "center", RIGHT: "e"}
27
28def sum(seq):
29 total = 0
30 for x in seq:
31 if x is not None:
32 total += x
33 return total
34
35class Sheet:
36
37 def __init__(self):
38 self.cells = {} # {(x, y): cell, ...}
Georg Brandla52bae72010-08-21 23:20:01 +000039 self.ns = dict(
40 cell = self.cellvalue,
41 cells = self.multicellvalue,
42 sum = sum,
43 )
Guido van Rossum69ccfcc2002-10-28 01:06:37 +000044
45 def cellvalue(self, x, y):
46 cell = self.getcell(x, y)
47 if hasattr(cell, 'recalc'):
Georg Brandla52bae72010-08-21 23:20:01 +000048 return cell.recalc(self.ns)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +000049 else:
50 return cell
51
52 def multicellvalue(self, x1, y1, x2, y2):
53 if x1 > x2:
54 x1, x2 = x2, x1
55 if y1 > y2:
56 y1, y2 = y2, y1
57 seq = []
58 for y in range(y1, y2+1):
59 for x in range(x1, x2+1):
60 seq.append(self.cellvalue(x, y))
61 return seq
62
63 def getcell(self, x, y):
64 return self.cells.get((x, y))
65
66 def setcell(self, x, y, cell):
67 assert x > 0 and y > 0
68 assert isinstance(cell, BaseCell)
69 self.cells[x, y] = cell
70
71 def clearcell(self, x, y):
72 try:
73 del self.cells[x, y]
74 except KeyError:
75 pass
76
77 def clearcells(self, x1, y1, x2, y2):
78 for xy in self.selectcells(x1, y1, x2, y2):
79 del self.cells[xy]
80
81 def clearrows(self, y1, y2):
82 self.clearcells(0, y1, sys.maxint, y2)
83
84 def clearcolumns(self, x1, x2):
85 self.clearcells(x1, 0, x2, sys.maxint)
86
87 def selectcells(self, x1, y1, x2, y2):
88 if x1 > x2:
89 x1, x2 = x2, x1
90 if y1 > y2:
91 y1, y2 = y2, y1
92 return [(x, y) for x, y in self.cells
93 if x1 <= x <= x2 and y1 <= y <= y2]
94
95 def movecells(self, x1, y1, x2, y2, dx, dy):
96 if dx == 0 and dy == 0:
97 return
98 if x1 > x2:
99 x1, x2 = x2, x1
100 if y1 > y2:
101 y1, y2 = y2, y1
102 assert x1+dx > 0 and y1+dy > 0
103 new = {}
104 for x, y in self.cells:
105 cell = self.cells[x, y]
106 if hasattr(cell, 'renumber'):
107 cell = cell.renumber(x1, y1, x2, y2, dx, dy)
108 if x1 <= x <= x2 and y1 <= y <= y2:
109 x += dx
110 y += dy
111 new[x, y] = cell
112 self.cells = new
113
114 def insertrows(self, y, n):
115 assert n > 0
116 self.movecells(0, y, sys.maxint, sys.maxint, 0, n)
117
118 def deleterows(self, y1, y2):
119 if y1 > y2:
120 y1, y2 = y2, y1
121 self.clearrows(y1, y2)
122 self.movecells(0, y2+1, sys.maxint, sys.maxint, 0, y1-y2-1)
123
124 def insertcolumns(self, x, n):
125 assert n > 0
126 self.movecells(x, 0, sys.maxint, sys.maxint, n, 0)
127
128 def deletecolumns(self, x1, x2):
129 if x1 > x2:
130 x1, x2 = x2, x1
131 self.clearcells(x1, x2)
132 self.movecells(x2+1, 0, sys.maxint, sys.maxint, x1-x2-1, 0)
133
134 def getsize(self):
135 maxx = maxy = 0
136 for x, y in self.cells:
137 maxx = max(maxx, x)
138 maxy = max(maxy, y)
139 return maxx, maxy
140
141 def reset(self):
Collin Winter6f2df4d2007-07-17 20:59:35 +0000142 for cell in self.cells.values():
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000143 if hasattr(cell, 'reset'):
144 cell.reset()
145
146 def recalc(self):
147 self.reset()
Collin Winter6f2df4d2007-07-17 20:59:35 +0000148 for cell in self.cells.values():
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000149 if hasattr(cell, 'recalc'):
Georg Brandla52bae72010-08-21 23:20:01 +0000150 cell.recalc(self.ns)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000151
152 def display(self):
153 maxx, maxy = self.getsize()
154 width, height = maxx+1, maxy+1
155 colwidth = [1] * width
156 full = {}
157 # Add column heading labels in row 0
158 for x in range(1, width):
159 full[x, 0] = text, alignment = colnum2name(x), RIGHT
160 colwidth[x] = max(colwidth[x], len(text))
161 # Add row labels in column 0
162 for y in range(1, height):
163 full[0, y] = text, alignment = str(y), RIGHT
164 colwidth[0] = max(colwidth[0], len(text))
165 # Add sheet cells in columns with x>0 and y>0
Collin Winter6f2df4d2007-07-17 20:59:35 +0000166 for (x, y), cell in self.cells.items():
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000167 if x <= 0 or y <= 0:
168 continue
169 if hasattr(cell, 'recalc'):
Georg Brandla52bae72010-08-21 23:20:01 +0000170 cell.recalc(self.ns)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000171 if hasattr(cell, 'format'):
172 text, alignment = cell.format()
173 assert isinstance(text, str)
174 assert alignment in (LEFT, CENTER, RIGHT)
175 else:
176 text = str(cell)
177 if isinstance(cell, str):
178 alignment = LEFT
179 else:
180 alignment = RIGHT
181 full[x, y] = (text, alignment)
182 colwidth[x] = max(colwidth[x], len(text))
183 # Calculate the horizontal separator line (dashes and dots)
184 sep = ""
185 for x in range(width):
186 if sep:
187 sep += "+"
188 sep += "-"*colwidth[x]
189 # Now print The full grid
190 for y in range(height):
191 line = ""
192 for x in range(width):
193 text, alignment = full.get((x, y)) or ("", LEFT)
194 text = align2action[alignment](text, colwidth[x])
195 if line:
196 line += '|'
197 line += text
Collin Winter6f2df4d2007-07-17 20:59:35 +0000198 print(line)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000199 if y == 0:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000200 print(sep)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000201
202 def xml(self):
203 out = ['<spreadsheet>']
Collin Winter6f2df4d2007-07-17 20:59:35 +0000204 for (x, y), cell in self.cells.items():
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000205 if hasattr(cell, 'xml'):
206 cellxml = cell.xml()
207 else:
Georg Brandlf19ff1e2010-10-26 10:39:14 +0000208 cellxml = '<value>%s</value>' % html.escape(cell)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000209 out.append('<cell row="%s" col="%s">\n %s\n</cell>' %
210 (y, x, cellxml))
211 out.append('</spreadsheet>')
212 return '\n'.join(out)
213
214 def save(self, filename):
215 text = self.xml()
216 f = open(filename, "w")
217 f.write(text)
218 if text and not text.endswith('\n'):
219 f.write('\n')
220 f.close()
221
222 def load(self, filename):
Georg Brandlf19ff1e2010-10-26 10:39:14 +0000223 f = open(filename, 'rb')
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000224 SheetParser(self).parsefile(f)
225 f.close()
226
227class SheetParser:
228
229 def __init__(self, sheet):
230 self.sheet = sheet
231
232 def parsefile(self, f):
233 parser = expat.ParserCreate()
234 parser.StartElementHandler = self.startelement
235 parser.EndElementHandler = self.endelement
236 parser.CharacterDataHandler = self.data
237 parser.ParseFile(f)
238
239 def startelement(self, tag, attrs):
240 method = getattr(self, 'start_'+tag, None)
241 if method:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000242 for key, value in attrs.items():
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000243 attrs[key] = str(value) # XXX Convert Unicode to 8-bit
244 method(attrs)
245 self.texts = []
246
247 def data(self, text):
248 text = str(text) # XXX Convert Unicode to 8-bit
249 self.texts.append(text)
250
251 def endelement(self, tag):
252 method = getattr(self, 'end_'+tag, None)
253 if method:
254 method("".join(self.texts))
255
256 def start_cell(self, attrs):
257 self.y = int(attrs.get("row"))
258 self.x = int(attrs.get("col"))
259
260 def start_value(self, attrs):
261 self.fmt = attrs.get('format')
262 self.alignment = xml2align.get(attrs.get('align'))
263
264 start_formula = start_value
265
266 def end_int(self, text):
267 try:
268 self.value = int(text)
269 except:
270 self.value = None
271
272 def end_long(self, text):
273 try:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000274 self.value = int(text)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000275 except:
276 self.value = None
277
278 def end_double(self, text):
279 try:
280 self.value = float(text)
281 except:
282 self.value = None
283
284 def end_complex(self, text):
285 try:
286 self.value = complex(text)
287 except:
288 self.value = None
289
290 def end_string(self, text):
291 try:
292 self.value = text
293 except:
294 self.value = None
295
296 def end_value(self, text):
297 if isinstance(self.value, BaseCell):
298 self.cell = self.value
299 elif isinstance(self.value, str):
300 self.cell = StringCell(self.value,
301 self.fmt or "%s",
302 self.alignment or LEFT)
303 else:
304 self.cell = NumericCell(self.value,
305 self.fmt or "%s",
306 self.alignment or RIGHT)
307
308 def end_formula(self, text):
309 self.cell = FormulaCell(text,
310 self.fmt or "%s",
311 self.alignment or RIGHT)
312
313 def end_cell(self, text):
314 self.sheet.setcell(self.x, self.y, self.cell)
315
316class BaseCell:
317 __init__ = None # Must provide
318 """Abstract base class for sheet cells.
319
320 Subclasses may but needn't provide the following APIs:
Tim Peters182b5ac2004-07-18 06:16:08 +0000321
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000322 cell.reset() -- prepare for recalculation
Georg Brandla52bae72010-08-21 23:20:01 +0000323 cell.recalc(ns) -> value -- recalculate formula
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000324 cell.format() -> (value, alignment) -- return formatted value
325 cell.xml() -> string -- return XML
326 """
327
328class NumericCell(BaseCell):
329
330 def __init__(self, value, fmt="%s", alignment=RIGHT):
Collin Winter6f2df4d2007-07-17 20:59:35 +0000331 assert isinstance(value, (int, int, float, complex))
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000332 assert alignment in (LEFT, CENTER, RIGHT)
333 self.value = value
334 self.fmt = fmt
335 self.alignment = alignment
336
Georg Brandla52bae72010-08-21 23:20:01 +0000337 def recalc(self, ns):
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000338 return self.value
339
340 def format(self):
341 try:
342 text = self.fmt % self.value
343 except:
344 text = str(self.value)
345 return text, self.alignment
346
347 def xml(self):
348 method = getattr(self, '_xml_' + type(self.value).__name__)
349 return '<value align="%s" format="%s">%s</value>' % (
350 align2xml[self.alignment],
351 self.fmt,
352 method())
353
354 def _xml_int(self):
355 if -2**31 <= self.value < 2**31:
356 return '<int>%s</int>' % self.value
357 else:
358 return self._xml_long()
359
360 def _xml_long(self):
361 return '<long>%s</long>' % self.value
362
363 def _xml_float(self):
364 return '<double>%s</double>' % repr(self.value)
365
366 def _xml_complex(self):
367 return '<complex>%s</double>' % repr(self.value)
368
369class StringCell(BaseCell):
370
371 def __init__(self, text, fmt="%s", alignment=LEFT):
Collin Winter6f2df4d2007-07-17 20:59:35 +0000372 assert isinstance(text, (str, str))
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000373 assert alignment in (LEFT, CENTER, RIGHT)
374 self.text = text
375 self.fmt = fmt
376 self.alignment = alignment
377
Georg Brandla52bae72010-08-21 23:20:01 +0000378 def recalc(self, ns):
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000379 return self.text
380
381 def format(self):
382 return self.text, self.alignment
383
384 def xml(self):
385 s = '<value align="%s" format="%s"><string>%s</string></value>'
386 return s % (
387 align2xml[self.alignment],
388 self.fmt,
Georg Brandlf19ff1e2010-10-26 10:39:14 +0000389 html.escape(self.text))
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000390
391class FormulaCell(BaseCell):
392
393 def __init__(self, formula, fmt="%s", alignment=RIGHT):
394 assert alignment in (LEFT, CENTER, RIGHT)
395 self.formula = formula
396 self.translated = translate(self.formula)
397 self.fmt = fmt
398 self.alignment = alignment
399 self.reset()
400
401 def reset(self):
402 self.value = None
403
Georg Brandla52bae72010-08-21 23:20:01 +0000404 def recalc(self, ns):
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000405 if self.value is None:
406 try:
Guido van Rossum36692422002-11-02 06:25:51 +0000407 # A hack to evaluate expressions using true division
Georg Brandla52bae72010-08-21 23:20:01 +0000408 self.value = eval(self.translated, ns)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000409 except:
410 exc = sys.exc_info()[0]
411 if hasattr(exc, "__name__"):
412 self.value = exc.__name__
413 else:
414 self.value = str(exc)
415 return self.value
416
417 def format(self):
418 try:
419 text = self.fmt % self.value
420 except:
421 text = str(self.value)
422 return text, self.alignment
423
424 def xml(self):
425 return '<formula align="%s" format="%s">%s</formula>' % (
426 align2xml[self.alignment],
427 self.fmt,
428 self.formula)
429
430 def renumber(self, x1, y1, x2, y2, dx, dy):
431 out = []
432 for part in re.split('(\w+)', self.formula):
433 m = re.match('^([A-Z]+)([1-9][0-9]*)$', part)
434 if m is not None:
435 sx, sy = m.groups()
436 x = colname2num(sx)
437 y = int(sy)
438 if x1 <= x <= x2 and y1 <= y <= y2:
439 part = cellname(x+dx, y+dy)
440 out.append(part)
441 return FormulaCell("".join(out), self.fmt, self.alignment)
442
443def translate(formula):
444 """Translate a formula containing fancy cell names to valid Python code.
445
446 Examples:
447 B4 -> cell(2, 4)
448 B4:Z100 -> cells(2, 4, 26, 100)
449 """
450 out = []
451 for part in re.split(r"(\w+(?::\w+)?)", formula):
452 m = re.match(r"^([A-Z]+)([1-9][0-9]*)(?::([A-Z]+)([1-9][0-9]*))?$", part)
453 if m is None:
454 out.append(part)
455 else:
456 x1, y1, x2, y2 = m.groups()
457 x1 = colname2num(x1)
458 if x2 is None:
459 s = "cell(%s, %s)" % (x1, y1)
460 else:
461 x2 = colname2num(x2)
462 s = "cells(%s, %s, %s, %s)" % (x1, y1, x2, y2)
463 out.append(s)
464 return "".join(out)
465
466def cellname(x, y):
467 "Translate a cell coordinate to a fancy cell name (e.g. (1, 1)->'A1')."
468 assert x > 0 # Column 0 has an empty name, so can't use that
469 return colnum2name(x) + str(y)
470
471def colname2num(s):
472 "Translate a column name to number (e.g. 'A'->1, 'Z'->26, 'AA'->27)."
473 s = s.upper()
474 n = 0
475 for c in s:
476 assert 'A' <= c <= 'Z'
477 n = n*26 + ord(c) - ord('A') + 1
478 return n
479
480def colnum2name(n):
481 "Translate a column number to name (e.g. 1->'A', etc.)."
482 assert n > 0
483 s = ""
484 while n:
485 n, m = divmod(n-1, 26)
486 s = chr(m+ord('A')) + s
487 return s
488
Benjamin Petersond6d63f52009-01-04 18:53:28 +0000489import tkinter as Tk
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000490
491class SheetGUI:
492
493 """Beginnings of a GUI for a spreadsheet.
494
495 TO DO:
496 - clear multiple cells
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000497 - Insert, clear, remove rows or columns
498 - Show new contents while typing
499 - Scroll bars
500 - Grow grid when window is grown
501 - Proper menus
502 - Undo, redo
503 - Cut, copy and paste
504 - Formatting and alignment
505 """
506
507 def __init__(self, filename="sheet1.xml", rows=10, columns=5):
508 """Constructor.
509
510 Load the sheet from the filename argument.
511 Set up the Tk widget tree.
512 """
513 # Create and load the sheet
514 self.filename = filename
515 self.sheet = Sheet()
516 if os.path.isfile(filename):
517 self.sheet.load(filename)
518 # Calculate the needed grid size
519 maxx, maxy = self.sheet.getsize()
520 rows = max(rows, maxy)
521 columns = max(columns, maxx)
522 # Create the widgets
523 self.root = Tk.Tk()
524 self.root.wm_title("Spreadsheet: %s" % self.filename)
525 self.beacon = Tk.Label(self.root, text="A1",
526 font=('helvetica', 16, 'bold'))
527 self.entry = Tk.Entry(self.root)
528 self.savebutton = Tk.Button(self.root, text="Save",
529 command=self.save)
530 self.cellgrid = Tk.Frame(self.root)
531 # Configure the widget lay-out
532 self.cellgrid.pack(side="bottom", expand=1, fill="both")
533 self.beacon.pack(side="left")
534 self.savebutton.pack(side="right")
535 self.entry.pack(side="left", expand=1, fill="x")
536 # Bind some events
537 self.entry.bind("<Return>", self.return_event)
538 self.entry.bind("<Shift-Return>", self.shift_return_event)
539 self.entry.bind("<Tab>", self.tab_event)
540 self.entry.bind("<Shift-Tab>", self.shift_tab_event)
Guido van Rossum36692422002-11-02 06:25:51 +0000541 self.entry.bind("<Delete>", self.delete_event)
Guido van Rossumbfcd6532002-11-02 06:50:05 +0000542 self.entry.bind("<Escape>", self.escape_event)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000543 # Now create the cell grid
544 self.makegrid(rows, columns)
545 # Select the top-left cell
546 self.currentxy = None
547 self.cornerxy = None
548 self.setcurrent(1, 1)
549 # Copy the sheet cells to the GUI cells
550 self.sync()
551
Guido van Rossum36692422002-11-02 06:25:51 +0000552 def delete_event(self, event):
553 if self.cornerxy != self.currentxy and self.cornerxy is not None:
554 self.sheet.clearcells(*(self.currentxy + self.cornerxy))
555 else:
556 self.sheet.clearcell(*self.currentxy)
557 self.sync()
558 self.entry.delete(0, 'end')
559 return "break"
560
Guido van Rossumbfcd6532002-11-02 06:50:05 +0000561 def escape_event(self, event):
562 x, y = self.currentxy
563 self.load_entry(x, y)
564
565 def load_entry(self, x, y):
566 cell = self.sheet.getcell(x, y)
567 if cell is None:
568 text = ""
569 elif isinstance(cell, FormulaCell):
570 text = '=' + cell.formula
571 else:
572 text, alignment = cell.format()
573 self.entry.delete(0, 'end')
574 self.entry.insert(0, text)
575 self.entry.selection_range(0, 'end')
576
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000577 def makegrid(self, rows, columns):
578 """Helper to create the grid of GUI cells.
579
580 The edge (x==0 or y==0) is filled with labels; the rest is real cells.
581 """
Guido van Rossum36692422002-11-02 06:25:51 +0000582 self.rows = rows
583 self.columns = columns
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000584 self.gridcells = {}
Guido van Rossum065627e2002-11-02 22:18:46 +0000585 # Create the top left corner cell (which selects all)
586 cell = Tk.Label(self.cellgrid, relief='raised')
587 cell.grid_configure(column=0, row=0, sticky='NSWE')
588 cell.bind("<ButtonPress-1>", self.selectall)
Ezio Melotti4969f702011-03-15 05:59:46 +0200589 # Create the top row of labels, and configure the grid columns
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000590 for x in range(1, columns+1):
591 self.cellgrid.grid_columnconfigure(x, minsize=64)
592 cell = Tk.Label(self.cellgrid, text=colnum2name(x), relief='raised')
593 cell.grid_configure(column=x, row=0, sticky='WE')
594 self.gridcells[x, 0] = cell
Guido van Rossum36692422002-11-02 06:25:51 +0000595 cell.__x = x
596 cell.__y = 0
597 cell.bind("<ButtonPress-1>", self.selectcolumn)
598 cell.bind("<B1-Motion>", self.extendcolumn)
599 cell.bind("<ButtonRelease-1>", self.extendcolumn)
600 cell.bind("<Shift-Button-1>", self.extendcolumn)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000601 # Create the leftmost column of labels
602 for y in range(1, rows+1):
603 cell = Tk.Label(self.cellgrid, text=str(y), relief='raised')
604 cell.grid_configure(column=0, row=y, sticky='WE')
605 self.gridcells[0, y] = cell
Guido van Rossum36692422002-11-02 06:25:51 +0000606 cell.__x = 0
607 cell.__y = y
608 cell.bind("<ButtonPress-1>", self.selectrow)
609 cell.bind("<B1-Motion>", self.extendrow)
610 cell.bind("<ButtonRelease-1>", self.extendrow)
611 cell.bind("<Shift-Button-1>", self.extendrow)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000612 # Create the real cells
613 for x in range(1, columns+1):
614 for y in range(1, rows+1):
615 cell = Tk.Label(self.cellgrid, relief='sunken',
616 bg='white', fg='black')
Guido van Rossum065627e2002-11-02 22:18:46 +0000617 cell.grid_configure(column=x, row=y, sticky='NSWE')
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000618 self.gridcells[x, y] = cell
Guido van Rossum36692422002-11-02 06:25:51 +0000619 cell.__x = x
620 cell.__y = y
621 # Bind mouse events
622 cell.bind("<ButtonPress-1>", self.press)
623 cell.bind("<B1-Motion>", self.motion)
624 cell.bind("<ButtonRelease-1>", self.release)
625 cell.bind("<Shift-Button-1>", self.release)
626
Guido van Rossum065627e2002-11-02 22:18:46 +0000627 def selectall(self, event):
628 self.setcurrent(1, 1)
629 self.setcorner(sys.maxint, sys.maxint)
630
Guido van Rossum36692422002-11-02 06:25:51 +0000631 def selectcolumn(self, event):
632 x, y = self.whichxy(event)
633 self.setcurrent(x, 1)
Guido van Rossum065627e2002-11-02 22:18:46 +0000634 self.setcorner(x, sys.maxint)
Guido van Rossum36692422002-11-02 06:25:51 +0000635
636 def extendcolumn(self, event):
637 x, y = self.whichxy(event)
638 if x > 0:
639 self.setcurrent(self.currentxy[0], 1)
Guido van Rossum065627e2002-11-02 22:18:46 +0000640 self.setcorner(x, sys.maxint)
Guido van Rossum36692422002-11-02 06:25:51 +0000641
642 def selectrow(self, event):
643 x, y = self.whichxy(event)
644 self.setcurrent(1, y)
Guido van Rossum065627e2002-11-02 22:18:46 +0000645 self.setcorner(sys.maxint, y)
Guido van Rossum36692422002-11-02 06:25:51 +0000646
647 def extendrow(self, event):
648 x, y = self.whichxy(event)
649 if y > 0:
650 self.setcurrent(1, self.currentxy[1])
Guido van Rossum065627e2002-11-02 22:18:46 +0000651 self.setcorner(sys.maxint, y)
Guido van Rossum36692422002-11-02 06:25:51 +0000652
653 def press(self, event):
654 x, y = self.whichxy(event)
655 if x > 0 and y > 0:
656 self.setcurrent(x, y)
657
658 def motion(self, event):
659 x, y = self.whichxy(event)
660 if x > 0 and y > 0:
661 self.setcorner(x, y)
662
663 release = motion
664
665 def whichxy(self, event):
666 w = self.cellgrid.winfo_containing(event.x_root, event.y_root)
667 if w is not None and isinstance(w, Tk.Label):
668 try:
669 return w.__x, w.__y
670 except AttributeError:
671 pass
672 return 0, 0
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000673
674 def save(self):
675 self.sheet.save(self.filename)
676
677 def setcurrent(self, x, y):
678 "Make (x, y) the current cell."
679 if self.currentxy is not None:
680 self.change_cell()
681 self.clearfocus()
Guido van Rossumbfcd6532002-11-02 06:50:05 +0000682 self.beacon['text'] = cellname(x, y)
683 self.load_entry(x, y)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000684 self.entry.focus_set()
685 self.currentxy = x, y
686 self.cornerxy = None
687 gridcell = self.gridcells.get(self.currentxy)
688 if gridcell is not None:
Guido van Rossum36692422002-11-02 06:25:51 +0000689 gridcell['bg'] = 'yellow'
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000690
691 def setcorner(self, x, y):
692 if self.currentxy is None or self.currentxy == (x, y):
693 self.setcurrent(x, y)
694 return
695 self.clearfocus()
696 self.cornerxy = x, y
697 x1, y1 = self.currentxy
698 x2, y2 = self.cornerxy or self.currentxy
699 if x1 > x2:
700 x1, x2 = x2, x1
701 if y1 > y2:
702 y1, y2 = y2, y1
Collin Winter6f2df4d2007-07-17 20:59:35 +0000703 for (x, y), cell in self.gridcells.items():
Guido van Rossum065627e2002-11-02 22:18:46 +0000704 if x1 <= x <= x2 and y1 <= y <= y2:
705 cell['bg'] = 'lightBlue'
Guido van Rossum36692422002-11-02 06:25:51 +0000706 gridcell = self.gridcells.get(self.currentxy)
707 if gridcell is not None:
708 gridcell['bg'] = 'yellow'
Guido van Rossum065627e2002-11-02 22:18:46 +0000709 self.setbeacon(x1, y1, x2, y2)
710
711 def setbeacon(self, x1, y1, x2, y2):
712 if x1 == y1 == 1 and x2 == y2 == sys.maxint:
713 name = ":"
714 elif (x1, x2) == (1, sys.maxint):
715 if y1 == y2:
716 name = "%d" % y1
717 else:
718 name = "%d:%d" % (y1, y2)
719 elif (y1, y2) == (1, sys.maxint):
720 if x1 == x2:
721 name = "%s" % colnum2name(x1)
722 else:
723 name = "%s:%s" % (colnum2name(x1), colnum2name(x2))
724 else:
725 name1 = cellname(*self.currentxy)
726 name2 = cellname(*self.cornerxy)
727 name = "%s:%s" % (name1, name2)
728 self.beacon['text'] = name
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000729
730
731 def clearfocus(self):
732 if self.currentxy is not None:
733 x1, y1 = self.currentxy
734 x2, y2 = self.cornerxy or self.currentxy
735 if x1 > x2:
736 x1, x2 = x2, x1
737 if y1 > y2:
738 y1, y2 = y2, y1
Collin Winter6f2df4d2007-07-17 20:59:35 +0000739 for (x, y), cell in self.gridcells.items():
Guido van Rossum065627e2002-11-02 22:18:46 +0000740 if x1 <= x <= x2 and y1 <= y <= y2:
741 cell['bg'] = 'white'
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000742
743 def return_event(self, event):
744 "Callback for the Return key."
745 self.change_cell()
746 x, y = self.currentxy
747 self.setcurrent(x, y+1)
748 return "break"
749
750 def shift_return_event(self, event):
751 "Callback for the Return key with Shift modifier."
752 self.change_cell()
753 x, y = self.currentxy
754 self.setcurrent(x, max(1, y-1))
755 return "break"
756
757 def tab_event(self, event):
758 "Callback for the Tab key."
759 self.change_cell()
760 x, y = self.currentxy
761 self.setcurrent(x+1, y)
762 return "break"
763
764 def shift_tab_event(self, event):
765 "Callback for the Tab key with Shift modifier."
766 self.change_cell()
767 x, y = self.currentxy
768 self.setcurrent(max(1, x-1), y)
769 return "break"
770
771 def change_cell(self):
772 "Set the current cell from the entry widget."
773 x, y = self.currentxy
774 text = self.entry.get()
775 cell = None
776 if text.startswith('='):
777 cell = FormulaCell(text[1:])
778 else:
Collin Winter6f2df4d2007-07-17 20:59:35 +0000779 for cls in int, int, float, complex:
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000780 try:
781 value = cls(text)
782 except:
783 continue
784 else:
785 cell = NumericCell(value)
786 break
787 if cell is None and text:
788 cell = StringCell(text)
789 if cell is None:
790 self.sheet.clearcell(x, y)
791 else:
792 self.sheet.setcell(x, y, cell)
793 self.sync()
794
795 def sync(self):
796 "Fill the GUI cells from the sheet cells."
797 self.sheet.recalc()
Collin Winter6f2df4d2007-07-17 20:59:35 +0000798 for (x, y), gridcell in self.gridcells.items():
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000799 if x == 0 or y == 0:
800 continue
801 cell = self.sheet.getcell(x, y)
802 if cell is None:
803 gridcell['text'] = ""
804 else:
805 if hasattr(cell, 'format'):
806 text, alignment = cell.format()
807 else:
808 text, alignment = str(cell), LEFT
809 gridcell['text'] = text
810 gridcell['anchor'] = align2anchor[alignment]
811
812
813def test_basic():
814 "Basic non-gui self-test."
815 import os
816 a = Sheet()
817 for x in range(1, 11):
818 for y in range(1, 11):
819 if x == 1:
820 cell = NumericCell(y)
821 elif y == 1:
822 cell = NumericCell(x)
823 else:
824 c1 = cellname(x, 1)
825 c2 = cellname(1, y)
826 formula = "%s*%s" % (c1, c2)
827 cell = FormulaCell(formula)
828 a.setcell(x, y, cell)
829## if os.path.isfile("sheet1.xml"):
830## print "Loading from sheet1.xml"
831## a.load("sheet1.xml")
832 a.display()
833 a.save("sheet1.xml")
834
835def test_gui():
836 "GUI test."
Guido van Rossum36692422002-11-02 06:25:51 +0000837 if sys.argv[1:]:
838 filename = sys.argv[1]
839 else:
840 filename = "sheet1.xml"
841 g = SheetGUI(filename)
Guido van Rossum69ccfcc2002-10-28 01:06:37 +0000842 g.root.mainloop()
843
844if __name__ == '__main__':
845 #test_basic()
846 test_gui()