#! /usr/bin/env python2

# SSGridView.py
#
# Demonstrates the creation of widgets as part of a Canvas object
# This is used to build a simple spreadsheet GUI 
#
# Hans Vangheluwe March 2002

# Required modules
from Tkinter import *   # basic Tkinter
import tkFileDialog     # file selection 

class CellCoord:
  """
  A coordinate class (unbounded)
  To enhance readability: 
  refer to coord.row and coord.col 
  rather than coord[0] and coord[1]
  """
  def __init__(self, row=0, col=0):
    self.row=row
    self.col=col

  # string representation
  def __repr__(self):
    return '(row: '+`self.row`+', column: '+`self.col`+')'

class DataElement:
  """
  Data found in cells:
   - str: the stringValue (for any dataType)
   - dataType: "Unknown", "Integer", "Real", "String" or "Formula"
     Note how the dataType can, by design NEVER be "Unknown": 
     data is only stored in a cell if its dataType was determined. 
     Illegal input for example is never entered in a DataElement 
     (see the FSA describing behaviour).
   - integerValue: the integer value when dataType == "Integer"
   - realValue: the real value when dataType == "Real"
   - stringValue and formulaValue don't exist (both are str)
   - textObject: a text object drawn on the canvas displaying str
   Note: Python would allow us to use a single attribute "value"
   in which any type of value could be stored. To allow easy transition
   to other implementation languages, this is not done here.
  """
  def __init__(self, stringValue=None):
    if  stringValue==None:
     self.str=""
    else:
     self.str=stringValue
    self.dataType = "Unknown"

#  def set(self, stringValue=None, textObject=None):
    # self.str=stringValue
    # self.textObject=textObject
#    self.setStringValue(stringValue)
#    self.setTextObject(textObject)
#   set with two default arguments was confusing and not really needed

  def setType(self, dataType="Unknown"):
    self.dataType = dataType

  def getType(self):
    return self.dataType

  def setStringValue(self, stringValue=None):
    self.str=stringValue

  def getStringValue(self):
    return self.str

  def setIntegerValue(self, integerValue=0):
    self.integerValue = integerValue

  def getIntegerValue(self):
    return self.integerValue 

  def setRealValue(self, realValue=0.0):
    self.realValue = realValue

  def getRealValue(self):
    return self.realValue

  def setTextObject(self, textObject=None):
    self.textObject=textObject

  def getTextObject(self):
    return self.textObject

  def __str__(self):
    return self.str

class SpreadsheetGridView(Frame):
  """
  The window containing the Grid View
  """
  def __init__(self,
               master=None,
               title="Spreadsheet View",
               data=None,
               dataOffsetHor=0, dataOffsetVer=0,
               height=30, width=7,
               cellHeight=18, cellWidth=50,
               padHor=10, padVer=10):

    # All code in this __init__ may be considered
    # as the default action taken when entering the
    # spreadsheet Statechart

    # Initialize superclass 
    Frame.__init__(self, master)
    
    # Initialize packer
    self.pack()

    # Make arguments available to all methods 
    # Note: should really test for validity
    #       for example, height and width should be >= 1
    self.window = master    # the parent Tk window
    self.title = title      # the window's title
    # currently not used: 
    #self.data  = data       # the observed data
    #self.dataOffsetHor= dataOffsetHor # horizontal offset from data(0,0)
    #self.dataOffsetVer= dataOffsetVer # vertical offset from data(0,0)
    self.height = height    # the number of grid rows
    self.width  = width     # the number of grid columns
    self.cellHeight = cellHeight # height in pixels of one cell
    self.cellWidth  = cellWidth  # width in pixels of one cell
    self.padHor  = padHor   # horizontal distance of grid from canvas border
    self.padVer  = padVer   # vertical distance of grid from canvas border

    # A dictionary indexed by (row, column) keys
    # containing DataElement entries
    # Thanks to the use of a dictionary, the extent of
    # the width x height can become arbitrarily large.
    # This is meaningful as the DataSheet "matrix" is often sparse.
    # (see assignment 1 for performance difference between list and dict)
    self.data={}

    # The initial "bounding box" (area filled with data)
    self.maxDim=CellCoord(0,0)
 
    # Handle window destruction 
    self.window.protocol("WM_DELETE_WINDOW", self.wmQuit) 

    # Set the window's title
    self.window.title(self.title)

    # Initialize the overCell object
    # which contains the coordinates of the cell
    # the mouse is currently over.
    # This could be any cell, but (0,0) seems most reasonable
    # We make sure this is a meaningful cell (within the
    # spreadsheet) by "snapping" it into the grid boundaries 
    # by means of spreadsheetBoundedCoord()
    self.overCell = self.spreadsheetBoundedCoord(CellCoord(0,0))

    # The Tk variable overCellString
    # is used to display the coordinates of the cell the mouse is 
    # currently over (modulo spreadsheet size)
    # It is updated in the callback routine for the mouse <Motion> event
    self.overCellString = StringVar()
    self.overCellString.set(`self.overCell`)

    # The Tk variable overCellData
    # is used to display the data in the cell the mouse is currently over
    # (modulo spreadsheet size)
    # It is updated in the callback routine for the mouse <Motion> event
    self.overCellData = StringVar()
    if self.getData(self.overCell) == None:
      self.overCellData.set("")
    else:
      self.overCellData.set(self.getData(self.overCell).getStringValue()) 

    # Initialize selectedCell to be equal to overCell
    # This could be some other cell,
    # but it seems reasonable to make this the same as overCell.
    # Note: selectedCell determines where typed in data will go.
    self.selectedCell=CellCoord(self.overCell.row, self.overCell.col)

    # Create all widgets in the Frame
    self.createWidgets()
 
    # For testing
    self.setData(coord=CellCoord(1, 0), stringValue=str(10))
    self.setData(coord=CellCoord(0, 0), stringValue=str(1000))
    self.setData(coord=CellCoord(3, 3), stringValue="hello")
    self.setData(coord=CellCoord(2, 5), stringValue=self.title)
    self.setData(coord=CellCoord(1, 0), stringValue=str(999))
    self.setData(coord=CellCoord(0, 0), stringValue=None)

  def getData(self, coord):
    """
    Get data from DataSheet at coord position
    If no entry at that position, return None
    """
    self.position=(coord.row, coord.col)
    if (self.data.has_key(self.position)):
      return self.data[self.position]
    else:
      return None

  def setData(self, coord, stringValue=None):
    """
    Set string in DataSheet at coord position.
    If a data element was present at that position, it gets overwritten.
    If stringValue == None, the entry at coord position gets removed
    from the DataSheet dictionary. The corresponding drawarea
    textObject also gets deleted.
    """
    self.position=(coord.row, coord.col)
    if (stringValue == None):
      if (self.data.has_key(self.position)):
        # delete the textobject from the drawarea Canvas
        self.drawarea.delete(self.data[self.position].getTextObject())
        # delete the entry from self.data, the DataSheet dictionary
        del self.data[self.position]
        self.calcMaxDim()
    else:
      self.maxDim.row=max(self.maxDim.row, coord.row)
      self.maxDim.col=max(self.maxDim.col, coord.col)
      if self.data.has_key(self.position):
        self.data[self.position].setStringValue(stringValue)
        self.drawarea.itemconfigure(\
         self.data[self.position].getTextObject(), text=stringValue)
      else:
        self.data[self.position]=DataElement()
        text = self.drawarea.create_text(\
               self.padHor + int((coord.col+1/2.0)*self.cellWidth),
               self.padVer + int((coord.row+1/2.0)*self.cellHeight),
               text=stringValue,
               anchor="c") # centered
               # Later, a cell should have other attributes such as 
	       #  font, font size, alignment, colour, ...
        self.data[self.position].setStringValue(stringValue)
        self.data[self.position].setTextObject(text)

      # Update the overCellData variable monitoring
      # the content of the cell the mouse is currently over
      if (self.overCell.row == coord.row) and \
         (self.overCell.col == coord.col):
         # Note: rather than the above test, 
         #  should define comparison 
         #  (__eq__ since Python 2.1, __cmp__ before)
         # for the CellCoord class 
         if self.getData(self.overCell) == None:
           self.overCellData.set("")
         else:
           self.overCellData.set(self.getData(self.overCell).getStringValue())

  def calcMaxDim(self):
    """
    Calculate and set the maximum row, col values
    (an instance of the CellCoord class)
    """
    rowMax=0
    colMax=0
    for coord in self.data.keys():
     if (coord[0] > rowMax): rowMax = coord[0]
     if (coord[1] > colMax): colMax = coord[1]
    self.maxDim.row=rowMax
    self.maxDim.col=colMax

  def getMaxDim(self):
    """
    Returns the maximum row and column values
    as an instance of the CellCoord class
    """
    return self.maxDim

  def printData(self):
    """
    print all data in the datasheet to stdout
    """
    maxDim=self.getMaxDim()
    for row in range(maxDim.row+1):
      print 'row %3d:' % row,
      for column in range(maxDim.col+1):
        if self.getData(CellCoord(row,column)) == None:
          print '%8s' % '--',
        else:
          print '%8s' % self.getData(CellCoord(row,column)).getStringValue(),
      print

  def wmQuit(self):
    # we may wish to put cleanup actions here
    print self.title + " was closed (by Window Manager)"
    self.window.destroy()

  def viewQuit(self):
    # we may wish to put cleanup actions here
    print self.title + " was closed (Quit View button pressed)"
    self.window.destroy()

  def viewQuitError(self):
    print self.title + " was closed (Fatal Error)"
    self.window.destroy()

  # The window and its widgets
  #
  def createWidgets(self):

    # Quit button at the bottom
    self.quitButton = Button(master=self,
                             takefocus=0,
                             text='Quit View',
                             foreground='red',
                             command=self.viewQuit)
    self.quitButton.pack(side=BOTTOM, fill=BOTH)

    # Frame to hold top labels and print button
    self.topFrame = Frame(master=self, background='white')
    self.topFrame.pack(side=TOP,fill=X)

    # Label displaying the content of the cell the mouse is
    # currently over.
    self.overCellContent = Label(master=self.topFrame,
                                 takefocus=0,
                                 textvariable=self.overCellData,
                                 width=20,
                                 relief="groove",
                                 background='white',
                                 foreground='blue')
    self.overCellContent.pack(side=RIGHT, padx=4)

    # Label displaying the cell the mouse is currently over
    # The Tk variable self.overCellString is used for this
    self.overCellCoord = Label(master=self.topFrame,
                               takefocus=0,
                               textvariable=self.overCellString,
                               width=20,
                               relief="groove",
                               background='white',
                               foreground='blue')
    self.overCellCoord.pack(side=RIGHT, padx=4)

    # Button to print this spreadsheet view to file
    self.printButton = Button(master=self.topFrame,
                              takefocus=0,
                              text="Print View",
                              command=self.printView)
    self.printButton.pack(side=TOP)

    # Canvas 15cm x 15cm, packed on top, left
    # Scrollable region calculated from 
    #   self.height, self.width
    #   self.cellHeight, self.cellWidth
    #   self.padHor, self.padVer
    scrollHor = self.width*self.cellWidth + self.padHor*2 
    scrollVer = self.height*self.cellHeight + self.padVer*2 
    self.drawarea = Canvas(master=self,
                           takefocus=1,
                           width="15c", height="15c",
                           background="white",
                           scrollregion=(0, 0, scrollHor, scrollVer))
    # Make sure the Canvas has the focus
    # This is useful if there are other widgets around in the same
    # window such as Entry fields.
    self.drawarea.focus_set()

    # An array of Rectangles in the canvas
    # For efficiency reasons, vertical and horizontal lines
    # are drawn ((width + 1) + (height + 1)) in total
    # rather than width * height rectangle objects.
    # Cell content properties are kept in self.data
    for row in range(self.height+1):
      self.drawarea.create_line(self.padHor,
                   self.padVer+row*self.cellHeight,
                   self.padHor+self.width*self.cellWidth,
                   self.padVer+row*self.cellHeight,
                   fill="grey")
    for column in range(self.width+1):
      self.drawarea.create_line(self.padHor+column*self.cellWidth,
                   self.padVer,
                   self.padHor+column*self.cellWidth,
                   self.padVer+self.height*self.cellHeight,
                   fill="grey")

    # scrollbars for drawarea
    self.drawarea.scrollX = Scrollbar(self, takefocus=0, orient=HORIZONTAL)
    self.drawarea.scrollY = Scrollbar(self, takefocus=0, orient=VERTICAL)

    # link canvas, scrollbars, and scrolling events
    self.drawarea['xscrollcommand'] = self.drawarea.scrollX.set
    self.drawarea['yscrollcommand'] = self.drawarea.scrollY.set
    self.drawarea.scrollX['command'] = self.drawarea.xview
    self.drawarea.scrollY['command'] = self.drawarea.yview

    # pack it all
    self.drawarea.scrollX.pack(side=BOTTOM, fill=X)
    self.drawarea.scrollY.pack(side=RIGHT, fill=Y)

    self.drawarea.pack(side=LEFT,fill=BOTH, expand=1)
    # TODO: make canvas width x height expand with window

    # Draw a thick blue rectangle self.overCellFrame
    # around the cell the mouse is over,
    # only if within the spreadsheet boundaries of course
    # ("snapped" to spreadsheet boundary)
    # This overCellFrame object will later be moved to whichever cell
    # the mouse moves to (as a result of mouse <Motion>).
    if not ((0 <= self.overCell.row < self.height) and\
            (0 <= self.overCell.col < self.width)):
      print "self.overCell should always be within spreadsheet boundaries"
      print "correct initialization and updating should have taken care"
      print "of this. Maybe a method updating self.overCell without"
      print "using self.spreadsheetBoundedCoord() was created"
      self.viewQuitError()
    self.overCellFrame = self.drawarea.create_rectangle(\
      self.padHor+self.overCell.col*self.cellWidth,
      self.padVer+self.overCell.row*self.cellHeight,
      self.padHor+(self.overCell.col+1)*self.cellWidth,
      self.padVer+(self.overCell.row+1)*self.cellHeight,
      width=1, outline="blue")

    # Draw a thick black rectangle self.selectedCellFrame
    # around the selected cell, only if within the spreadsheet
    # boundaries of course ("snapped" to spreadsheet boundary).
    # This selectedCellFrame object will later be moved to the
    # a new cell the user selects (as a result of mouse <ButtonPress-1>).
    if not ((0 <= self.selectedCell.row < self.height) and\
            (0 <= self.selectedCell.col < self.width)):
      print "self.selectedCell should always be within spreadsheet boundaries"
      print "correct initialization and updating should have taken care"
      print "of this. Maybe a method updating self.selectedCell without"
      print "using self.spreadsheetBoundedCoord() was created"
      self.viewQuitError()
    self.selectedCellFrame = self.drawarea.create_rectangle(\
      self.padHor+self.selectedCell.col*self.cellWidth,
      self.padVer+self.selectedCell.row*self.cellHeight,
      self.padHor+(self.selectedCell.col+1)*self.cellWidth,
      self.padVer+(self.selectedCell.row+1)*self.cellHeight,
      width=2, outline="black")

    # Bind mouse events in drawarea to callback methods
    #
    self.drawarea.bind("<Motion>", self.onDrawareaMouseMove)
    self.drawarea.bind("<ButtonPress-1>", self.onDrawareaMouse1Click)
    #self.drawarea.bind("<Left>", self.onDrawareaLeft)
    #self.drawarea.bind("<Right>", self.onDrawareaRight)
    #self.drawarea.bind("<Up>", self.onDrawareaUp)
    #self.drawarea.bind("<Down>", self.onDrawareaDown)
    #self.drawarea.bind("<Return>", self.onDrawareaDown)
    #self.drawarea.bind("<KP_Enter>", self.onDrawareaDown)
    #self.drawarea.bind("<KeyPress>", self.onDrawareaKeyPressed)

  def cellCoords(self, mouseX, mouseY):
    """
    Utility method to convert mouse position (event.x, event.y)
    into cell coordinates (row, col)
    Returns a CellCoord object
    This conversion takes scrolling into account and translates
    screen coordinates into canvas coordinates (canvasx(), canvasy())
    """
    cellRow =\
     (int(self.drawarea.canvasy(mouseY)) - self.padVer)/self.cellHeight
    cellCol =\
     (int(self.drawarea.canvasx(mouseX)) - self.padHor)/self.cellWidth
    return CellCoord(cellRow, cellCol)

  def spreadsheetBoundedCoord(self, cellCoord):
    """
    Returns coordinates bounded by the size of the spreadsheet.
    If a coordinate exceeds the boundary, the boundary value is used.
    """
    boundedCoord=CellCoord(cellCoord.row,cellCoord.col)
    if   (cellCoord.row < 0):            boundedCoord.row = 0
    elif (cellCoord.row >= self.height): boundedCoord.row = self.height-1
    if   (cellCoord.col < 0):            boundedCoord.col = 0
    elif (cellCoord.col >= self.width):  boundedCoord.col = self.width-1
    return boundedCoord

  def moveCellFrame(self, cellFrame, oldCellCoord, newCellCoord):
    """
    Move the cellFrame rectangle from oldCellCoord to newCellCoord
    Update oldCellCoord (passed by reference as all mutable objects
    are really object references is Python)
    """

    # move the cellFrame to the new selected cell area
    deltaX = (newCellCoord.col - oldCellCoord.col)\
             * self.cellWidth
    deltaY = (newCellCoord.row - oldCellCoord.row)\
             * self.cellHeight
    self.drawarea.move(cellFrame, deltaX, deltaY)

    # update the selected cell
    oldCellCoord.row = newCellCoord.row
    oldCellCoord.col = newCellCoord.col

  # The callbacks
  #

  def onDrawareaMouseMove(self, event):
    """
    Callback for the mouse <Motion> event on the drawarea canvas
    This updates the self.overCell variable
    and moves the self.overCellFrame rectangle
    Both, only in case the mouse is over a different cell
    than it was before
    """
    cellCoord = self.spreadsheetBoundedCoord(\
                     self.cellCoords(event.x, event.y))
    # Only if the mouse moves over a different cell
    if (cellCoord.row != self.overCell.row) or\
       (cellCoord.col != self.overCell.col):
       self.moveCellFrame(self.overCellFrame, self.overCell, cellCoord)
       # Update the overCellString variable monitoring
       # the cell the mouse is currently over
       self.overCellString.set(self.overCell)
       # Update the overCellData variable monitoring
       # the content of the cell the mouse is currently over
       if self.getData(self.overCell) == None:
         self.overCellData.set("")
       else:
         self.overCellData.set(self.getData(self.overCell).getStringValue())

  def onDrawareaMouse1Click(self, event):
    """
    Callback for the mouse <Mouse-1> event on the drawarea canvas
    This updates the self.selectedCell variable
    and moves the self.selectedCellFrame rectangle.
    """
 
    # translate to cell coordinates and "snap" to within the spreadsheet
    newSelectedCell = self.spreadsheetBoundedCoord(\
                           self.cellCoords(event.x, event.y))

    # For efficiency reasons, 
    # only if the selected cell is different from the
    # previously selected one: move the self.selectedCellFrame
    if (newSelectedCell.row != self.selectedCell.row) or\
       (newSelectedCell.col != self.selectedCell.col):
       self.moveCellFrame(self.selectedCellFrame,
                          self.selectedCell, newSelectedCell)

  def printView(self):
    """
    Print the drawn-on area of the canvas to a PostScript file.
    Ask the user for filename interactively.
    """
    # For debugging, print non-empty data area to stdout
    self.printData()

    # Ask user for file to generate Postscipt in
    printFilename = \
     tkFileDialog.asksaveasfilename(filetypes=[("Postscript files", "*.ps")])

    # boundingbox of all objects on canvas
    bbox=self.drawarea.bbox(ALL)    

    # generate Postscript
    self.drawarea.postscript(file=printFilename,
                             width=bbox[2]+10, height=bbox[3]+10)
   
# End of file SSGridView.py

