Skip to content
Snippets Groups Projects
ccg_editor.py 49.3 KiB
Newer Older
#!/usr/bin/python

# Author: Ben Wing <ben@666.com>
# Date: April 2006

#############################################################################
#                                                                           #
#                                ccg_editor.ply                             #
#                                                                           #
#   Edit a CCG-format file, graphically.  Will have a mode for displaying   #
#   CCG files in a friendly fashion and allowing for editing of parts or    #
#   all of the file.  Will also have a mode for testing a CCG grammar, and  #
#   allow for compilation and error-finding under control of the editor.    #
#                                                                           #
#############################################################################

# This code is based on PyEdit version 1.1, from Oreilly's Programming
# Python, 2nd Edition, 2001, by Mark Lutz.

from tkinter import *  # base widgets, constants
from tkinter.filedialog import *  # standard dialogs
from tkinter.messagebox import *
from tkinter.simpledialog import *
from tkinter.colorchooser import askcolor
from tkinter.ttk import *
import sys
import os
import hashlib
import ccg2xml
import Tree
import re

START = '1.0'  # index of first char: row=1,col=0
SEL_FIRST = SEL + '.first'  # map sel tag to index
SEL_LAST = SEL + '.last'  # same as 'sel.last'

FontScale = 0                              # use bigger font on linux
if sys.platform[:3] != 'win':              # and other non-windows boxes
    FontScale = 3

# Initial top-level window; it's not clear we need this.
# FIXME: It sucks that we have to call Tk() to get the first top-level window
# but Toplevel() for all others.  We should be able to call Tk() initially,
# and then Toplevel() to create all top-level windows, including the first.
root = None

# List of all open CFile objects
openfiles = {}
filenames = []

def set_ttk_styles():
    sty = Style()
    sty.configure("Main.TFrame", relief=SUNKEN)
    sty = Style()
    sty.configure("Child.TFrame", relief=SUNKEN, border=2)
    sty = Style()
    sty.configure("TBSelected.TButton", relief=SUNKEN)
    sty = Style()
    sty.configure("TestBed.TLabel", relief=SUNKEN, border=1,
                  foreground='#77AA77', font=("Helvetica", FontScale+12))


class CTab(Frame):
    # Initialize this tab.  Usually called from a subclass.  PARENT is
    # the parent widget, CFILE the CFile object associated with the
    # top-level window, and TABNAME is the name of this tab (that tab
    # will be removed from the toolbar).
    def __init__(self, parent, cfile, tabname):
        Frame.__init__(self, parent, style='Main.TFrame')
        self.parent = parent
        self.tabname = tabname
        self.cfile = cfile
        self.checkbar = None
        self.menubar = [
            ('File', 0,
                [('Open...',    0, self.cfile.onOpen),
                 ('New',        0, self.cfile.onNew),
                 ('Save',       0, self.onSave),
                 ('Save As...', 5, self.onSaveAs),
                 ('Close',      0, self.cfile.onClose),
                 'separator',
                 ('Quit VisCCG',    0, self.cfile.onQuit)]
            ),
            ('Tools', 0,
                 [('Font List',   0, self.cfile.onFontList),
                  ('Pick Bg...',  4, self.cfile.onPickBg),
                  ('Pick Fg...',  0, self.cfile.onPickFg),
                  ('Color List',  0, self.cfile.onColorList),
                 'separator',
        self.toolbar = [
            # ('Display',   self.cfile.onDisplay,    {'side': LEFT}),
            ('Edit',   self.cfile.onEdit,       {'side': LEFT}),
            ('Lexicon',   self.cfile.onLexicon,  {'side': LEFT}),
            ('Testbed',   self.cfile.onTestbed,  {'side': LEFT}),
            ('Features',   self.cfile.onFeatures,  {'side': LEFT}),
            ('Words',   self.cfile.onWords,  {'side': LEFT}),
            ('Rules',   self.cfile.onRules,  {'side': LEFT}),
            ('Quit',  self.cfile.onClose,   {'side': RIGHT}),
            ('Help',  self.cfile.help,     {'side': RIGHT}),
            ('Save',  self.onSave,         {'side': RIGHT}),
#        self.remove_toolbar_button(tabname)

    # Add MENU (a tuple corresponding to a single top-level menu item)
    # after the item with the name AFTER.
    def add_menu(self, after, menu):
        newmenu = []
        for x in self.menubar:
            newmenu += [x]
            if x[0] == after:
                newmenu += [menu]
        self.menubar = newmenu

    # Remove the toolbar button named NAME.
    def remove_toolbar_button(self, name):
        newtoolbar = []
        for x in self.toolbar:
            if x[0] != name:
                newtoolbar += [x]
        self.toolbar = newtoolbar

    def reinit(self):
        pass

    #####################
    # File menu commands
    #####################

    def onSave(self):
        self.onSaveAs(self.cfile.currfile)  # may be None

    def onSaveAs(self, forcefile=None):
        file = forcefile or self.cfile.my_asksaveasfilename()
        if file:
            text = self.cfile.getAllText()
            try:
                open(file, 'w').write(text)
            except:
                showerror('CCG Editor', 'Could not write file ' + file)
            else:
                self.cfile.setFileName(file)         # may be newly created
                self.cfile.edit_modified(NO)
        self.cfile.last_save_signature = self.cfile.getSignature(text)


class CEdit(CTab):
    def __init__(self, parent, cfile):
        CTab.__init__(self, parent, cfile, 'Edit')
        self.debugFrame = None

        # Add a frame here, so that debug mode can be enabled
        # by embedding other objects within this frame
        editFrame = Frame(self, style='Child.TFrame')
        editFrame.pack(fill=BOTH, expand=YES, side=TOP)

        # Add a button frame, embed the button and
        # link to command for the debug mode
        btnFrame = Frame(editFrame, style='Child.TFrame')
        btnFrame.grid (row=0, columnspan=3, sticky=NSEW)

        vldButton = Button (btnFrame, text='Validate', command = lambda: self.onValidate(editFrame, cfile))
        vldButton.pack(side=RIGHT)

        # Put the main edit window in the row below this
        vbar = Scrollbar(editFrame)
        hbar = Scrollbar(editFrame, orient='horizontal')
        self.text = Text(editFrame, padx=5, wrap='none', undo=YES)

        vbar.grid(row=1, column=2, sticky=NS)
        hbar.grid(row=2, columnspan=2, sticky=EW)     # pack text last
        self.text.grid(row=1, column=1, sticky=NSEW)  # else sbars clipped

        editFrame.columnconfigure(1, weight=1)
        editFrame.rowconfigure(1, weight=1)
        # Add a list containing line numbers
        self.lineList = Text(editFrame, relief=SUNKEN, bg='white', bd=2, yscrollcommand = vbar.set, width=3)
        self.lineList.grid(row=1, column=0, sticky=NS)
        self.lineList.config(font=self.cfile.fonts[0],
                             bg=self.cfile.colors[0]['bg'], fg=self.cfile.colors[0]['fg'])

        # TODO: The first time the display of the line numbers
        # strangely doesn't go through --- somehow cfile
        # isn't initialized. However, it works properly in the display.
        # Need to understand why this happens.
        try:
            self.showLineNums()
        except KeyError:
            self.text.config(yscrollcommand=vbar.set)    # call vbar.set on text move
            self.text.config(xscrollcommand=hbar.set)
            #vbar.config(command=text.yview)         # call text.yview on scroll move
            hbar.config(command=self.text.xview)         # or hbar['command']=text.xview
            self.text.config(font=self.cfile.fonts[0],
                             bg=self.cfile.colors[0]['bg'], fg=self.cfile.colors[0]['fg'])
        #Setting the movement of the listbox and the text
        #together to be controlled by the scrollbar
        vbar.config(command=self.scrollSet)

        self.add_menu('File',
                      ('Edit', 0,
                          [('Cut',        0, self.onCut),
                           ('Copy',       1, self.onCopy),
                           ('Paste',      0, self.onPaste),
                           'separator',
                           ('Delete',     0, self.onDelete),
                           ('Select All', 0, self.onSelectAll)]
                      ))
        self.add_menu('Edit',
                      ('Search', 0,
                          [('Goto...',    0, self.cfile.onGoto),
                           ('Find...',    0, self.cfile.onFind),
                           ('Refind',     0, self.cfile.onRefind),
                           ('Change...',  0, self.onChange)]
                      ))

    def scrollSet(self, *args):
        self.lineList.yview(*args)
        self.text.yview(*args)

    def reinit(self):
        self.text.focus()

    def showLineNums(self):
        # Make the list of lines editable
        self.lineList.config(state=NORMAL)
        textData = self.cfile.getAllText()
        listOfLines = textData.splitlines()
        for num in range(1,len(listOfLines)):
            self.lineList.insert(END,"%s\n" % num)
        # Now that we are done changing the number of lines,
        # we reset the text to be uneditable
        self.lineList.config(state=NORMAL)

    def onValidate(self, editFrame, cfile):
        # Destroy previous display of debug or error messages
        # if present
        if self.debugFrame:
            self.debugFrame.grid_forget()

        # Compile if file signature has changed
        cfile.compile_if_needed()

        # Now, call the error debug routine if errors are found
        if (ccg2xml.error_count > 0):
            self.debugError(editFrame, cfile)
        else:
            showinfo(title='VisCCG: Success', message='No validation errors!')

    def debugError(self, editFrame, cfile):
        self.debugFrame = Frame(editFrame, bg='white', bd=2)
        self.debugFrame.grid(row=3, columnspan=2, sticky=NSEW)
        # Create Listbox and scrollbars
        sbar = Scrollbar(self.debugFrame)
        list = Listbox(self.debugFrame, relief=SUNKEN, bg='white', bd=2, yscrollcommand = sbar.set)
        sbar.config(command=list.yview)
        list.pack(fill=BOTH, side=LEFT, expand=YES)
        sbar.pack(fill=Y, side=RIGHT)
        # Display each message in the log
        for mesg in ccg2xml.message_log:
            type = mesg[0]
            lineno = mesg[1]
            errwarn = mesg[2]
            if lineno:
                dispError = type+' at Line '+str(lineno)+': '+errwarn
            else:
                dispError = type+': '+errwarn

    #####################
    # Edit menu commands
    #####################

    def onCopy(self):                           # get text selected by mouse,etc
        if not self.text.tag_ranges(SEL):       # save in cross-app clipboard
            showerror('CCG Editor', 'No text selected')
        else:
            text = self.text.get(SEL_FIRST, SEL_LAST)
            self.clipboard_clear()
            self.clipboard_append(text)

    def onDelete(self):                         # delete selected text, no save
        if not self.text.tag_ranges(SEL):
            showerror('CCG Editor', 'No text selected')
        else:
            self.text.delete(SEL_FIRST, SEL_LAST)

    def onCut(self):
        if not self.text.tag_ranges(SEL):
            showerror('CCG Editor', 'No text selected')
            self.onCopy()                       # save and delete selected text
            self.onDelete()

    def onPaste(self):
        try:
            text = self.selection_get(selection='CLIPBOARD')
        except TclError:
            showerror('CCG Editor', 'Nothing to paste')
            return
        self.text.insert(INSERT, text)          # add at current insert cursor
        self.text.tag_remove(SEL, '1.0', END)
        self.text.tag_add(SEL, INSERT+'-%dc' % len(text), INSERT)
        self.text.see(INSERT)                   # select it, so it can be cut

    def onSelectAll(self):
        self.text.tag_add(SEL, '1.0', END+'-1c')   # select entire text
        self.text.mark_set(INSERT, '1.0')          # move insert point to top
        self.text.see(INSERT)                      # scroll to top

    #######################
    # Search menu commands
    #######################
    def onChange(self):
        new = Toplevel(self)
        Label(new, text='Find text:').grid(row=0, column=0)
        Label(new, text='Change to:').grid(row=1, column=0)
        self.change1 = Entry(new)
        self.change2 = Entry(new)
        self.change1.grid(row=0, column=1, sticky=EW)
        self.change2.grid(row=1, column=1, sticky=EW)
               command=self.onDoFind).grid(row=0, column=2, sticky=EW)
               command=self.onDoChange).grid(row=1, column=2, sticky=EW)
        new.columnconfigure(1, weight=1)    # expandable entrys

    def onDoFind(self):
        self.onFind(self.change1.get())                    # Find in change box

    def onDoChange(self):
        if self.text.tag_ranges(SEL):                      # must find first
            self.text.delete(SEL_FIRST, SEL_LAST)          # Apply in change
            self.text.insert(INSERT, self.change2.get())   # deletes if empty
            self.text.see(INSERT)
            self.onFind(self.change1.get())                # goto next appear
            self.text.update()                             # force refresh

    ####################################
    # Others, useful outside this class
    ####################################

    def isEmpty(self):

    def getAllText(self):
        return self.text.get('1.0', END+'-1c')  # extract text as a string

    def setAllText(self, text):
        self.text.delete('1.0', END)            # store text string in widget
        self.text.insert(END, text)             # or '1.0'
        self.text.mark_set(INSERT, '1.0')       # move insert point to top
        self.text.see(INSERT)                   # scroll to top, insert set
        self.cfile.edit_modified(NO)

    def clearAllText(self):
        self.text.delete('1.0', END)            # clear text in widget


class CWords(CTab):
    def __init__(self, parent, cfile):
        CTab.__init__(self, parent, cfile, 'Words')
        self.child=None
        self.wordList = None
        self.cfile = cfile

    # Called when we switch to this mode using the toolbar at top.
    def reinit(self):
        if self.child:
            self.child.pack_forget()

        self.child = Frame(self, style='Child.TFrame')
        self.child.pack(expand=YES, fill=BOTH)

        scrollbar = Scrollbar(self.child, orient=VERTICAL)
        self.wordList = Listbox(self.child, yscrollcommand=scrollbar.set)
        self.wordList.grid(row=0, column=0, sticky=N+S+E+W)
        scrollbar.config(command= self.wordList.yview)
        scrollbar.grid(row=0, column=1, sticky=N+S)
        self.child.grid_rowconfigure(0, weight=1)
        self.child.grid_columnconfigure(0, weight=1)
        #If the data hasn't been compiled yet, then do so
        try:
            dummy = ccg2xml.morph_xml
        except:
            self.cfile.compile_if_needed()
        #Adding dummy code for all words
        for x in ccg2xml.morph_xml:
            assert x[0] == 'entry'
            self.wordList.insert (END, ccg2xml.getprop('word', x[1]))
            # print(ccg2xml.getprop('word', x[1]))

class CLexicon(CTab):
    class lexicon_vars(object):
        def __init__(self):
            self.show_feat_id = IntVar()
            self.show_feat_id.set(1)
            self.show_feat_struct = IntVar()
            self.show_feat_struct.set(1)
            self.show_full_features = IntVar()
            self.show_full_features.set(0)
            self.show_semantics = IntVar()
            self.show_semantics.set(1)

    def __init__(self, parent, cfile):
        CTab.__init__(self, parent, cfile, 'Lexicon')
        self.child = None
        self.cnv = None
        self.mainFrame = None

        self.vars = self.lexicon_vars()
        # FIXME?  It's a bit awkward that ccg.ply has references to the
        # variables below scattered throughout it.  But I'm not sure what
        # a better solution would be.
        self.checkbar = [
            ("Show feature ID's", self.vars.show_feat_id),
            ("Show features", self.vars.show_feat_struct),
            ('Full-form features', self.vars.show_full_features),
            ('Show semantics', self.vars.show_semantics),

    # Called when we switch to this mode using the toolbar at top.
    def reinit(self):
        self.redraw()

    def redraw(self):
        self.cfile.compile_if_needed()
        if self.child:
            self.child.pack_forget()
        if self.mainFrame:
            self.mainFrame.pack_forget()
        self.mainFrame = Frame(self, style='Main.TFrame')
        self.mainFrame.pack_propagate(0)
        self.mainFrame.pack(expand=YES, fill=BOTH)
        self.mainFrame.grid_rowconfigure(0, weight=1)
        self.mainFrame.grid_columnconfigure(0, weight=1)
        xscrollbar = Scrollbar(self.mainFrame, orient=HORIZONTAL)
        xscrollbar.grid(row=1, column=0, sticky=E+W)
        yscrollbar = Scrollbar(self.mainFrame)
        yscrollbar.grid(row=0, column=1, sticky=N+S)
        self.cnv = Canvas(self.mainFrame, bd=2, xscrollcommand=xscrollbar.set,
                          yscrollcommand=yscrollbar.set, width = 847, height=369)
        xscrollbar.config(command= self.cnv.xview)
        yscrollbar.config(command= self.cnv.yview)
        self.child = Frame(self.cnv, style='Child.TFrame')
        self.cnv.create_window(0, 0, anchor='nw', window=self.child)
        ccg2xml.draw_parse(self.cfile.curparse.parse, self.cfile, self.child, self.vars, self.cnv, self.mainFrame)
        self.cnv.config(scrollregion=self.cnv.bbox("all"))
        self.cnv.grid(row=0, column=0, sticky='NSEW')


class CRules(CTab):
    def __init__(self, parent, cfile):
        CTab.__init__(self, parent, cfile, 'Rules')

class CFeatures(CTab):
    def __init__(self, parent, cfile):
        CTab.__init__(self, parent, cfile, 'Features')
        self.child=None
        self.checkbar=None
        self.edit=None
        self.text=None

    # Called when we switch to this mode using the toolbar at top.
    def reinit(self):
        if self.child:
            self.child.pack_forget()

        self.child = Frame(self, style='Child.TFrame', width=847, height=369)
        self.child.pack(expand=YES, fill=BOTH)
        butframe = Frame(self.child, cursor='hand2', style='Child.TFrame')
        butframe.pack(fill=X)
        but1 = Button(butframe, text='Expand All', command=self.expand_all)
        but1.pack(side=LEFT)
        but2 = Button(butframe, text='Contract All', command=self.contract_all)
        but2.pack(side=LEFT)
        # Force editing in the same frame: but a lower view:
        # pass self.child as the parent frame
        self.edit = Button(butframe, text='Edit', command=lambda:self.edit_tree(self.child))
        self.edit.pack(side=RIGHT)
        featframe = Frame(self.child, style='Child.TFrame')
        featframe.pack(expand=YES, fill=BOTH)
        self.cfile.compile_if_needed()

        # Build the tree
        self.tree={}
        self.root_name = re.sub(r'^(.*)\.(.*)$', r'\1', self.cfile.file)
        self.tree[self.root_name]=[]
        for feat in self.cfile.curparse.feature_to_values:
            self.tree[self.root_name] += [str(feat)]
        for feat in self.cfile.curparse.feature_to_values:
            self.tree[feat] = []

            for x in self.cfile.curparse.feature_to_values[feat]:
                if x.name not in self.tree:
                    self.tree[x.name] = []

            for x in self.cfile.curparse.feature_to_values[feat]:
                if x.parents:
                    par = x.parents[0]
                    self.tree[par.name] += [x.name]
                else:
                    self.tree[feat] += [x.name]

        # Define the images for opened and closed categories
        shut_icon=PhotoImage(data='R0lGODlhCQAQAJH/AMDAwAAAAGnD/wAAACH5BAEAAAAALAAA'
                             'AAAJABAAQAIdhI8hu2EqXIroyQrb\nyRf0VG0UxnSZ5jFjulrhaxQ'
                             'AO6olVwAAOw==')
        open_icon=PhotoImage(data='R0lGODlhEAAJAJH/AMDAwAAAAGnD/wAAACH5BAEAAAAALAAA'
                             'AAAQAAkAQAIahI+pyyEPg3KwPrko\nTqH7/yGUJWxcZTapUQAAO8b'
                             'yUgAAOw==')

        # Create the tree
        self.t=Tree.Tree(master=featframe,
                         root_id='',
                         root_label=self.root_name,
                         collapsed_icon=shut_icon,
                         expanded_icon=open_icon,
                         get_contents_callback=self.get_treedata,
                         line_flag=False)

        self.t.grid(row=0, column=0, sticky = 'nsew')

        featframe.grid_rowconfigure(0, weight=1)
        featframe.grid_columnconfigure(0, weight=1)

        sb=Scrollbar(featframe)
        sb.grid(row=0, column=1, sticky='ns')
        self.t.configure(yscrollcommand=sb.set)
        sb.configure(command=self.t.yview)

        sb=Scrollbar(featframe, orient=HORIZONTAL)
        sb.grid(row=1, column=0, sticky='ew')
        self.t.configure(xscrollcommand=sb.set)
        sb.configure(command=self.t.xview)

        # Expand the whole tree out
        self.expand_tree(self.t.root)

    # Returns the nodes rooted at the node passed and adds them to the tree
    def get_treedata(self,node):
        lbl = str(node.get_label())
        children = self.tree[lbl]
        for x in children:
            if self.tree[x]:
                expands=1
            else:
                expands=0
        self.t.add_node(name=x,flag=expands)

    # Expand the tree rooted at node recursively
    def expand_tree(self, node):
        node.expand()
        for child in node.children():
            if child.expandable():
                self.expand_tree(child)

    def expand_all(self):
        self.expand_tree(self.t.root)

    def contract_all(self):
        self.t.root.collapse()

    def edit_tree(self, parent):
        editFrame = Frame(parent, style='Main.TFrame')


        self.text = Text(editFrame, padx=5, wrap=None, undo = YES, background='white')
        vbar = Scrollbar(editFrame)
        hbar = Scrollbar(editFrame, orient='horizontal')

        self.text.config(yscrollcommand=vbar.set)    # call vbar.set on text move
        self.text.config(xscrollcommand=hbar.set)
        vbar.config(command=self.text.yview)         # call text.yview on scroll move
        hbar.config(command=self.text.xview)         # or hbar['command']=text.xview

        # Change the text on the button, and also pass the rest
        # of the arguments so that the grid for the statements can be reset
        self.edit.config(text='Done', command= lambda:self.save_tree(parent))

        # Changing the mode of the cfile object here,
        # so that once the user clicks done,
        # the whole object is recompiled and redisplayed
        self.cfile.mode= 'Edit'

        vbar.pack(side=RIGHT, fill=Y)
        hbar.pack(side=BOTTOM, fill=X)
        self.text.pack(fill= BOTH, expand= YES)

        # Set a mark at the beginning of the text
        self.text.mark_set("START", INSERT)
        self.text.mark_gravity("START", LEFT)

        # Push in the rest of the file's contents
        fileData = self.cfile.getAllText()
        self.text.insert(INSERT, fileData)

        # Move the insert position to the first occurence of the family name
        # FIXME: this is poor implementation
        # The positioning of the insert cursor should be happening by parsing the
        # CFG production rules, using CSFamily.prod.lineno and endlineno
        self.text.config(takefocus=True)
        idx= self.text.search('feature', "START")
        if idx:
            self.text.mark_set(CURRENT, idx)
            self.text.see(CURRENT)
        else:
            showwarning('Warning','Features not located in text')

        editFrame.pack(expand=YES, fill=BOTH)

    def save_tree(self, parent):
        # We force the text contents of the cfile object to copy over
        # all that is presently in the current text-box
        self.cfile.setAllText(self.text.get(1.0,END))
        self.edit.config(text='Edit', command= lambda:self.edit_tree(parent))

        # Recompile whatever was edited and redisplay
        # Note: changes are not saved hereby!!
        self.cfile.compile_if_needed()
        self.cfile.onFeatures()
class CTestbed(CTab):
    def __init__(self, parent, cfile):
        CTab.__init__(self, parent, cfile, 'Testbed')
        self.child = None
        self.edit = None
        self.text = None
        self.editFrame = None
        self.cnv = None
        self.mainFrame = None
        self.newInsert = None
    def makelab(self, text, row, col, style_name=None, **kwargs):
        lab = Label(self.child, text=text, style=(style_name or ''), **kwargs)
        # Make the label grow to fill all space allocated for the column
        lab.grid(row=row, column=col, sticky='NSEW')

    # Called when we switch to this mode using the toolbar at top.
    def reinit(self):
        if self.child:
            self.child.pack_forget()
        if self.mainFrame:
            self.mainFrame.pack_forget()
        self.mainFrame = Frame(self, style='Main.TFrame')
        self.mainFrame.pack(expand=YES, fill=BOTH)
        self.mainFrame.grid_rowconfigure(0, weight=1)
        self.mainFrame.grid_columnconfigure(0, weight=1)
        xscrollbar = Scrollbar(self.mainFrame, orient=HORIZONTAL)
        xscrollbar.grid(row=1, column=0, sticky=E+W)
        yscrollbar = Scrollbar(self.mainFrame)
        yscrollbar.grid(row=0, column=1, sticky=N+S)
        self.cnv= Canvas(self.mainFrame, bd=2, xscrollcommand=xscrollbar.set,
                         yscrollcommand=yscrollbar.set, width = 847, height=369)
        xscrollbar.config(command=self.cnv.xview)
        yscrollbar.config(command=self.cnv.yview)
        self.child = Frame(self.cnv, style='Child.TFrame')
        self.child.rowconfigure(1, weight=1)
        self.child.columnconfigure(1, weight=1)
        self.child.pack(expand=YES, fill=BOTH)

        butnFrame = Frame(self.child, style='Child.TFrame')
        butnFrame.grid(row=0, sticky='NSEW', columnspan=2)
        self.edit = Button(butnFrame, text='Edit', command= self.edit_testbed)
        self.edit.pack(side=RIGHT)
        self.newInsert = Button(butnFrame, text='New Sentence', command= self.new_sentence)
        self.newInsert.pack(side=RIGHT)

        self.cfile.compile_if_needed()

        self.makelab("Num Parses", 1, 0, 'TestBed.TLabel')
        self.makelab("Sentence", 1, 1, 'TestBed.TLabel')

        # Make the column containing the sentences grow to include all
        # extra space
        self.child.columnconfigure(1, weight=1)
        for i in range(len(self.cfile.curparse.testbed_statements)):
            x = self.cfile.curparse.testbed_statements[i]
            assert x[0] == 'item'
            x = x[1]
            # Left-justify the text
            numparse = ccg2xml.getprop('numOfParses', x)
            string = ccg2xml.getprop('string', x)

        # How many parses of the sentence are produced?
        self.makelab('%s' % numparse, i+2, 0)
        # Print the sentence itself
        self.makelab('%s%s' % (numparse == 0 and '*' or '', string),
                     i+2, 1, anchor=W)
        self.cnv.create_window(0, 0, anchor='nw', window=self.child)
        self.child.update_idletasks()
        #self.child.grid(row=0, column=0, sticky=NSEW)
        self.cnv.config(scrollregion=self.cnv.bbox("all"))
        self.cnv.grid(row=0, column=0, sticky='NSEW')


    # Edit the testbed
    def edit_testbed(self):
        self.editFrame = Frame(self.mainFrame, style='Main.TFrame')
        #self.editFrame.grid(row=len(self.cfile.curparse.testbed_statements)+3, columnspan=2, sticky='NSEW')
        self.editFrame.grid(row=2, columnspan=2, sticky='NSEW')
        self.text = Text(self.editFrame, padx=5, wrap=None, undo=YES, background='white')
        vbar = Scrollbar(self.editFrame)
        hbar = Scrollbar(self.editFrame, orient='horizontal')

        self.text.config(yscrollcommand=vbar.set)    # call vbar.set on text move
        self.text.config(xscrollcommand=hbar.set)
        vbar.config(command=self.text.yview)         # call text.yview on scroll move
        hbar.config(command=self.text.xview)         # or hbar['command']=text.xview

        # Change the text on the button, and also pass the rest
        # of the arguments so that the grid for the statements can be reset
        self.edit.config(text='Done', command=self.save_testbed)
        # Changing the mode of the cfile object here,
        # so that once the user clicks done,
        # the whole object is recompiled and redisplayed
        self.cfile.mode = 'Edit'

        vbar.pack(side=RIGHT, fill=Y)
        hbar.pack(side=BOTTOM, fill=X)
        self.text.pack(fill= BOTH, expand= YES)

        # Set a mark at the beginning of the text
        self.text.mark_set("START", INSERT)
        self.text.mark_gravity("START", LEFT)

        # Push in the rest of the file's contents
        fileData = self.cfile.getAllText()
        self.text.insert(INSERT, fileData)

        # Move the insert position to the first occurence of the family name
        # FIXME: this is poor implementation
        # The positioning of the insert cursor should be happening by parsing the
        # CFG production rules, using CSFamily.prod.lineno and endlineno
        self.text.config(takefocus=True)
        idx = self.text.search('testbed', "START")
        if idx:
            self.text.mark_set(CURRENT, idx)
            self.text.see(CURRENT)
        else:
            showwarning(title= 'VisCCG: Warning', message='No initial testbed found')
        #self.editFrame.pack(expand=YES, fill=BOTH)
        self.child.update_idletasks()
        self.cnv.config(scrollregion=self.cnv.bbox("all"))

    # Save the edited text
    def save_testbed(self):
        # We force the text contents of the cfile object to copy over
        # all that is presently in the current text-box
        self.cfile.setAllText(self.text.get(1.0, END))
        self.edit.config(text='Edit', command=self.edit_testbed)

        # Recompile whatever was edited and redisplay
        # Note: changes are not saved hereby!!
        self.cfile.compile_if_needed()
        self.cfile.onTestbed()

    # Enter a new sentence
    def new_sentence(self):
        master = Tk()
        master.title('VisCCG: New Sentence for the testbed')
        sent = Entry(master, width=100)
        nParses = Entry(master, width=2)
        sLabel = Label(master, text='Sentence:')
        nLabel = Label(master, text='Number of parses:')

        sent.focus_set()

        b = Button(master, text="Add sentence", width=10, command= lambda:self.editNew(master, sent, nParses))
        c = Button(master, text="Cancel", command= master.destroy)

        sent.grid (row=1, column=0, sticky = W)
        nParses.grid (row=1, column=1, sticky= W)
        sLabel.grid (row=0, column=0, sticky=W)
        nLabel.grid (row=0, column=1, sticky = W)
        b.grid (row=2, column = 0)
        c.grid (row=2, column = 1)

    # Print from the new sentence
    def editNew(self, master, sent, nParses):
        # Prepare the file's contents for editing
        fileData = self.cfile.getAllText()

        self.text.mark_set("START", INSERT)
        self.text.mark_gravity("START", LEFT)
        self.text.insert(INSERT, fileData)

        testSent = sent.get()
        npSent = nParses.get()

        self.text.config(takefocus=True)
        idx= self.text.search('testbed', "START")
        if idx:
            self.text.mark_set("START", idx)
            idx = self.text.search('{', "START", forwards = True)
            self.text.mark_set("START", idx)
            idx = self.text.search('\n', "START", forwards = True)
            # FIXME: really poor search for locating the right position
            # to insert text here. Needs correction!
            self.text.mark_set(INSERT, idx)
            self.text.mark_gravity(INSERT, RIGHT)

            self.text.insert (INSERT, '\n\t'+ testSent+ ':\t'+  npSent+ ';')

        else:
            showwarning(title= 'VisCCG: Warning', message='No initial testbed found, creating new')
            self.text.mark_set(INSERT, END)
            self.text.mark_gravity(INSERT, RIGHT)

            self.text.insert (INSERT, ' testbed {\n')
            self.text.insert (INSERT, '\n\t'+ testSent+ ':\t'+  npSent+ ';')
            self.text.insert (INSERT, '}\n')


        # Set the original file's data to be this
        fileData= self.text.get(1.0, END)
        self.cfile.setAllText(fileData)

        # Destroy the entry window
        master.destroy()

        # Update the display
        self.cfile.mode= 'Edit'
        self.cfile.compile_if_needed()
        self.cfile.onTestbed()

# Creates the top-level window and populates the widgets below it.
    #### NOTE NOTE NOTE! Variables declared like this, in the class itself,
    #### are class variables (not instance variables) until they are
    #### assigned to.  If you want pure instance variables, you need to
    #### initialize them inside of __init__().

    # Hash table describing modes and the associated class
    modelist = {'Edit':CEdit, 'Lexicon':CLexicon, 'Features':CFeatures,
                'Words':CWords, 'Testbed':CTestbed, 'Rules':CRules}

    startfiledir = '.'
    ftypes = [('All files',     '*'),                 # for file open dialog
              ('Text files',   '.txt'),               # customize in subclass
              ('Python files', '.py')]                # or set in each instance

    colors = [{'fg':'black',      'bg':'white'},      # color pick list
              {'fg':'yellow',     'bg':'black'},      # first item is default
              {'fg':'white',      'bg':'blue'},       # tailor me as desired
              {'fg':'black',      'bg':'beige'},      # or do PickBg/Fg chooser
              {'fg':'yellow',     'bg':'purple'},
              {'fg':'black',      'bg':'brown'},
              {'fg':'lightgreen', 'bg':'darkgreen'},
              {'fg':'darkblue',   'bg':'orange'},
              {'fg':'orange',     'bg':'darkblue'}]

    fonts  = [('courier',    9+FontScale, 'normal'),  # platform-neutral fonts
              ('courier',   12+FontScale, 'normal'),  # (family, size, style)
              ('courier',   10+FontScale, 'bold'),    # or popup a listbox
              ('courier',   10+FontScale, 'italic'),  # make bigger on linux
              ('times',     10+FontScale, 'normal'),
              ('helvetica', 10+FontScale, 'normal'),
              ('ariel',     10+FontScale, 'normal'),
              ('system',    10+FontScale, 'normal'),
              ('courier',   20+FontScale, 'normal')]

    def __init__(self, file=None, parent=None):
        self.file = file

        self.openDialog = None
        self.saveDialog = None
        self.lastfind   = None
        self.current_parse = None
        self.mode = None
        self.last_save_signature = None
        self.last_compile_signature = None
        self.top = parent or Toplevel(root)

        ccg2xml.late_init_graphics()
        openfiles[self] = True
        self.top.protocol('WM_DELETE_WINDOW', self.onClose)

        # We create an outer frame to hold the toolbar and the main widget.
        # Create all the different kinds of main widget.
        # FIXME: Maybe outer isn't necessary?
        self.outer = Frame(self.top)
        self.outer.pack(expand=YES, fill=BOTH)  # make frame stretchable
        self.modes = {}
        for mode in self.modelist:
            self.modes[mode] = self.modelist[mode](self.outer, self)
        self.main = None
        self.toolbar_widget = None
        self.checkbar_widget = None
        #self.switch_to('Edit')
        self.setFileName(None)
        if file:
                self.onFirstOpen(file)
        else:
            # When the user has just opened a new file
            # Need to load template from the src folder
            openccg_home = os.environ['OPENCCG_HOME']
            template = open(openccg_home + '/src/ccg2xml/grammar_template.ccg', 'r').read()
            self.setAllText(template)

        # Save the MD5 signature for future comparison
        self.last_save_signature = self.getSignature(self.getAllText())
        self.switch_to('Edit')

    def switch_to(self, mode):
        # Switch to a different mode (display, edit, test).  Remove the
        # existing main and toolbar widgets, if existing.  Redo the menubar
        # and toolbar widgets according to the new mode and then display
        # the new widgets.
        #
        # FIXME: We should probably create the menubar and toolbar widgets
        # only once, and remember them.
        if self.mode != mode:
            if self.main:
                self.main.pack_forget()
            if self.toolbar_widget:
                self.toolbar_widget.pack_forget()
            if self.checkbar_widget:
                self.checkbar_widget.pack_forget()
            self.mode = mode
            self.main = self.modes[mode]
            self.makeMenubar()
            self.makeToolbar(mode)
            self.makeCheckbar()
            # print("Reinit being called now...")
            self.main.reinit()
            # Pack the main widget after the toolbar, so it goes below it.
            self.main.pack(side=TOP, expand=YES, fill=BOTH)

    # Create the menubar; assumes that self.menubar has been set to the
    # appropriate menubar description.  Note that the menubar has to be a
    # child of the top-level window itself rather than any child of it, so
    # that it can be correctly displayed at the top of the window -- or
    # possibly in its decoration (Windows) or at top of screen (Mac).
    #
    # From PP2E guimaker.py.
    def makeMenubar(self):
        menubar = Menu(self.top)
        self.top.config(menu=menubar)

        for (name, key, items) in self.main.menubar:
            pulldown = Menu(menubar)
            self.addMenuItems(pulldown, items)
            menubar.add_cascade(label=name, underline=key, menu=pulldown)

        if sys.platform[:3] == 'win':
            menubar.add_command(label='Help', command=self.help)
        else:
            pulldown = Menu(menubar)  # linux needs real pulldown
            pulldown.add_command(label='About', command=self.help)
            menubar.add_cascade(label='Help', menu=pulldown)