(root)/
Python-3.11.7/
Tools/
demo/
spreadsheet.py
       1  #!/usr/bin/env python3
       2  
       3  """
       4  SS1 -- a spreadsheet-like application.
       5  """
       6  
       7  import os
       8  import re
       9  import sys
      10  from xml.parsers import expat
      11  from xml.sax.saxutils import escape
      12  
      13  LEFT, CENTER, RIGHT = "LEFT", "CENTER", "RIGHT"
      14  
      15  def ljust(x, n):
      16      return x.ljust(n)
      17  def center(x, n):
      18      return x.center(n)
      19  def rjust(x, n):
      20      return x.rjust(n)
      21  align2action = {LEFT: ljust, CENTER: center, RIGHT: rjust}
      22  
      23  align2xml = {LEFT: "left", CENTER: "center", RIGHT: "right"}
      24  xml2align = {"left": LEFT, "center": CENTER, "right": RIGHT}
      25  
      26  align2anchor = {LEFT: "w", CENTER: "center", RIGHT: "e"}
      27  
      28  def sum(seq):
      29      total = 0
      30      for x in seq:
      31          if x is not None:
      32              total += x
      33      return total
      34  
      35  class ESC[4;38;5;81mSheet:
      36  
      37      def __init__(self):
      38          self.cells = {} # {(x, y): cell, ...}
      39          self.ns = dict(
      40              cell = self.cellvalue,
      41              cells = self.multicellvalue,
      42              sum = sum,
      43          )
      44  
      45      def cellvalue(self, x, y):
      46          cell = self.getcell(x, y)
      47          if hasattr(cell, 'recalc'):
      48              return cell.recalc(self.ns)
      49          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.maxsize, y2)
      83  
      84      def clearcolumns(self, x1, x2):
      85          self.clearcells(x1, 0, x2, sys.maxsize)
      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.maxsize, sys.maxsize, 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.maxsize, sys.maxsize, 0, y1-y2-1)
     123  
     124      def insertcolumns(self, x, n):
     125          assert n > 0
     126          self.movecells(x, 0, sys.maxsize, sys.maxsize, 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.maxsize, sys.maxsize, 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):
     142          for cell in self.cells.values():
     143              if hasattr(cell, 'reset'):
     144                  cell.reset()
     145  
     146      def recalc(self):
     147          self.reset()
     148          for cell in self.cells.values():
     149              if hasattr(cell, 'recalc'):
     150                  cell.recalc(self.ns)
     151  
     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
     166          for (x, y), cell in self.cells.items():
     167              if x <= 0 or y <= 0:
     168                  continue
     169              if hasattr(cell, 'recalc'):
     170                  cell.recalc(self.ns)
     171              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
     198              print(line)
     199              if y == 0:
     200                  print(sep)
     201  
     202      def xml(self):
     203          out = ['<spreadsheet>']
     204          for (x, y), cell in self.cells.items():
     205              if hasattr(cell, 'xml'):
     206                  cellxml = cell.xml()
     207              else:
     208                  cellxml = '<value>%s</value>' % escape(cell)
     209              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          with open(filename, "w", encoding='utf-8') as f:
     217              f.write(text)
     218              if text and not text.endswith('\n'):
     219                  f.write('\n')
     220  
     221      def load(self, filename):
     222          with open(filename, 'rb') as f:
     223              SheetParser(self).parsefile(f)
     224  
     225  class ESC[4;38;5;81mSheetParser:
     226  
     227      def __init__(self, sheet):
     228          self.sheet = sheet
     229  
     230      def parsefile(self, f):
     231          parser = expat.ParserCreate()
     232          parser.StartElementHandler = self.startelement
     233          parser.EndElementHandler = self.endelement
     234          parser.CharacterDataHandler = self.data
     235          parser.ParseFile(f)
     236  
     237      def startelement(self, tag, attrs):
     238          method = getattr(self, 'start_'+tag, None)
     239          if method:
     240              method(attrs)
     241          self.texts = []
     242  
     243      def data(self, text):
     244          self.texts.append(text)
     245  
     246      def endelement(self, tag):
     247          method = getattr(self, 'end_'+tag, None)
     248          if method:
     249              method("".join(self.texts))
     250  
     251      def start_cell(self, attrs):
     252          self.y = int(attrs.get("row"))
     253          self.x = int(attrs.get("col"))
     254  
     255      def start_value(self, attrs):
     256          self.fmt = attrs.get('format')
     257          self.alignment = xml2align.get(attrs.get('align'))
     258  
     259      start_formula = start_value
     260  
     261      def end_int(self, text):
     262          try:
     263              self.value = int(text)
     264          except (TypeError, ValueError):
     265              self.value = None
     266  
     267      end_long = end_int
     268  
     269      def end_double(self, text):
     270          try:
     271              self.value = float(text)
     272          except (TypeError, ValueError):
     273              self.value = None
     274  
     275      def end_complex(self, text):
     276          try:
     277              self.value = complex(text)
     278          except (TypeError, ValueError):
     279              self.value = None
     280  
     281      def end_string(self, text):
     282          self.value = text
     283  
     284      def end_value(self, text):
     285          if isinstance(self.value, BaseCell):
     286              self.cell = self.value
     287          elif isinstance(self.value, str):
     288              self.cell = StringCell(self.value,
     289                                     self.fmt or "%s",
     290                                     self.alignment or LEFT)
     291          else:
     292              self.cell = NumericCell(self.value,
     293                                      self.fmt or "%s",
     294                                      self.alignment or RIGHT)
     295  
     296      def end_formula(self, text):
     297          self.cell = FormulaCell(text,
     298                                  self.fmt or "%s",
     299                                  self.alignment or RIGHT)
     300  
     301      def end_cell(self, text):
     302          self.sheet.setcell(self.x, self.y, self.cell)
     303  
     304  class ESC[4;38;5;81mBaseCell:
     305      __init__ = None # Must provide
     306      """Abstract base class for sheet cells.
     307  
     308      Subclasses may but needn't provide the following APIs:
     309  
     310      cell.reset() -- prepare for recalculation
     311      cell.recalc(ns) -> value -- recalculate formula
     312      cell.format() -> (value, alignment) -- return formatted value
     313      cell.xml() -> string -- return XML
     314      """
     315  
     316  class ESC[4;38;5;81mNumericCell(ESC[4;38;5;149mBaseCell):
     317  
     318      def __init__(self, value, fmt="%s", alignment=RIGHT):
     319          assert isinstance(value, (int, float, complex))
     320          assert alignment in (LEFT, CENTER, RIGHT)
     321          self.value = value
     322          self.fmt = fmt
     323          self.alignment = alignment
     324  
     325      def recalc(self, ns):
     326          return self.value
     327  
     328      def format(self):
     329          try:
     330              text = self.fmt % self.value
     331          except:
     332              text = str(self.value)
     333          return text, self.alignment
     334  
     335      def xml(self):
     336          method = getattr(self, '_xml_' + type(self.value).__name__)
     337          return '<value align="%s" format="%s">%s</value>' % (
     338                  align2xml[self.alignment],
     339                  self.fmt,
     340                  method())
     341  
     342      def _xml_int(self):
     343          if -2**31 <= self.value < 2**31:
     344              return '<int>%s</int>' % self.value
     345          else:
     346              return '<long>%s</long>' % self.value
     347  
     348      def _xml_float(self):
     349          return '<double>%r</double>' % self.value
     350  
     351      def _xml_complex(self):
     352          return '<complex>%r</complex>' % self.value
     353  
     354  class ESC[4;38;5;81mStringCell(ESC[4;38;5;149mBaseCell):
     355  
     356      def __init__(self, text, fmt="%s", alignment=LEFT):
     357          assert isinstance(text, str)
     358          assert alignment in (LEFT, CENTER, RIGHT)
     359          self.text = text
     360          self.fmt = fmt
     361          self.alignment = alignment
     362  
     363      def recalc(self, ns):
     364          return self.text
     365  
     366      def format(self):
     367          return self.text, self.alignment
     368  
     369      def xml(self):
     370          s = '<value align="%s" format="%s"><string>%s</string></value>'
     371          return s % (
     372              align2xml[self.alignment],
     373              self.fmt,
     374              escape(self.text))
     375  
     376  class ESC[4;38;5;81mFormulaCell(ESC[4;38;5;149mBaseCell):
     377  
     378      def __init__(self, formula, fmt="%s", alignment=RIGHT):
     379          assert alignment in (LEFT, CENTER, RIGHT)
     380          self.formula = formula
     381          self.translated = translate(self.formula)
     382          self.fmt = fmt
     383          self.alignment = alignment
     384          self.reset()
     385  
     386      def reset(self):
     387          self.value = None
     388  
     389      def recalc(self, ns):
     390          if self.value is None:
     391              try:
     392                  self.value = eval(self.translated, ns)
     393              except:
     394                  exc = sys.exc_info()[0]
     395                  if hasattr(exc, "__name__"):
     396                      self.value = exc.__name__
     397                  else:
     398                      self.value = str(exc)
     399          return self.value
     400  
     401      def format(self):
     402          try:
     403              text = self.fmt % self.value
     404          except:
     405              text = str(self.value)
     406          return text, self.alignment
     407  
     408      def xml(self):
     409          return '<formula align="%s" format="%s">%s</formula>' % (
     410              align2xml[self.alignment],
     411              self.fmt,
     412              escape(self.formula))
     413  
     414      def renumber(self, x1, y1, x2, y2, dx, dy):
     415          out = []
     416          for part in re.split(r'(\w+)', self.formula):
     417              m = re.match('^([A-Z]+)([1-9][0-9]*)$', part)
     418              if m is not None:
     419                  sx, sy = m.groups()
     420                  x = colname2num(sx)
     421                  y = int(sy)
     422                  if x1 <= x <= x2 and y1 <= y <= y2:
     423                      part = cellname(x+dx, y+dy)
     424              out.append(part)
     425          return FormulaCell("".join(out), self.fmt, self.alignment)
     426  
     427  def translate(formula):
     428      """Translate a formula containing fancy cell names to valid Python code.
     429  
     430      Examples:
     431          B4 -> cell(2, 4)
     432          B4:Z100 -> cells(2, 4, 26, 100)
     433      """
     434      out = []
     435      for part in re.split(r"(\w+(?::\w+)?)", formula):
     436          m = re.match(r"^([A-Z]+)([1-9][0-9]*)(?::([A-Z]+)([1-9][0-9]*))?$", part)
     437          if m is None:
     438              out.append(part)
     439          else:
     440              x1, y1, x2, y2 = m.groups()
     441              x1 = colname2num(x1)
     442              if x2 is None:
     443                  s = "cell(%s, %s)" % (x1, y1)
     444              else:
     445                  x2 = colname2num(x2)
     446                  s = "cells(%s, %s, %s, %s)" % (x1, y1, x2, y2)
     447              out.append(s)
     448      return "".join(out)
     449  
     450  def cellname(x, y):
     451      "Translate a cell coordinate to a fancy cell name (e.g. (1, 1)->'A1')."
     452      assert x > 0 # Column 0 has an empty name, so can't use that
     453      return colnum2name(x) + str(y)
     454  
     455  def colname2num(s):
     456      "Translate a column name to number (e.g. 'A'->1, 'Z'->26, 'AA'->27)."
     457      s = s.upper()
     458      n = 0
     459      for c in s:
     460          assert 'A' <= c <= 'Z'
     461          n = n*26 + ord(c) - ord('A') + 1
     462      return n
     463  
     464  def colnum2name(n):
     465      "Translate a column number to name (e.g. 1->'A', etc.)."
     466      assert n > 0
     467      s = ""
     468      while n:
     469          n, m = divmod(n-1, 26)
     470          s = chr(m+ord('A')) + s
     471      return s
     472  
     473  import tkinter as Tk
     474  
     475  class ESC[4;38;5;81mSheetGUI:
     476  
     477      """Beginnings of a GUI for a spreadsheet.
     478  
     479      TO DO:
     480      - clear multiple cells
     481      - Insert, clear, remove rows or columns
     482      - Show new contents while typing
     483      - Scroll bars
     484      - Grow grid when window is grown
     485      - Proper menus
     486      - Undo, redo
     487      - Cut, copy and paste
     488      - Formatting and alignment
     489      """
     490  
     491      def __init__(self, filename="sheet1.xml", rows=10, columns=5):
     492          """Constructor.
     493  
     494          Load the sheet from the filename argument.
     495          Set up the Tk widget tree.
     496          """
     497          # Create and load the sheet
     498          self.filename = filename
     499          self.sheet = Sheet()
     500          if os.path.isfile(filename):
     501              self.sheet.load(filename)
     502          # Calculate the needed grid size
     503          maxx, maxy = self.sheet.getsize()
     504          rows = max(rows, maxy)
     505          columns = max(columns, maxx)
     506          # Create the widgets
     507          self.root = Tk.Tk()
     508          self.root.wm_title("Spreadsheet: %s" % self.filename)
     509          self.beacon = Tk.Label(self.root, text="A1",
     510                                 font=('helvetica', 16, 'bold'))
     511          self.entry = Tk.Entry(self.root)
     512          self.savebutton = Tk.Button(self.root, text="Save",
     513                                      command=self.save)
     514          self.cellgrid = Tk.Frame(self.root)
     515          # Configure the widget lay-out
     516          self.cellgrid.pack(side="bottom", expand=1, fill="both")
     517          self.beacon.pack(side="left")
     518          self.savebutton.pack(side="right")
     519          self.entry.pack(side="left", expand=1, fill="x")
     520          # Bind some events
     521          self.entry.bind("<Return>", self.return_event)
     522          self.entry.bind("<Shift-Return>", self.shift_return_event)
     523          self.entry.bind("<Tab>", self.tab_event)
     524          self.entry.bind("<Shift-Tab>", self.shift_tab_event)
     525          self.entry.bind("<Delete>", self.delete_event)
     526          self.entry.bind("<Escape>", self.escape_event)
     527          # Now create the cell grid
     528          self.makegrid(rows, columns)
     529          # Select the top-left cell
     530          self.currentxy = None
     531          self.cornerxy = None
     532          self.setcurrent(1, 1)
     533          # Copy the sheet cells to the GUI cells
     534          self.sync()
     535  
     536      def delete_event(self, event):
     537          if self.cornerxy != self.currentxy and self.cornerxy is not None:
     538              self.sheet.clearcells(*(self.currentxy + self.cornerxy))
     539          else:
     540              self.sheet.clearcell(*self.currentxy)
     541          self.sync()
     542          self.entry.delete(0, 'end')
     543          return "break"
     544  
     545      def escape_event(self, event):
     546          x, y = self.currentxy
     547          self.load_entry(x, y)
     548  
     549      def load_entry(self, x, y):
     550          cell = self.sheet.getcell(x, y)
     551          if cell is None:
     552              text = ""
     553          elif isinstance(cell, FormulaCell):
     554              text = '=' + cell.formula
     555          else:
     556              text, alignment = cell.format()
     557          self.entry.delete(0, 'end')
     558          self.entry.insert(0, text)
     559          self.entry.selection_range(0, 'end')
     560  
     561      def makegrid(self, rows, columns):
     562          """Helper to create the grid of GUI cells.
     563  
     564          The edge (x==0 or y==0) is filled with labels; the rest is real cells.
     565          """
     566          self.rows = rows
     567          self.columns = columns
     568          self.gridcells = {}
     569          # Create the top left corner cell (which selects all)
     570          cell = Tk.Label(self.cellgrid, relief='raised')
     571          cell.grid_configure(column=0, row=0, sticky='NSWE')
     572          cell.bind("<ButtonPress-1>", self.selectall)
     573          # Create the top row of labels, and configure the grid columns
     574          for x in range(1, columns+1):
     575              self.cellgrid.grid_columnconfigure(x, minsize=64)
     576              cell = Tk.Label(self.cellgrid, text=colnum2name(x), relief='raised')
     577              cell.grid_configure(column=x, row=0, sticky='WE')
     578              self.gridcells[x, 0] = cell
     579              cell.__x = x
     580              cell.__y = 0
     581              cell.bind("<ButtonPress-1>", self.selectcolumn)
     582              cell.bind("<B1-Motion>", self.extendcolumn)
     583              cell.bind("<ButtonRelease-1>", self.extendcolumn)
     584              cell.bind("<Shift-Button-1>", self.extendcolumn)
     585          # Create the leftmost column of labels
     586          for y in range(1, rows+1):
     587              cell = Tk.Label(self.cellgrid, text=str(y), relief='raised')
     588              cell.grid_configure(column=0, row=y, sticky='WE')
     589              self.gridcells[0, y] = cell
     590              cell.__x = 0
     591              cell.__y = y
     592              cell.bind("<ButtonPress-1>", self.selectrow)
     593              cell.bind("<B1-Motion>", self.extendrow)
     594              cell.bind("<ButtonRelease-1>", self.extendrow)
     595              cell.bind("<Shift-Button-1>", self.extendrow)
     596          # Create the real cells
     597          for x in range(1, columns+1):
     598              for y in range(1, rows+1):
     599                  cell = Tk.Label(self.cellgrid, relief='sunken',
     600                                  bg='white', fg='black')
     601                  cell.grid_configure(column=x, row=y, sticky='NSWE')
     602                  self.gridcells[x, y] = cell
     603                  cell.__x = x
     604                  cell.__y = y
     605                  # Bind mouse events
     606                  cell.bind("<ButtonPress-1>", self.press)
     607                  cell.bind("<B1-Motion>", self.motion)
     608                  cell.bind("<ButtonRelease-1>", self.release)
     609                  cell.bind("<Shift-Button-1>", self.release)
     610  
     611      def selectall(self, event):
     612          self.setcurrent(1, 1)
     613          self.setcorner(sys.maxsize, sys.maxsize)
     614  
     615      def selectcolumn(self, event):
     616          x, y = self.whichxy(event)
     617          self.setcurrent(x, 1)
     618          self.setcorner(x, sys.maxsize)
     619  
     620      def extendcolumn(self, event):
     621          x, y = self.whichxy(event)
     622          if x > 0:
     623              self.setcurrent(self.currentxy[0], 1)
     624              self.setcorner(x, sys.maxsize)
     625  
     626      def selectrow(self, event):
     627          x, y = self.whichxy(event)
     628          self.setcurrent(1, y)
     629          self.setcorner(sys.maxsize, y)
     630  
     631      def extendrow(self, event):
     632          x, y = self.whichxy(event)
     633          if y > 0:
     634              self.setcurrent(1, self.currentxy[1])
     635              self.setcorner(sys.maxsize, y)
     636  
     637      def press(self, event):
     638          x, y = self.whichxy(event)
     639          if x > 0 and y > 0:
     640              self.setcurrent(x, y)
     641  
     642      def motion(self, event):
     643          x, y = self.whichxy(event)
     644          if x > 0 and y > 0:
     645              self.setcorner(x, y)
     646  
     647      release = motion
     648  
     649      def whichxy(self, event):
     650          w = self.cellgrid.winfo_containing(event.x_root, event.y_root)
     651          if w is not None and isinstance(w, Tk.Label):
     652              try:
     653                  return w.__x, w.__y
     654              except AttributeError:
     655                  pass
     656          return 0, 0
     657  
     658      def save(self):
     659          self.sheet.save(self.filename)
     660  
     661      def setcurrent(self, x, y):
     662          "Make (x, y) the current cell."
     663          if self.currentxy is not None:
     664              self.change_cell()
     665          self.clearfocus()
     666          self.beacon['text'] = cellname(x, y)
     667          self.load_entry(x, y)
     668          self.entry.focus_set()
     669          self.currentxy = x, y
     670          self.cornerxy = None
     671          gridcell = self.gridcells.get(self.currentxy)
     672          if gridcell is not None:
     673              gridcell['bg'] = 'yellow'
     674  
     675      def setcorner(self, x, y):
     676          if self.currentxy is None or self.currentxy == (x, y):
     677              self.setcurrent(x, y)
     678              return
     679          self.clearfocus()
     680          self.cornerxy = x, y
     681          x1, y1 = self.currentxy
     682          x2, y2 = self.cornerxy or self.currentxy
     683          if x1 > x2:
     684              x1, x2 = x2, x1
     685          if y1 > y2:
     686              y1, y2 = y2, y1
     687          for (x, y), cell in self.gridcells.items():
     688              if x1 <= x <= x2 and y1 <= y <= y2:
     689                  cell['bg'] = 'lightBlue'
     690          gridcell = self.gridcells.get(self.currentxy)
     691          if gridcell is not None:
     692              gridcell['bg'] = 'yellow'
     693          self.setbeacon(x1, y1, x2, y2)
     694  
     695      def setbeacon(self, x1, y1, x2, y2):
     696          if x1 == y1 == 1 and x2 == y2 == sys.maxsize:
     697              name = ":"
     698          elif (x1, x2) == (1, sys.maxsize):
     699              if y1 == y2:
     700                  name = "%d" % y1
     701              else:
     702                  name = "%d:%d" % (y1, y2)
     703          elif (y1, y2) == (1, sys.maxsize):
     704              if x1 == x2:
     705                  name = "%s" % colnum2name(x1)
     706              else:
     707                  name = "%s:%s" % (colnum2name(x1), colnum2name(x2))
     708          else:
     709              name1 = cellname(*self.currentxy)
     710              name2 = cellname(*self.cornerxy)
     711              name = "%s:%s" % (name1, name2)
     712          self.beacon['text'] = name
     713  
     714  
     715      def clearfocus(self):
     716          if self.currentxy is not None:
     717              x1, y1 = self.currentxy
     718              x2, y2 = self.cornerxy or self.currentxy
     719              if x1 > x2:
     720                  x1, x2 = x2, x1
     721              if y1 > y2:
     722                  y1, y2 = y2, y1
     723              for (x, y), cell in self.gridcells.items():
     724                  if x1 <= x <= x2 and y1 <= y <= y2:
     725                      cell['bg'] = 'white'
     726  
     727      def return_event(self, event):
     728          "Callback for the Return key."
     729          self.change_cell()
     730          x, y = self.currentxy
     731          self.setcurrent(x, y+1)
     732          return "break"
     733  
     734      def shift_return_event(self, event):
     735          "Callback for the Return key with Shift modifier."
     736          self.change_cell()
     737          x, y = self.currentxy
     738          self.setcurrent(x, max(1, y-1))
     739          return "break"
     740  
     741      def tab_event(self, event):
     742          "Callback for the Tab key."
     743          self.change_cell()
     744          x, y = self.currentxy
     745          self.setcurrent(x+1, y)
     746          return "break"
     747  
     748      def shift_tab_event(self, event):
     749          "Callback for the Tab key with Shift modifier."
     750          self.change_cell()
     751          x, y = self.currentxy
     752          self.setcurrent(max(1, x-1), y)
     753          return "break"
     754  
     755      def change_cell(self):
     756          "Set the current cell from the entry widget."
     757          x, y = self.currentxy
     758          text = self.entry.get()
     759          cell = None
     760          if text.startswith('='):
     761              cell = FormulaCell(text[1:])
     762          else:
     763              for cls in int, float, complex:
     764                  try:
     765                      value = cls(text)
     766                  except (TypeError, ValueError):
     767                      continue
     768                  else:
     769                      cell = NumericCell(value)
     770                      break
     771          if cell is None and text:
     772              cell = StringCell(text)
     773          if cell is None:
     774              self.sheet.clearcell(x, y)
     775          else:
     776              self.sheet.setcell(x, y, cell)
     777          self.sync()
     778  
     779      def sync(self):
     780          "Fill the GUI cells from the sheet cells."
     781          self.sheet.recalc()
     782          for (x, y), gridcell in self.gridcells.items():
     783              if x == 0 or y == 0:
     784                  continue
     785              cell = self.sheet.getcell(x, y)
     786              if cell is None:
     787                  gridcell['text'] = ""
     788              else:
     789                  if hasattr(cell, 'format'):
     790                      text, alignment = cell.format()
     791                  else:
     792                      text, alignment = str(cell), LEFT
     793                  gridcell['text'] = text
     794                  gridcell['anchor'] = align2anchor[alignment]
     795  
     796  
     797  def test_basic():
     798      "Basic non-gui self-test."
     799      a = Sheet()
     800      for x in range(1, 11):
     801          for y in range(1, 11):
     802              if x == 1:
     803                  cell = NumericCell(y)
     804              elif y == 1:
     805                  cell = NumericCell(x)
     806              else:
     807                  c1 = cellname(x, 1)
     808                  c2 = cellname(1, y)
     809                  formula = "%s*%s" % (c1, c2)
     810                  cell = FormulaCell(formula)
     811              a.setcell(x, y, cell)
     812  ##    if os.path.isfile("sheet1.xml"):
     813  ##        print "Loading from sheet1.xml"
     814  ##        a.load("sheet1.xml")
     815      a.display()
     816      a.save("sheet1.xml")
     817  
     818  def test_gui():
     819      "GUI test."
     820      if sys.argv[1:]:
     821          filename = sys.argv[1]
     822      else:
     823          filename = "sheet1.xml"
     824      g = SheetGUI(filename)
     825      g.root.mainloop()
     826  
     827  if __name__ == '__main__':
     828      #test_basic()
     829      test_gui()