mirror of
https://github.com/python/cpython.git
synced 2024-11-29 09:02:51 +01:00
44b548dda8
And most of the tools. Patch by Emanual Barry, reviewed by me, Serhiy Storchaka, and Martin Panter.
830 lines
25 KiB
Python
Executable File
830 lines
25 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
"""
|
|
SS1 -- a spreadsheet-like application.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
from xml.parsers import expat
|
|
from xml.sax.saxutils import escape
|
|
|
|
LEFT, CENTER, RIGHT = "LEFT", "CENTER", "RIGHT"
|
|
|
|
def ljust(x, n):
|
|
return x.ljust(n)
|
|
def center(x, n):
|
|
return x.center(n)
|
|
def rjust(x, n):
|
|
return x.rjust(n)
|
|
align2action = {LEFT: ljust, CENTER: center, RIGHT: rjust}
|
|
|
|
align2xml = {LEFT: "left", CENTER: "center", RIGHT: "right"}
|
|
xml2align = {"left": LEFT, "center": CENTER, "right": RIGHT}
|
|
|
|
align2anchor = {LEFT: "w", CENTER: "center", RIGHT: "e"}
|
|
|
|
def sum(seq):
|
|
total = 0
|
|
for x in seq:
|
|
if x is not None:
|
|
total += x
|
|
return total
|
|
|
|
class Sheet:
|
|
|
|
def __init__(self):
|
|
self.cells = {} # {(x, y): cell, ...}
|
|
self.ns = dict(
|
|
cell = self.cellvalue,
|
|
cells = self.multicellvalue,
|
|
sum = sum,
|
|
)
|
|
|
|
def cellvalue(self, x, y):
|
|
cell = self.getcell(x, y)
|
|
if hasattr(cell, 'recalc'):
|
|
return cell.recalc(self.ns)
|
|
else:
|
|
return cell
|
|
|
|
def multicellvalue(self, x1, y1, x2, y2):
|
|
if x1 > x2:
|
|
x1, x2 = x2, x1
|
|
if y1 > y2:
|
|
y1, y2 = y2, y1
|
|
seq = []
|
|
for y in range(y1, y2+1):
|
|
for x in range(x1, x2+1):
|
|
seq.append(self.cellvalue(x, y))
|
|
return seq
|
|
|
|
def getcell(self, x, y):
|
|
return self.cells.get((x, y))
|
|
|
|
def setcell(self, x, y, cell):
|
|
assert x > 0 and y > 0
|
|
assert isinstance(cell, BaseCell)
|
|
self.cells[x, y] = cell
|
|
|
|
def clearcell(self, x, y):
|
|
try:
|
|
del self.cells[x, y]
|
|
except KeyError:
|
|
pass
|
|
|
|
def clearcells(self, x1, y1, x2, y2):
|
|
for xy in self.selectcells(x1, y1, x2, y2):
|
|
del self.cells[xy]
|
|
|
|
def clearrows(self, y1, y2):
|
|
self.clearcells(0, y1, sys.maxsize, y2)
|
|
|
|
def clearcolumns(self, x1, x2):
|
|
self.clearcells(x1, 0, x2, sys.maxsize)
|
|
|
|
def selectcells(self, x1, y1, x2, y2):
|
|
if x1 > x2:
|
|
x1, x2 = x2, x1
|
|
if y1 > y2:
|
|
y1, y2 = y2, y1
|
|
return [(x, y) for x, y in self.cells
|
|
if x1 <= x <= x2 and y1 <= y <= y2]
|
|
|
|
def movecells(self, x1, y1, x2, y2, dx, dy):
|
|
if dx == 0 and dy == 0:
|
|
return
|
|
if x1 > x2:
|
|
x1, x2 = x2, x1
|
|
if y1 > y2:
|
|
y1, y2 = y2, y1
|
|
assert x1+dx > 0 and y1+dy > 0
|
|
new = {}
|
|
for x, y in self.cells:
|
|
cell = self.cells[x, y]
|
|
if hasattr(cell, 'renumber'):
|
|
cell = cell.renumber(x1, y1, x2, y2, dx, dy)
|
|
if x1 <= x <= x2 and y1 <= y <= y2:
|
|
x += dx
|
|
y += dy
|
|
new[x, y] = cell
|
|
self.cells = new
|
|
|
|
def insertrows(self, y, n):
|
|
assert n > 0
|
|
self.movecells(0, y, sys.maxsize, sys.maxsize, 0, n)
|
|
|
|
def deleterows(self, y1, y2):
|
|
if y1 > y2:
|
|
y1, y2 = y2, y1
|
|
self.clearrows(y1, y2)
|
|
self.movecells(0, y2+1, sys.maxsize, sys.maxsize, 0, y1-y2-1)
|
|
|
|
def insertcolumns(self, x, n):
|
|
assert n > 0
|
|
self.movecells(x, 0, sys.maxsize, sys.maxsize, n, 0)
|
|
|
|
def deletecolumns(self, x1, x2):
|
|
if x1 > x2:
|
|
x1, x2 = x2, x1
|
|
self.clearcells(x1, x2)
|
|
self.movecells(x2+1, 0, sys.maxsize, sys.maxsize, x1-x2-1, 0)
|
|
|
|
def getsize(self):
|
|
maxx = maxy = 0
|
|
for x, y in self.cells:
|
|
maxx = max(maxx, x)
|
|
maxy = max(maxy, y)
|
|
return maxx, maxy
|
|
|
|
def reset(self):
|
|
for cell in self.cells.values():
|
|
if hasattr(cell, 'reset'):
|
|
cell.reset()
|
|
|
|
def recalc(self):
|
|
self.reset()
|
|
for cell in self.cells.values():
|
|
if hasattr(cell, 'recalc'):
|
|
cell.recalc(self.ns)
|
|
|
|
def display(self):
|
|
maxx, maxy = self.getsize()
|
|
width, height = maxx+1, maxy+1
|
|
colwidth = [1] * width
|
|
full = {}
|
|
# Add column heading labels in row 0
|
|
for x in range(1, width):
|
|
full[x, 0] = text, alignment = colnum2name(x), RIGHT
|
|
colwidth[x] = max(colwidth[x], len(text))
|
|
# Add row labels in column 0
|
|
for y in range(1, height):
|
|
full[0, y] = text, alignment = str(y), RIGHT
|
|
colwidth[0] = max(colwidth[0], len(text))
|
|
# Add sheet cells in columns with x>0 and y>0
|
|
for (x, y), cell in self.cells.items():
|
|
if x <= 0 or y <= 0:
|
|
continue
|
|
if hasattr(cell, 'recalc'):
|
|
cell.recalc(self.ns)
|
|
if hasattr(cell, 'format'):
|
|
text, alignment = cell.format()
|
|
assert isinstance(text, str)
|
|
assert alignment in (LEFT, CENTER, RIGHT)
|
|
else:
|
|
text = str(cell)
|
|
if isinstance(cell, str):
|
|
alignment = LEFT
|
|
else:
|
|
alignment = RIGHT
|
|
full[x, y] = (text, alignment)
|
|
colwidth[x] = max(colwidth[x], len(text))
|
|
# Calculate the horizontal separator line (dashes and dots)
|
|
sep = ""
|
|
for x in range(width):
|
|
if sep:
|
|
sep += "+"
|
|
sep += "-"*colwidth[x]
|
|
# Now print The full grid
|
|
for y in range(height):
|
|
line = ""
|
|
for x in range(width):
|
|
text, alignment = full.get((x, y)) or ("", LEFT)
|
|
text = align2action[alignment](text, colwidth[x])
|
|
if line:
|
|
line += '|'
|
|
line += text
|
|
print(line)
|
|
if y == 0:
|
|
print(sep)
|
|
|
|
def xml(self):
|
|
out = ['<spreadsheet>']
|
|
for (x, y), cell in self.cells.items():
|
|
if hasattr(cell, 'xml'):
|
|
cellxml = cell.xml()
|
|
else:
|
|
cellxml = '<value>%s</value>' % escape(cell)
|
|
out.append('<cell row="%s" col="%s">\n %s\n</cell>' %
|
|
(y, x, cellxml))
|
|
out.append('</spreadsheet>')
|
|
return '\n'.join(out)
|
|
|
|
def save(self, filename):
|
|
text = self.xml()
|
|
with open(filename, "w", encoding='utf-8') as f:
|
|
f.write(text)
|
|
if text and not text.endswith('\n'):
|
|
f.write('\n')
|
|
|
|
def load(self, filename):
|
|
with open(filename, 'rb') as f:
|
|
SheetParser(self).parsefile(f)
|
|
|
|
class SheetParser:
|
|
|
|
def __init__(self, sheet):
|
|
self.sheet = sheet
|
|
|
|
def parsefile(self, f):
|
|
parser = expat.ParserCreate()
|
|
parser.StartElementHandler = self.startelement
|
|
parser.EndElementHandler = self.endelement
|
|
parser.CharacterDataHandler = self.data
|
|
parser.ParseFile(f)
|
|
|
|
def startelement(self, tag, attrs):
|
|
method = getattr(self, 'start_'+tag, None)
|
|
if method:
|
|
method(attrs)
|
|
self.texts = []
|
|
|
|
def data(self, text):
|
|
self.texts.append(text)
|
|
|
|
def endelement(self, tag):
|
|
method = getattr(self, 'end_'+tag, None)
|
|
if method:
|
|
method("".join(self.texts))
|
|
|
|
def start_cell(self, attrs):
|
|
self.y = int(attrs.get("row"))
|
|
self.x = int(attrs.get("col"))
|
|
|
|
def start_value(self, attrs):
|
|
self.fmt = attrs.get('format')
|
|
self.alignment = xml2align.get(attrs.get('align'))
|
|
|
|
start_formula = start_value
|
|
|
|
def end_int(self, text):
|
|
try:
|
|
self.value = int(text)
|
|
except (TypeError, ValueError):
|
|
self.value = None
|
|
|
|
end_long = end_int
|
|
|
|
def end_double(self, text):
|
|
try:
|
|
self.value = float(text)
|
|
except (TypeError, ValueError):
|
|
self.value = None
|
|
|
|
def end_complex(self, text):
|
|
try:
|
|
self.value = complex(text)
|
|
except (TypeError, ValueError):
|
|
self.value = None
|
|
|
|
def end_string(self, text):
|
|
self.value = text
|
|
|
|
def end_value(self, text):
|
|
if isinstance(self.value, BaseCell):
|
|
self.cell = self.value
|
|
elif isinstance(self.value, str):
|
|
self.cell = StringCell(self.value,
|
|
self.fmt or "%s",
|
|
self.alignment or LEFT)
|
|
else:
|
|
self.cell = NumericCell(self.value,
|
|
self.fmt or "%s",
|
|
self.alignment or RIGHT)
|
|
|
|
def end_formula(self, text):
|
|
self.cell = FormulaCell(text,
|
|
self.fmt or "%s",
|
|
self.alignment or RIGHT)
|
|
|
|
def end_cell(self, text):
|
|
self.sheet.setcell(self.x, self.y, self.cell)
|
|
|
|
class BaseCell:
|
|
__init__ = None # Must provide
|
|
"""Abstract base class for sheet cells.
|
|
|
|
Subclasses may but needn't provide the following APIs:
|
|
|
|
cell.reset() -- prepare for recalculation
|
|
cell.recalc(ns) -> value -- recalculate formula
|
|
cell.format() -> (value, alignment) -- return formatted value
|
|
cell.xml() -> string -- return XML
|
|
"""
|
|
|
|
class NumericCell(BaseCell):
|
|
|
|
def __init__(self, value, fmt="%s", alignment=RIGHT):
|
|
assert isinstance(value, (int, float, complex))
|
|
assert alignment in (LEFT, CENTER, RIGHT)
|
|
self.value = value
|
|
self.fmt = fmt
|
|
self.alignment = alignment
|
|
|
|
def recalc(self, ns):
|
|
return self.value
|
|
|
|
def format(self):
|
|
try:
|
|
text = self.fmt % self.value
|
|
except:
|
|
text = str(self.value)
|
|
return text, self.alignment
|
|
|
|
def xml(self):
|
|
method = getattr(self, '_xml_' + type(self.value).__name__)
|
|
return '<value align="%s" format="%s">%s</value>' % (
|
|
align2xml[self.alignment],
|
|
self.fmt,
|
|
method())
|
|
|
|
def _xml_int(self):
|
|
if -2**31 <= self.value < 2**31:
|
|
return '<int>%s</int>' % self.value
|
|
else:
|
|
return '<long>%s</long>' % self.value
|
|
|
|
def _xml_float(self):
|
|
return '<double>%r</double>' % self.value
|
|
|
|
def _xml_complex(self):
|
|
return '<complex>%r</complex>' % self.value
|
|
|
|
class StringCell(BaseCell):
|
|
|
|
def __init__(self, text, fmt="%s", alignment=LEFT):
|
|
assert isinstance(text, str)
|
|
assert alignment in (LEFT, CENTER, RIGHT)
|
|
self.text = text
|
|
self.fmt = fmt
|
|
self.alignment = alignment
|
|
|
|
def recalc(self, ns):
|
|
return self.text
|
|
|
|
def format(self):
|
|
return self.text, self.alignment
|
|
|
|
def xml(self):
|
|
s = '<value align="%s" format="%s"><string>%s</string></value>'
|
|
return s % (
|
|
align2xml[self.alignment],
|
|
self.fmt,
|
|
escape(self.text))
|
|
|
|
class FormulaCell(BaseCell):
|
|
|
|
def __init__(self, formula, fmt="%s", alignment=RIGHT):
|
|
assert alignment in (LEFT, CENTER, RIGHT)
|
|
self.formula = formula
|
|
self.translated = translate(self.formula)
|
|
self.fmt = fmt
|
|
self.alignment = alignment
|
|
self.reset()
|
|
|
|
def reset(self):
|
|
self.value = None
|
|
|
|
def recalc(self, ns):
|
|
if self.value is None:
|
|
try:
|
|
self.value = eval(self.translated, ns)
|
|
except:
|
|
exc = sys.exc_info()[0]
|
|
if hasattr(exc, "__name__"):
|
|
self.value = exc.__name__
|
|
else:
|
|
self.value = str(exc)
|
|
return self.value
|
|
|
|
def format(self):
|
|
try:
|
|
text = self.fmt % self.value
|
|
except:
|
|
text = str(self.value)
|
|
return text, self.alignment
|
|
|
|
def xml(self):
|
|
return '<formula align="%s" format="%s">%s</formula>' % (
|
|
align2xml[self.alignment],
|
|
self.fmt,
|
|
escape(self.formula))
|
|
|
|
def renumber(self, x1, y1, x2, y2, dx, dy):
|
|
out = []
|
|
for part in re.split(r'(\w+)', self.formula):
|
|
m = re.match('^([A-Z]+)([1-9][0-9]*)$', part)
|
|
if m is not None:
|
|
sx, sy = m.groups()
|
|
x = colname2num(sx)
|
|
y = int(sy)
|
|
if x1 <= x <= x2 and y1 <= y <= y2:
|
|
part = cellname(x+dx, y+dy)
|
|
out.append(part)
|
|
return FormulaCell("".join(out), self.fmt, self.alignment)
|
|
|
|
def translate(formula):
|
|
"""Translate a formula containing fancy cell names to valid Python code.
|
|
|
|
Examples:
|
|
B4 -> cell(2, 4)
|
|
B4:Z100 -> cells(2, 4, 26, 100)
|
|
"""
|
|
out = []
|
|
for part in re.split(r"(\w+(?::\w+)?)", formula):
|
|
m = re.match(r"^([A-Z]+)([1-9][0-9]*)(?::([A-Z]+)([1-9][0-9]*))?$", part)
|
|
if m is None:
|
|
out.append(part)
|
|
else:
|
|
x1, y1, x2, y2 = m.groups()
|
|
x1 = colname2num(x1)
|
|
if x2 is None:
|
|
s = "cell(%s, %s)" % (x1, y1)
|
|
else:
|
|
x2 = colname2num(x2)
|
|
s = "cells(%s, %s, %s, %s)" % (x1, y1, x2, y2)
|
|
out.append(s)
|
|
return "".join(out)
|
|
|
|
def cellname(x, y):
|
|
"Translate a cell coordinate to a fancy cell name (e.g. (1, 1)->'A1')."
|
|
assert x > 0 # Column 0 has an empty name, so can't use that
|
|
return colnum2name(x) + str(y)
|
|
|
|
def colname2num(s):
|
|
"Translate a column name to number (e.g. 'A'->1, 'Z'->26, 'AA'->27)."
|
|
s = s.upper()
|
|
n = 0
|
|
for c in s:
|
|
assert 'A' <= c <= 'Z'
|
|
n = n*26 + ord(c) - ord('A') + 1
|
|
return n
|
|
|
|
def colnum2name(n):
|
|
"Translate a column number to name (e.g. 1->'A', etc.)."
|
|
assert n > 0
|
|
s = ""
|
|
while n:
|
|
n, m = divmod(n-1, 26)
|
|
s = chr(m+ord('A')) + s
|
|
return s
|
|
|
|
import tkinter as Tk
|
|
|
|
class SheetGUI:
|
|
|
|
"""Beginnings of a GUI for a spreadsheet.
|
|
|
|
TO DO:
|
|
- clear multiple cells
|
|
- Insert, clear, remove rows or columns
|
|
- Show new contents while typing
|
|
- Scroll bars
|
|
- Grow grid when window is grown
|
|
- Proper menus
|
|
- Undo, redo
|
|
- Cut, copy and paste
|
|
- Formatting and alignment
|
|
"""
|
|
|
|
def __init__(self, filename="sheet1.xml", rows=10, columns=5):
|
|
"""Constructor.
|
|
|
|
Load the sheet from the filename argument.
|
|
Set up the Tk widget tree.
|
|
"""
|
|
# Create and load the sheet
|
|
self.filename = filename
|
|
self.sheet = Sheet()
|
|
if os.path.isfile(filename):
|
|
self.sheet.load(filename)
|
|
# Calculate the needed grid size
|
|
maxx, maxy = self.sheet.getsize()
|
|
rows = max(rows, maxy)
|
|
columns = max(columns, maxx)
|
|
# Create the widgets
|
|
self.root = Tk.Tk()
|
|
self.root.wm_title("Spreadsheet: %s" % self.filename)
|
|
self.beacon = Tk.Label(self.root, text="A1",
|
|
font=('helvetica', 16, 'bold'))
|
|
self.entry = Tk.Entry(self.root)
|
|
self.savebutton = Tk.Button(self.root, text="Save",
|
|
command=self.save)
|
|
self.cellgrid = Tk.Frame(self.root)
|
|
# Configure the widget lay-out
|
|
self.cellgrid.pack(side="bottom", expand=1, fill="both")
|
|
self.beacon.pack(side="left")
|
|
self.savebutton.pack(side="right")
|
|
self.entry.pack(side="left", expand=1, fill="x")
|
|
# Bind some events
|
|
self.entry.bind("<Return>", self.return_event)
|
|
self.entry.bind("<Shift-Return>", self.shift_return_event)
|
|
self.entry.bind("<Tab>", self.tab_event)
|
|
self.entry.bind("<Shift-Tab>", self.shift_tab_event)
|
|
self.entry.bind("<Delete>", self.delete_event)
|
|
self.entry.bind("<Escape>", self.escape_event)
|
|
# Now create the cell grid
|
|
self.makegrid(rows, columns)
|
|
# Select the top-left cell
|
|
self.currentxy = None
|
|
self.cornerxy = None
|
|
self.setcurrent(1, 1)
|
|
# Copy the sheet cells to the GUI cells
|
|
self.sync()
|
|
|
|
def delete_event(self, event):
|
|
if self.cornerxy != self.currentxy and self.cornerxy is not None:
|
|
self.sheet.clearcells(*(self.currentxy + self.cornerxy))
|
|
else:
|
|
self.sheet.clearcell(*self.currentxy)
|
|
self.sync()
|
|
self.entry.delete(0, 'end')
|
|
return "break"
|
|
|
|
def escape_event(self, event):
|
|
x, y = self.currentxy
|
|
self.load_entry(x, y)
|
|
|
|
def load_entry(self, x, y):
|
|
cell = self.sheet.getcell(x, y)
|
|
if cell is None:
|
|
text = ""
|
|
elif isinstance(cell, FormulaCell):
|
|
text = '=' + cell.formula
|
|
else:
|
|
text, alignment = cell.format()
|
|
self.entry.delete(0, 'end')
|
|
self.entry.insert(0, text)
|
|
self.entry.selection_range(0, 'end')
|
|
|
|
def makegrid(self, rows, columns):
|
|
"""Helper to create the grid of GUI cells.
|
|
|
|
The edge (x==0 or y==0) is filled with labels; the rest is real cells.
|
|
"""
|
|
self.rows = rows
|
|
self.columns = columns
|
|
self.gridcells = {}
|
|
# Create the top left corner cell (which selects all)
|
|
cell = Tk.Label(self.cellgrid, relief='raised')
|
|
cell.grid_configure(column=0, row=0, sticky='NSWE')
|
|
cell.bind("<ButtonPress-1>", self.selectall)
|
|
# Create the top row of labels, and configure the grid columns
|
|
for x in range(1, columns+1):
|
|
self.cellgrid.grid_columnconfigure(x, minsize=64)
|
|
cell = Tk.Label(self.cellgrid, text=colnum2name(x), relief='raised')
|
|
cell.grid_configure(column=x, row=0, sticky='WE')
|
|
self.gridcells[x, 0] = cell
|
|
cell.__x = x
|
|
cell.__y = 0
|
|
cell.bind("<ButtonPress-1>", self.selectcolumn)
|
|
cell.bind("<B1-Motion>", self.extendcolumn)
|
|
cell.bind("<ButtonRelease-1>", self.extendcolumn)
|
|
cell.bind("<Shift-Button-1>", self.extendcolumn)
|
|
# Create the leftmost column of labels
|
|
for y in range(1, rows+1):
|
|
cell = Tk.Label(self.cellgrid, text=str(y), relief='raised')
|
|
cell.grid_configure(column=0, row=y, sticky='WE')
|
|
self.gridcells[0, y] = cell
|
|
cell.__x = 0
|
|
cell.__y = y
|
|
cell.bind("<ButtonPress-1>", self.selectrow)
|
|
cell.bind("<B1-Motion>", self.extendrow)
|
|
cell.bind("<ButtonRelease-1>", self.extendrow)
|
|
cell.bind("<Shift-Button-1>", self.extendrow)
|
|
# Create the real cells
|
|
for x in range(1, columns+1):
|
|
for y in range(1, rows+1):
|
|
cell = Tk.Label(self.cellgrid, relief='sunken',
|
|
bg='white', fg='black')
|
|
cell.grid_configure(column=x, row=y, sticky='NSWE')
|
|
self.gridcells[x, y] = cell
|
|
cell.__x = x
|
|
cell.__y = y
|
|
# Bind mouse events
|
|
cell.bind("<ButtonPress-1>", self.press)
|
|
cell.bind("<B1-Motion>", self.motion)
|
|
cell.bind("<ButtonRelease-1>", self.release)
|
|
cell.bind("<Shift-Button-1>", self.release)
|
|
|
|
def selectall(self, event):
|
|
self.setcurrent(1, 1)
|
|
self.setcorner(sys.maxsize, sys.maxsize)
|
|
|
|
def selectcolumn(self, event):
|
|
x, y = self.whichxy(event)
|
|
self.setcurrent(x, 1)
|
|
self.setcorner(x, sys.maxsize)
|
|
|
|
def extendcolumn(self, event):
|
|
x, y = self.whichxy(event)
|
|
if x > 0:
|
|
self.setcurrent(self.currentxy[0], 1)
|
|
self.setcorner(x, sys.maxsize)
|
|
|
|
def selectrow(self, event):
|
|
x, y = self.whichxy(event)
|
|
self.setcurrent(1, y)
|
|
self.setcorner(sys.maxsize, y)
|
|
|
|
def extendrow(self, event):
|
|
x, y = self.whichxy(event)
|
|
if y > 0:
|
|
self.setcurrent(1, self.currentxy[1])
|
|
self.setcorner(sys.maxsize, y)
|
|
|
|
def press(self, event):
|
|
x, y = self.whichxy(event)
|
|
if x > 0 and y > 0:
|
|
self.setcurrent(x, y)
|
|
|
|
def motion(self, event):
|
|
x, y = self.whichxy(event)
|
|
if x > 0 and y > 0:
|
|
self.setcorner(x, y)
|
|
|
|
release = motion
|
|
|
|
def whichxy(self, event):
|
|
w = self.cellgrid.winfo_containing(event.x_root, event.y_root)
|
|
if w is not None and isinstance(w, Tk.Label):
|
|
try:
|
|
return w.__x, w.__y
|
|
except AttributeError:
|
|
pass
|
|
return 0, 0
|
|
|
|
def save(self):
|
|
self.sheet.save(self.filename)
|
|
|
|
def setcurrent(self, x, y):
|
|
"Make (x, y) the current cell."
|
|
if self.currentxy is not None:
|
|
self.change_cell()
|
|
self.clearfocus()
|
|
self.beacon['text'] = cellname(x, y)
|
|
self.load_entry(x, y)
|
|
self.entry.focus_set()
|
|
self.currentxy = x, y
|
|
self.cornerxy = None
|
|
gridcell = self.gridcells.get(self.currentxy)
|
|
if gridcell is not None:
|
|
gridcell['bg'] = 'yellow'
|
|
|
|
def setcorner(self, x, y):
|
|
if self.currentxy is None or self.currentxy == (x, y):
|
|
self.setcurrent(x, y)
|
|
return
|
|
self.clearfocus()
|
|
self.cornerxy = x, y
|
|
x1, y1 = self.currentxy
|
|
x2, y2 = self.cornerxy or self.currentxy
|
|
if x1 > x2:
|
|
x1, x2 = x2, x1
|
|
if y1 > y2:
|
|
y1, y2 = y2, y1
|
|
for (x, y), cell in self.gridcells.items():
|
|
if x1 <= x <= x2 and y1 <= y <= y2:
|
|
cell['bg'] = 'lightBlue'
|
|
gridcell = self.gridcells.get(self.currentxy)
|
|
if gridcell is not None:
|
|
gridcell['bg'] = 'yellow'
|
|
self.setbeacon(x1, y1, x2, y2)
|
|
|
|
def setbeacon(self, x1, y1, x2, y2):
|
|
if x1 == y1 == 1 and x2 == y2 == sys.maxsize:
|
|
name = ":"
|
|
elif (x1, x2) == (1, sys.maxsize):
|
|
if y1 == y2:
|
|
name = "%d" % y1
|
|
else:
|
|
name = "%d:%d" % (y1, y2)
|
|
elif (y1, y2) == (1, sys.maxsize):
|
|
if x1 == x2:
|
|
name = "%s" % colnum2name(x1)
|
|
else:
|
|
name = "%s:%s" % (colnum2name(x1), colnum2name(x2))
|
|
else:
|
|
name1 = cellname(*self.currentxy)
|
|
name2 = cellname(*self.cornerxy)
|
|
name = "%s:%s" % (name1, name2)
|
|
self.beacon['text'] = name
|
|
|
|
|
|
def clearfocus(self):
|
|
if self.currentxy is not None:
|
|
x1, y1 = self.currentxy
|
|
x2, y2 = self.cornerxy or self.currentxy
|
|
if x1 > x2:
|
|
x1, x2 = x2, x1
|
|
if y1 > y2:
|
|
y1, y2 = y2, y1
|
|
for (x, y), cell in self.gridcells.items():
|
|
if x1 <= x <= x2 and y1 <= y <= y2:
|
|
cell['bg'] = 'white'
|
|
|
|
def return_event(self, event):
|
|
"Callback for the Return key."
|
|
self.change_cell()
|
|
x, y = self.currentxy
|
|
self.setcurrent(x, y+1)
|
|
return "break"
|
|
|
|
def shift_return_event(self, event):
|
|
"Callback for the Return key with Shift modifier."
|
|
self.change_cell()
|
|
x, y = self.currentxy
|
|
self.setcurrent(x, max(1, y-1))
|
|
return "break"
|
|
|
|
def tab_event(self, event):
|
|
"Callback for the Tab key."
|
|
self.change_cell()
|
|
x, y = self.currentxy
|
|
self.setcurrent(x+1, y)
|
|
return "break"
|
|
|
|
def shift_tab_event(self, event):
|
|
"Callback for the Tab key with Shift modifier."
|
|
self.change_cell()
|
|
x, y = self.currentxy
|
|
self.setcurrent(max(1, x-1), y)
|
|
return "break"
|
|
|
|
def change_cell(self):
|
|
"Set the current cell from the entry widget."
|
|
x, y = self.currentxy
|
|
text = self.entry.get()
|
|
cell = None
|
|
if text.startswith('='):
|
|
cell = FormulaCell(text[1:])
|
|
else:
|
|
for cls in int, float, complex:
|
|
try:
|
|
value = cls(text)
|
|
except (TypeError, ValueError):
|
|
continue
|
|
else:
|
|
cell = NumericCell(value)
|
|
break
|
|
if cell is None and text:
|
|
cell = StringCell(text)
|
|
if cell is None:
|
|
self.sheet.clearcell(x, y)
|
|
else:
|
|
self.sheet.setcell(x, y, cell)
|
|
self.sync()
|
|
|
|
def sync(self):
|
|
"Fill the GUI cells from the sheet cells."
|
|
self.sheet.recalc()
|
|
for (x, y), gridcell in self.gridcells.items():
|
|
if x == 0 or y == 0:
|
|
continue
|
|
cell = self.sheet.getcell(x, y)
|
|
if cell is None:
|
|
gridcell['text'] = ""
|
|
else:
|
|
if hasattr(cell, 'format'):
|
|
text, alignment = cell.format()
|
|
else:
|
|
text, alignment = str(cell), LEFT
|
|
gridcell['text'] = text
|
|
gridcell['anchor'] = align2anchor[alignment]
|
|
|
|
|
|
def test_basic():
|
|
"Basic non-gui self-test."
|
|
a = Sheet()
|
|
for x in range(1, 11):
|
|
for y in range(1, 11):
|
|
if x == 1:
|
|
cell = NumericCell(y)
|
|
elif y == 1:
|
|
cell = NumericCell(x)
|
|
else:
|
|
c1 = cellname(x, 1)
|
|
c2 = cellname(1, y)
|
|
formula = "%s*%s" % (c1, c2)
|
|
cell = FormulaCell(formula)
|
|
a.setcell(x, y, cell)
|
|
## if os.path.isfile("sheet1.xml"):
|
|
## print "Loading from sheet1.xml"
|
|
## a.load("sheet1.xml")
|
|
a.display()
|
|
a.save("sheet1.xml")
|
|
|
|
def test_gui():
|
|
"GUI test."
|
|
if sys.argv[1:]:
|
|
filename = sys.argv[1]
|
|
else:
|
|
filename = "sheet1.xml"
|
|
g = SheetGUI(filename)
|
|
g.root.mainloop()
|
|
|
|
if __name__ == '__main__':
|
|
#test_basic()
|
|
test_gui()
|