# This file is part of the source code of
# gradint v0.99851 (c) 2002-2013 Silas S. Brown. GPL v3+.
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 3 of the License, or
#    (at your option) any later version.
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.

# Start of frontend.py - Tk and other front-ends

def interrupt_instructions():
    if soundCollector or app or appuifw or android: return ""
    elif msvcrt: return "\nPress Space if you have to interrupt the lesson."
    elif riscos_sound: return "\nLesson interruption not yet implemented on RISC OS.  If you stop the program before the end of the lesson, your progress will be lost.  Sorry about that."
    elif winCEsound: return "\nLesson interruption not implemented on\nWinCE without GUI.  Can't stop, sorry!"
    elif macsound: return "\nPress Ctrl-C if you have to interrupt the lesson."
    else: return "\nPress Control-C if you have to interrupt the lesson."

appTitle += time.strftime(" %A") # in case leave 2+ instances on the desktop
def waitOnMessage(msg):
    global warnings_printed
    if type(msg)==type(u""): msg2=msg.encode("utf-8")
    else: msg2=msg
    if appuifw:
        t=appuifw.Text() ; t.add(u"".join(warnings_printed)+msg) ; appuifw.app.body = t # in case won't fit in the query()  (and don't use note() because it doesn't wait)
        appuifw.query(u""+msg,'query')
    elif android:
        # android.notify("Gradint","".join(warnings_printed)+msg) # doesn't work?
        android.dialogCreateAlert("Gradint","".join(warnings_printed)+msg)
        android.dialogSetPositiveButtonText("OK")
        android.dialogShow() ; android.dialogGetResponse()
    elif app:
        if not (winsound or winCEsound or mingw32 or cygwin): show_info(msg2+"\n\nWaiting for you to press OK on the message box... ",True) # in case terminal is in front
        app.todo.alert = "".join(warnings_printed)+msg
        while True:
            try:
              if not hasattr(app.todo,"alert"): break
            except: break # app destroyed
            time.sleep(0.5)
        if not (winsound or winCEsound or mingw32 or cygwin): show_info("OK\n",True)
    else:
        if clearScreen(): msg2 = "This is "+program_name.replace("(c)","\n(c)")+"\n\n"+msg2 # clear screen is less confusing for beginners, but NB it may not happen if warnings etc
        show_info(msg2+"\n\n"+cond(winCEsound,"Press OK to continue\n","Press Enter to continue\n"))
        sys.stderr.flush() # hack because some systems don't do it (e.g. some mingw32 builds), and we don't want the user to fail to see why the program is waiting (especially when there's an error)
        try:
            raw_input(cond(winCEsound,"See message under this window.","")) # (WinCE uses boxes for raw_input so may need to repeat the message - but can't because the prompt is size-limited, so need to say look under the window)
            clearScreen() # less confusing for beginners
        except EOFError: show_info("EOF on input - continuing\n")
    warnings_printed = []

def getYN(msg,defaultIfEof="n"):
    if appuifw:
        appuifw.app.body = None
        return appuifw.query(u""+msg,'query')
    elif android:
        android.dialogCreateAlert("Gradint",msg)
        android.dialogSetPositiveButtonText("Yes") # TODO do we have to localise this ourselves or can we have a platform default?
        android.dialogSetNegativeButtonText("No")
        android.dialogShow()
        return android.dialogGetResponse().result['which'] == 'positive'
    elif app:
        app.todo.question = localise(msg)
        while app and not hasattr(app,"answer_given"): time.sleep(0.5)
        if not app: raise SystemExit
        ans = app.answer_given
        del app.answer_given
        return ans
    else:
        ans=None
        clearScreen() # less confusing for beginners
        while not ans=='y' and not ans=='n':
            try: ans = raw_input("%s\nPress y for yes, or n for no.  Then press Enter.  --> " % (msg,))
            except EOFError:
                ans=defaultIfEof ; print ans
        clearScreen() # less confusing for beginners
        if ans=='y': return 1
        return 0

def primitive_synthloop():
    global justSynthesize,warnings_printed
    lang = None
    interactive = appuifw or winCEsound or android or not hasattr(sys.stdin,"isatty") or sys.stdin.isatty()
    if interactive: interactive=cond(winCEsound and warnings_printed,"(see warnings under this window) Say:","Say: ") # (WinCE uses an input box so need to repeat the warnings if any - but can't because prompt is size-limited, so need to say move the window.)
    else: interactive="" # no prompt on the raw_input (we might be doing outputFile="-" as well)
    while True:
        old_js = justSynthesize
        if appuifw:
            if not justSynthesize: justSynthesize=""
            justSynthesize=appuifw.query(u"Say:","text",u""+justSynthesize)
            if justSynthesize: justSynthesize=justSynthesize.encode("utf-8")
            else: break
        else:
            if android:
              justSynthesize = android.dialogGetInput("Gradint",interactive).result
              if type(justSynthesize)==type(u""): justSynthesize=justSynthesize.encode("utf-8")
            else:
              try: justSynthesize=raw_input(interactive)
              except EOFError: break
            if (winCEsound or riscos_sound or android) and not justSynthesize: break # because no way to send EOF (and we won't be taking i/p from a file)
            if interactive and not readline:
              interactive="('a' for again) Say: "
              if justSynthesize=="a": justSynthesize=old_js
        oldLang = lang
        if justSynthesize: lang = just_synthesize(interactive,lang)
        # and see if it transliterates:
        if justSynthesize and lang and not "#" in justSynthesize:
            if justSynthesize.startswith(lang+" "):
                t = transliterates_differently(justSynthesize[len(lang+" "):],lang)
                if t: t=lang+" "+t
            else: t = transliterates_differently(justSynthesize,lang)
            if t:
                if appuifw: justSynthesize = t
                else: show_info("Spoken as "+t+"\n")
        if warnings_printed: # at end not beginning, because don't want to overwrite the info message if appuifw
            if appuifw:
                t=appuifw.Text()
                t.add(u"".join(warnings_printed))
                appuifw.app.body = t
            elif android: waitOnMessage("") # (makeToast doesn't stay around for very long)
            # else they'll have already been printed
            warnings_printed = []
        if not lang: lang=oldLang

if android:
  if not isDirectory("/mnt/sdcard/svox") and not isDirectory("/system/tts/lang_pico"): waitOnMessage("English voice might not be installed. Check under Home > Menu > Settings > Voice output > text to speech > Pico > English")

def startBrowser(url): # true if success
  if winCEsound: return None # user might be paying per byte! + difficult to switch back if no Alt-Tab program
  try:
      import webbrowser
      g=webbrowser.get()
  except: g=0
  if g and (winCEsound or macsound or (hasattr(g,"background") and g.background) or (hasattr(webbrowser,"BackgroundBrowser") and g.__class__==webbrowser.BackgroundBrowser) or (hasattr(webbrowser,"Konqueror") and g.__class__==webbrowser.Konqueror)):
      return g.open_new(url)
  # else don't risk it - it might be text-mode and unsuitable for multitask-with-gradint
  if winsound: return not os.system('start "%ProgramFiles%\\Internet Explorer\\iexplore.exe" '+url) # use os.system not system here (don't know why but system() doesn't always work for IE)
  # (NB DON'T replace % with %%, it doesn't work. just hope nobody set an environment variable to any hex code we're using in mp3web)

def clearScreen():
    global warnings_printed
    if not (winsound or mingw32 or unix): return # can't do it anyway
    if warnings_printed:
        # don't do it this time (had warnings etc)
        warnings_printed = []
        return
    if winsound or mingw32: os.system("cls")
    else: os.system("clear 1>&2") # (1>&2 in case using stdout for something else)
    return True

cancelledFiles = []
def handleInterrupt(): # called only if there was an interrupt while the runner was running (interrupts in setup etc are propagated back to mainmenu/exit instead, because lesson state could be anything)
    needCountItems = 0
    if saveProgress:
        if dbase and not dbase.saved_completely:
            show_info("Calculating partial progress... ") # (normally quite quick but might not be on PDAs etc, + we want this written if not app)
            needCountItems = 1 # used if not app
        elif dbase and not app: show_info("Interrupted on not-first-time; no need to save partial progress\n")
    # DON'T init cancelledFiles to empty - there may have been other missed events.
    while copy_of_runner_events:
        cancelledEvent = copy_of_runner_events[0][0]
        try: runner.cancel(copy_of_runner_events[0][1])
        except: pass # wasn't in the queue - must have slipped out
        del copy_of_runner_events[0]
        # cancelledEvent = runner.queue[0][-1][0] worked in python 2.3, but sched implementation seems to have changed in python 2.5 so we're using copy_of_runner_events instead
        if hasattr(cancelledEvent,"wordToCancel") and cancelledEvent.wordToCancel: cancelledFiles.append(cancelledEvent.wordToCancel)
    if not app and needCountItems and cancelledFiles: show_info("(%d cancelled items)...\n" % len(cancelledFiles))
    global repeatMode ; repeatMode = 0 # so Ctrl-C on justSynth-with-R works

tkNumWordsToShow = 10 # the default number of list-box items

def addStatus(widget,status,mouseOnly=0):
    # Be VERY CAREFUL with status line changes.  Don't do it on things that are focused by default (except with mouseOnly=1).  Don't do it when the default status line might be the widest thing (i.e. when list box is not displayed) or window size could jump about too much.  And in any event don't use lines longer than about 53 characters (the approx default width of the listbox when using monospace fonts).
    # (NB addStatus now takes effect only when the list box is displayed anyway, so OK for buttons that might also be displayed without it)
    widget.bind('<Enter>',lambda *args:app.set_statusline(status))
    widget.bind('<Leave>',app.restore_statusline)
    if not mouseOnly:
        widget.bind('<FocusIn>',lambda *args:app.set_statusline(status))
        widget.bind('<FocusOut>',app.restore_statusline)
def makeButton(parent,text,command):
    button = Tkinter.Button(parent)
    button["text"] = text
    button["command"] = command
    button.bind('<Return>',command) # so can Tab through them
    button.bind('<ButtonRelease-3>', app.wrongMouseButton)
    bindUpDown(button,True)
    return button
def addButton(parent,text,command,packing=None,status=None):
    button = makeButton(parent,text,command)
    if status: addStatus(button,status)
    if packing=="nopack": pass
    elif type(packing)==type(""): button.pack(side=packing)
    elif packing: button.pack(packing)
    else: button.pack()
    return button
def addLabel(row,label):
    label = Tkinter.Label(row,text=label)
    label.pack({"side":"left"})
    return label
def CXVMenu(e): # callback for right-click
    e.widget.focus()
    m=Tkinter.Menu(None, tearoff=0, takefocus=0)
    if macsound:
        cut,copy,paste = "<<Cut>>","<<Copy>>","<<Paste>>"
    else:
        ctrl="<Control-"
        cut,copy,paste = ctrl+'x>',ctrl+'c>',ctrl+'v>'
    def evgen(e,cmd): e.widget.event_generate(cmd)
    funclist = [("Paste",paste),("Delete",'<Delete>')]
    if not macsound:
        funclist = [("Cut",cut),("Copy",copy)]+funclist # doesn't work reliably on Mac Tk
    for l,cmd in funclist: m.add_command(label=l,command=(lambda e=e,c=cmd: e.widget.after(10,evgen,e,c)))
    m.add_command(label="Select All",command=(lambda e=e: e.widget.after(10,selectAll,e)))
    m.tk_popup(e.x_root-3, e.y_root+3,entry="0")
def selectAll(e):
    e.widget.event_generate('<Home>')
    e.widget.event_generate('<Shift-End>')
def selectAllButNumber(e): # hack for recording.py - select all but any number at the start
    e.widget.event_generate('<Home>')
    for i in list(e.widget.theText.get()):
        if "0"<=i<="9" or i=="_": e.widget.event_generate('<Right>')
        else: return e.widget.event_generate('<Shift-End>')
def addTextBox(row,wide=0):
    text = Tkinter.StringVar(row)
    entry = Tkinter.Entry(row,textvariable=text)
    entry.bind('<ButtonRelease-3>',CXVMenu)
    if macsound:
        entry.bind('<Control-ButtonRelease-1>',CXVMenu)
        entry.bind('<ButtonRelease-2>',CXVMenu)
    if winCEsound:
      if WMstandard: # non-numeric inputs no good on WMstandard Tkinter
        def doRawInput(text,entry):
            app.input_to_set = text
            app.menu_response="input"
        entry.bind('<Return>',lambda e:doRawInput(text,entry))
        if wide: # put help in 1st wide textbox
          global had_doRawInput
          try: had_doRawInput
          except:
            had_doRawInput=1
            text.set("(Push OK to type A-Z)") # (if changing this message, change it below too)
            class E: pass
            e=E() ; e.widget = entry
            entry.after(10,lambda *args:selectAll(e))
      else: # PocketPC: try to detect long clicks. This is awkward. time.time is probably 1sec resolution so will get false +ves if go by that only.
        def timeStamp(entry): entry.buttonPressTime=time.time()
        entry.bind('<ButtonPress-1>',lambda e:timeStamp(entry))
        global lastDblclkAdvisory,lastDblclk
        lastDblclkAdvisory=lastDblclk=0
        def pasteInstructions(t):
            if t>=0.5: # they probably want tap-and-hold, which we don't do properly
                global lastDblclkAdvisory
                if t<2 and (lastDblclkAdvisory>time.time()-30 or lastDblclk>time.time()-90): return # reduce repeated false +ves
                lastDblclkAdvisory=time.time()
                app.todo.alert="Double-click in the box if you want to replace it with the clipboard contents"
        def doPaste(text,entry):
            text.set(entry.selection_get(selection="CLIPBOARD"))
            global lastDblclk ; lastDblclk=time.time()
        entry.bind('<ButtonRelease-1>',lambda e:pasteInstructions(time.time()-getattr(entry,"buttonPressTime",time.time())))
        entry.bind('<Double-Button-1>',lambda e:doPaste(text,entry))
    # Tkinter bug workaround (some versions): event_generate from within a key event handler can be unreliable, so the Ctrl-A handler delays selectAll by 10ms:
    entry.bind(cond(macsound,'<Command-a>','<Control-a>'),(lambda e:e.widget.after(10,lambda e=e:selectAll(e))))
    bindUpDown(entry,False)
    if wide=="nopack": pass
    elif wide:
        if winCEsound or olpc: entry["width"]=1 # so it will squash down rather than push off-screen any controls to the right (but DON'T do this on other platforms, where we want the window to expand in that case, e.g. when there are cache controls)
        entry.pack(side="left",fill=Tkinter.X,expand=1)
    else: entry.pack({"side":"left"})
    return text,entry
def bindUpDown(o,alsoLeftRight=False): # bind the up and down arrows to do shift-tab and tab (may be easier for some users, especially on devices where tab is awkward)
    tab=(lambda e:e.widget.after(10,lambda e=e:e.widget.event_generate('<Tab>')))
    shTab=(lambda e:e.widget.after(10,lambda e=e:e.widget.event_generate('<Shift-Tab>')))
    o.bind('<Up>',shTab)
    o.bind('<Down>',tab)
    if alsoLeftRight:
        o.bind('<Left>',shTab)
        o.bind('<Right>',tab)
def addLabelledBox(row,wide=0,status=None):
    label = addLabel(row,"") # will set contents later
    text,entry = addTextBox(row,wide)
    if status: addStatus(entry,status)
    return label,text,entry
def addRow(parent,wide=0):
    row = Tkinter.Frame(parent)
    if wide: row.pack(fill=Tkinter.X,expand=1)
    else: row.pack()
    return row
def addRightRow(widerow): # call only after adding any left-hand buttons.  better tab order than filling buttons from the right.
    rrow = Tkinter.Frame(widerow)
    rrow.pack(side="right") ; return rrow

def make_output_row(parent):
    # make a row of buttons for choosing where the output goes to
    # if there aren't any options then return None
    # we also put script-variant selection here, if any
    row = None
    def getRow(row):
      if not row:
        row = Tkinter.Frame(parent)
        row.pack(fill=Tkinter.X,expand=1)
      return row
    GUIlang = GUI_languages.get(firstLanguage,firstLanguage)
    if "@variants-"+GUIlang in GUI_translations: # the firstLanguage has script variants
        row=getRow(row)
        if not hasattr(app,"scriptVariant"): app.scriptVariant = Tkinter.StringVar(app)
        count = 0
        for variant in GUI_translations["@variants-"+GUIlang]:
            Tkinter.Radiobutton(row, text=u" "+variant+u" ", variable=app.scriptVariant, value=str(count), indicatoron=forceRadio).pack({"side":"left"})
            count += 1
        app.scriptVariant.set(str(scriptVariants.get(GUIlang,0)))
    if synth_partials_voices and guiVoiceOptions:
        row=getRow(row)
        if not hasattr(app,"voiceOption"): app.voiceOption = Tkinter.StringVar(app)
        Tkinter.Radiobutton(row, text=u" Normal ", variable=app.voiceOption, value="", indicatoron=forceRadio).pack({"side":"left"})
        for o in guiVoiceOptions: Tkinter.Radiobutton(row, text=u" "+o[1].upper()+o[2:]+u" ", variable=app.voiceOption, value=o, indicatoron=forceRadio).pack({"side":"left"})
        app.voiceOption.set(voiceOption)
    if not gotSox: return row # can't do any file output without sox
    if not hasattr(app,"outputTo"):
        app.outputTo = Tkinter.StringVar(app) # NB app not parent (as parent is no longer app)
        app.outputTo.set("0") # not "" or get tri-state boxes on OS X 10.6
    row=getRow(row)
    rightrow = addRightRow(row) # to show beginners this row probably isn't the most important thing despite being in a convenient place, we'll right-align
    def addFiletypeButton(fileType):
        ftu = fileType.upper()
        t = Tkinter.Radiobutton(rightrow, text=cond(forceRadio,""," ")+ftu+" ", variable=app.outputTo, value=fileType, indicatoron=forceRadio)
        bindUpDown(t,True)
        addStatus(t,"Select this to save a lesson or\na phrase to a%s %s file" % (cond(ftu[0] in "AEFHILMNORSX","n",""),ftu))
        t.pack({"side":"left"})
    if winsound or mingw32: got_windows_encoder = fileExists(programFiles+"\\Windows Media Components\\Encoder\\WMCmd.vbs")
    elif cygwin: got_windows_encoder = fileExists(programFiles+"/Windows Media Components/Encoder/WMCmd.vbs")
    else: got_windows_encoder = 0
    Tkinter.Label(rightrow,text=localise("To")+":").pack({"side":"left"})
    t=Tkinter.Radiobutton(rightrow, text=cond(forceRadio,""," ")+localise("Speaker")+" ", variable=app.outputTo, value="0", indicatoron=forceRadio) # (must be value="0" not value="" for OS X 10.6 otherwise the other buttons become tri-state)
    addStatus(t,"Select this to send all sounds to\nthe speaker, not to files on disk")
    bindUpDown(t,True)
    t.pack({"side":"left"})
    if got_program("lame"): addFiletypeButton("mp3")
    if got_windows_encoder: addFiletypeButton("wma")
    if got_program("neroAacEnc") or got_program("faac") or got_program("afconvert"): addFiletypeButton("aac")
    if got_program("oggenc"): addFiletypeButton("ogg")
    if got_program("toolame"): addFiletypeButton("mp2")
    if got_program("speexenc"): addFiletypeButton("spx")
    addFiletypeButton("wav")
    # "Get MP3 encoder" and "Get WMA encoder" changed to "MP3..." and "WMA..." to save width (+ no localisation necessary)
    if unix and not got_program("lame") and got_program("make") and got_program("gcc") and (got_program("curl") or got_program("wget")): addButton(rightrow,"MP3...",app.getEncoder,status="Press this to compile an MP3 encoder\nso Gradint can output to MP3 files") # (checking gcc as well as make because some distros strangely have make but no compiler; TODO what if has a non-gcc compiler)
    # (no longer available) elif (winsound or mingw32) and not got_windows_encoder and not got_program("lame"): addButton(rightrow,"WMA...",app.getEncoder,status="Press this to download a WMA encoder\nso Gradint can output to WMA files")
    return row

def updateSettingsFile(fname,newVals):
    # leaves comments etc intact, but TODO does not cope with changing variables that have been split over multiple lines
    replacement_lines = []
    try: oldLines=u8strip(read(fname)).replace("\r\n","\n").split("\n")
    except IOError: oldLines=[]
    for l in oldLines:
        found=0
        for k in newVals.keys():
            if l.startswith(k):
                replacement_lines.append(k+"="+repr(newVals[k]))
                del newVals[k]
                found=1
        if not found: replacement_lines.append(l)
    for k,v in newVals.items(): replacement_lines.append(k+"="+repr(v))
    if replacement_lines and replacement_lines[-1]: replacement_lines.append("") # ensure blank line at end so there's a \n but we don't add 1 more with each save
    open(fname,"w").write("\n".join(replacement_lines))

def asUnicode(x): # for handling the return value of Tkinter entry.get()
    try: return u""+x # original behaviour
    except: # some localised versions of Windows e.g. German will return Latin1 instead of Unicode, so try interpreting it as utf-8 and Latin-1
        try: return x.decode("utf-8")
        except: return x.decode("iso-8859-1") # TODO can we get what it actually IS? (on German WinXP, sys.getdefaultencoding==ascii and locale==C but Tkinter still returns Latin1)

def setupScrollbar(parent,rowNo):
    onLeft = winCEsound or olpc or (winsound and hasattr(app,"isBigPrint")) # window placement on Vista can sometimes end up too far to the right in big print recordings manager
    s = Tkinter.Scrollbar(parent,takefocus=0)
    s.grid(row=rowNo,column=cond(onLeft,0,1),sticky="ns"+cond(onLeft,"w","e"))
    c=Tkinter.Canvas(parent,bd=0,width=200,height=100,yscrollcommand=s.set)
    c.grid(row=rowNo,column=cond(onLeft,1,0),sticky="nsw")
    s.config(command=c.yview)
    scrolledFrame=Tkinter.Frame(c) ; c.create_window(0,0,window=scrolledFrame,anchor="nw")
    # Mousewheel binding.  TODO the following bind_all assumes only one scrolledFrame on screen at once (redirect all mousewheel events to the frame; necessary as otherwise they'll go to buttons etc)
    app.ScrollUpHandler = lambda *args:c.yview("scroll","-1","units")
    app.ScrollDownHandler = lambda *args:c.yview("scroll","1","units")
    scrolledFrame.bind_all('<Button-4>',app.ScrollUpHandler)
    scrolledFrame.bind_all('<Button-5>',app.ScrollDownHandler)
    # DON'T bind <MouseWheel> on Windows - our version of Tk will segfault when it occurs. See http://mail.python.org/pipermail/python-bugs-list/2005-May/028768.html but we can't patch our library.zip's Tkinter anymore (TODO can we use newer Tk DLLs and ensure setup.bat updates them?)
    return scrolledFrame, c

# GUI presets buttons:
shortDescriptionName = "short-description"+dottxt
longDescriptionName = "long-description"+dottxt
class ExtraButton(object):
    def __init__(self,directory):
        self.shortDescription = u8strip(read(directory+os.sep+shortDescriptionName)).strip(wsp)
        if fileExists(directory+os.sep+longDescriptionName): self.longDescription = u8strip(read(directory+os.sep+longDescriptionName)).strip(wsp)
        else: self.longDescription = self.shortDescription
        self.directory = directory
    def add(self):
        app.extra_button_callables.append(self) # so we're not lost when deleted from the waiting list
        self.button = addButton(app.rightPanel,localise("Add ")+unicode(self.shortDescription,"utf-8"),self,{"fill":"x"})
        self.button["anchor"]="w"
    def __call__(self,*args):
        if not tkMessageBox.askyesno(app.master.title(),unicode(self.longDescription,"utf-8")+"\n"+localise("Add this to your collection?")): return
        newName = self.directory
        if os.sep in newName: newName=newName[newName.rfind(os.sep)+1:]
        if newName.endswith(exclude_from_scan): newName=newName[:-len(exclude_from_scan)]
        if not newName: newName="1"
        ls = []
        try: ls = os.listdir(samplesDirectory)
        except: os.mkdir(samplesDirectory)
        name1=newName
        while newName in ls: newName+="1"
        name2=newName
        newName = samplesDirectory+os.sep+newName
        os.rename(self.directory,newName)
        which_collection = localise(" has been added to your recorded words collection.")
        if fileExists(newName+os.sep+"add-to-vocab"+dottxt):
            which_collection = localise(" has been added to your collection.")
            o=open(vocabFile,"a")
            o.write("# --- BEGIN "+self.shortDescription+" ---\n")
            o.write(u8strip(read(newName+os.sep+"add-to-vocab"+dottxt)).strip(wsp)+"\n")
            o.write("# ----- END "+self.shortDescription+" ---\n")
            if hasattr(app,"vocabList"): del app.vocabList # so re-reads
            os.remove(newName+os.sep+"add-to-vocab"+dottxt)
        if fileExists(newName+os.sep+"add-to-languages"+dottxt):
            changed = 0
            for lang in u8strip(read(newName+os.sep+"add-to-languages"+dottxt)).strip(wsp).split():
                if not lang in [firstLanguage,secondLanguage]+otherLanguages:
                    otherLanguages.append(lang) ; changed = 1
            if changed: sanitise_otherLanguages(), updateSettingsFile("advanced"+dottxt,{"otherLanguages":otherLanguages,"possible_otherLanguages":possible_otherLanguages})
            os.remove(newName+os.sep+"add-to-languages"+dottxt)
        promptsAdd = newName+os.sep+"add-to-prompts"
        if isDirectory(promptsAdd):
            for f in os.listdir(promptsAdd):
                if fileExists_stat(promptsDirectory+os.sep+f): os.remove(promptsAdd+os.sep+f)
                else: os.rename(promptsAdd+os.sep+f, promptsDirectory+os.sep+f)
            os.rmdir(promptsAdd)
        if not name1==name2: which_collection += "\n(NB you already had a "+name1+" so the new one was called "+name2+" - you might want to sort this out.)"
        self.button.pack_forget()
        app.extra_button_callables.remove(self)
        if extra_buttons_waiting_list: app.add_extra_button()
        app.wordsExist = 1
        if tkMessageBox.askyesno(app.master.title(),unicode(self.shortDescription,"utf-8")+which_collection+"\n"+localise("Do you want to start learning immediately?")): app.makelesson()

extra_buttons_waiting_list = []
def make_extra_buttons_waiting_list():
    if os.sep in samplesDirectory:
        oneUp=samplesDirectory[:samplesDirectory.rfind(os.sep)]
        if not oneUp: oneUp=os.sep
    else: oneUp=os.getcwd()
    for d in [samplesDirectory,oneUp]:
        try: ls = os.listdir(d)
        except: continue
        ls.sort()
        for l in ls:
            if l.endswith(exclude_from_scan) and fileExists(d+os.sep+l+os.sep+shortDescriptionName): extra_buttons_waiting_list.append(ExtraButton(d+os.sep+l))

def focusButton(button):
    button.focus()
    if macsound: # focus() should display the fact on Windows and Linux, but doesn't on OS X so:
        def flashButton(button,state):
            try: button.config(state=state)
            except: pass # maybe not a button
        for t in range(250,1000,250): # (NB avoid epilepsy's 5-30Hz!)
          app.after(t,lambda *args:flashButton(button,"active"))
          app.after(t+150,lambda *args:flashButton(button,"normal"))
        # (Don't like flashing, but can't make it permanently active as it won't change when the focus does)

if WMstandard: GUI_omit_statusline = 1 # unlikely to be room (and can disrupt nav)

def startTk():
    class Application(Tkinter.Frame):
        def __init__(self, master=None):
            Tkinter.Frame.__init__(self, master)
            class EmptyClass: pass
            self.todo = EmptyClass() ; self.toRestore = []
            self.ScrollUpHandler = self.ScrollDownHandler = lambda *args:True
            global app ; app = self
            make_extra_buttons_waiting_list()
            if olpc: self.master.option_add('*font',cond(extra_buttons_waiting_list,'Helvetica 9','Helvetica 14'))
            elif macsound and Tkinter.TkVersion>=8.6: self.master.option_add('*font','System 13') # ok with magnification.  Note >13 causes square buttons.  (Including this line causes "Big print" to work)
            elif WMstandard: self.master.option_add('*font','Helvetica 7') # TODO on ALL WMstandard devices?
            if winsound or cygwin or macsound: self.master.resizable(1,0) # resizable in X direction but not Y (latter doesn't make sense, see below).  (Don't do this on X11 because on some distros it results in loss of automatic expansion as we pack more widgets.)
            elif unix:
                import commands
                if commands.getoutput("xlsatoms|grep COMPIZ_WINDOW").find("COMPIZ")>-1: # (not _COMPIZ_WM_WINDOW_BLUR, that's sometimes present outside Compiz)
                  # Compiz sometimes has trouble auto-resizing our window (e.g. on Ubuntu 11.10)
                  self.master.geometry("%dx%d" % (self.winfo_screenwidth(),self.winfo_screenheight()))
                  if not GUI_always_big_print: self.todo.alert = "Gradint had to maximize itself because your window manager is Compiz which sometimes has trouble handling Tkinter window sizes"
            self.extra_button_callables = []
            self.pack(fill=Tkinter.BOTH,expand=1)
            self.leftPanel = Tkinter.Frame(self)
            self.leftPanel.pack(side="left",fill=Tkinter.X,expand=1) # "fill" needed so listbox can fill later
            self.rightPanel = None # for now
            self.cancelling = 0 # guard against multiple presses of Cancel
            self.Label = Tkinter.Label(self.leftPanel,text="Please wait a moment")
            self.Label.pack()
            self.Label["wraplength"]=self.Label.winfo_screenwidth() # don't go off-screen in teacherMode
            # See if we can figure out what Tk is doing with the fonts (on systems without magnification):
            try:
                f=str(self.Label.cget("font")).split()
                nominalSize = intor0(f[-1])
                if nominalSize: f=" ".join(f[:-1])+" %d"
                else: # Tk 8.5+ ?
                    f=str(self.tk.eval('set font [font actual '+' '.join(f)+']')).split()
                    upNext = 0
                    for i in range(len(f)):
                        if f[i]=="-size": upNext=1
                        elif upNext:
                            nominalSize=intor0(f[i])
                            if nominalSize<0: nominalSize,f[i] = -nominalSize,"-%d"
                            else: f[i]="%d"
                            break
                    f=" ".join(f)
                    if not "%d" in f: raise Exception("wrong format") # caught below
                pixelSize = self.Label.winfo_reqheight()-2*int(str(self.Label["borderwidth"]))-2*int(str(self.Label["pady"]))
                # NB DO NOT try to tell Tk a desired pixel size - you may get a *larger* pixel size.  Need to work out the desired nominal size.
                approx_lines_per_screen_when_large = 25 # TODO really? (24 at 800x600 192dpi 15in but misses the status line, but OK for advanced users.  setting 25 gives nominal 7 which is rather smaller.)
                largeNominalSize = int(nominalSize*self.Label.winfo_screenheight()/approx_lines_per_screen_when_large/pixelSize)
                if largeNominalSize >= nominalSize+3:
                    self.bigPrintFont = f % largeNominalSize
                    self.bigPrintMult = largeNominalSize*1.0/nominalSize
                    if GUI_always_big_print:
                        self.bigPrint0()
                else: self.after(100,self.check_window_position) # (needs to happen when window is already drawn if you want it to preserve the X co-ordinate)
            except: pass # wrong font format or something - can't do it
            if winCEsound and ask_teacherMode: self.Label["font"]="Helvetica 16" # might make it slightly easier
            self.remake_cancel_button(localise("Cancel lesson"))
            self.Cancel.focus() # (default focus if we don't add anything else, e.g. reader)
            self.copyright_string = u"This is "+(u""+program_name).replace("(c)",u"\n\u00a9").replace("-",u"\u2013")
            self.Version = Tkinter.Label(self.leftPanel,text=self.copyright_string)
            addStatus(self.Version,self.copyright_string)
            if olpc: self.Version["font"]='Helvetica 9'
            self.pollInterval = cond(winCEsound,300,100) # ms
            self.startTime=time.time()
            self.after(self.pollInterval,self.poll)
            # and hide the console on Mac OS:
            try: self.tk.call('console','hide')
            except: pass
            self.change_button_shown = 0
            self.bind("<Leave>",self.restore_copyright)
            self.bind("<FocusOut>",self.restore_copyright)
            global recorderMode
            if recorderMode:
                if tkSnack: doRecWords()
                else:
                    show_warning("Cannot do recorderMode because tkSnack library (python-tksnack) not installed")
                    recorderMode = 0
        def remake_cancel_button(self,text=""): # sometimes need to re-make it to preserve tab order
            self.CancelRow = addRow(self.leftPanel)
            self.Cancel = addButton(self.CancelRow,text,self.cancel,{"side":"left"})
            self.CancelRow.pack()
        def set_statusline(self,text): # ONLY from callbacks
            if GUI_omit_statusline or not hasattr(self,"ListBox"): return # status changes on main screen can cause too much jumping
            if not "\n" in text: text += "\n(TODO: Make that a 2-line message)" # being 2 lines helps to reduce flashing problems.  but don't want to leave 2nd line blank.
            self.Version["text"] = text
            if not winCEsound: self.balance_statusline,self.pollInterval = self.pollInterval,10
        def restore_statusline(self,*args): # ONLY from callbacks
            if not hasattr(self,"ListBox"): return
            # self.Version["text"] = self.copyright_string
            self.Version["text"] = "\n"
        def restore_copyright(self,*args): self.Version["text"] = self.copyright_string
        def addOrTestScreen_poll(self):
            if hasattr(self,"balance_statusline"): # try to prevent flashing on some systems/languages due to long statusline causing window resize which then takes the mouse out of the button that set the long statusline etc
                if self.Version.winfo_reqwidth() > self.ListBox.winfo_reqwidth(): self.ListBox["width"] = int(self.ListBox["width"])+1
                else:
                    self.pollInterval = self.balance_statusline
                    del self.balance_statusline
            self.sync_listbox_etc()
            if self.ListBox.curselection():
                if not self.change_button_shown:
                    self.ChangeButton.pack()
                    self.change_button_shown = 1
                    self.Cancel["text"] = localise("Cancel selection")
            else:
                if self.change_button_shown:
                    self.ChangeButton.pack_forget()
                    self.change_button_shown = 0
                    self.lastText1 = 1 # force update
            if self.toRestore:
                if not hasattr(self,"restoreButton"): self.restoreButton = addButton(self.TestEtcCol,localise("Restore"),self.restoreText,status="This button will undo\nGradint's transliteration of the input")
            elif hasattr(self,"restoreButton"):
                self.restoreButton.pack_forget() ; del self.restoreButton
            try:
                if hasattr(self,"set_watch_cursor"):
                    self.config(cursor="watch") ; self.TestTextButton.config(cursor="watch")
                    del self.set_watch_cursor
                if hasattr(self,"unset_watch_cursor"):
                    self.config(cursor="") ; self.TestTextButton.config(cursor="")
                    del self.unset_watch_cursor
            except: pass # (if the Tk for some reason doesn't support them then that's OK)
        def poll(self):
          try:
            global voiceOption
            if hasattr(self,"ListBox"): self.addOrTestScreen_poll()
            if hasattr(self,"scriptVariant"):
              v = self.scriptVariant.get()
              if v: v=int(v)
              else: v=0
              if not v==scriptVariants.get(firstLanguage,0): self.setVariant(v)
            if hasattr(self,"voiceOption") and not self.voiceOption.get()==voiceOption:
              voiceOption=self.voiceOption.get() ; updateSettingsFile(settingsFile,{"voiceOption":voiceOption})
            if hasattr(self,"outputTo"):
             outTo = self.outputTo.get()
             if hasattr(self,"lastOutTo") and self.lastOutTo==outTo: pass
             else:
              self.lastOutTo = outTo
              if outTo=="0": outTo=""
              if hasattr(self,"TestTextButton"):
                if outTo: self.TestTextButton["text"]=localise("To")+" "+outTo.upper()
                else: self.TestTextButton["text"]=localise("Speak")
                # used to be called "Test" instead of "Speak", but some people didn't understand that THEY'RE doing the testing (not the computer)
              if hasattr(self,"MakeLessonButton"):
                if outTo: self.MakeLessonButton["text"]=localise("Make")+" "+outTo.upper()
                else: self.MakeLessonButton["text"]=localise("Start lesson") # less confusing for beginners than "Make lesson", if someone else has set up the words
            if hasattr(self,"BriefIntButton"):
                if emergency_lessonHold_to < time.time(): t=localise("Brief interrupt")
                else: t=localise("Resume")+" ("+str(int(emergency_lessonHold_to-time.time()))+")"
                if not self.BriefIntButton["text"]==t:
                    self.BriefIntButton["text"]=t
                    if t==localise("Brief interrupt"): self.Label["text"]=localise("Resuming...")
            if not self.todo.__dict__: return # can skip the rest
            if hasattr(self.todo,"not_first_time"):
                self.Cancel["text"] = "Stop lesson"
                del self.todo.not_first_time
            if hasattr(self.todo,"set_main_menu") and not recorderMode:
                # set up the main menu (better do it on this thread just in case)
                self.cancelling = 0 # in case just pressed "stop lesson" on a repeat - make sure Quit button will now work
                self.Label.pack_forget()
                self.CancelRow.pack_forget()
                if self.todo.set_main_menu=="keep-outrow":
                    if hasattr(self,"OutputRow"): self.OutputRow.pack(fill=Tkinter.X,expand=1) # just done pack_forget in thindown
                else:
                    if hasattr(self,"OutputRow"): self.OutputRow.pack_forget()
                    outRow = make_output_row(self.leftPanel)
                    if outRow: self.OutputRow=outRow
                self.TestButton = addButton(self.leftPanel,localise(cond(self.wordsExist,"Manage word list","Create word list")),self.showtest) # used to be called "Add or test words", but "Manage word list" may be better for beginners.  And it seems that "Create word list" is even better for absolute beginners, although it shouldn't matter if self.wordsExist is not always set back to 0 when it should be.
                self.make_lesson_row()
                if userNameFile:
                    global GUI_usersRow
                    # if GUI_usersRow: GUI_usersRow.pack() else:  -- don't do this (need to re-create every time for correct tab order)
                    GUI_usersRow=addRow(self.leftPanel)
                    updateUserRow(1)
                if hasattr(self,"bigPrintFont"):
                    self.BigPrintButton = addButton(self.leftPanel,localise("Big print"),self.bigPrint)
                    self.BigPrintButton["font"]=self.bigPrintFont
                self.remake_cancel_button(localise("Quit"))
                if not GUI_omit_statusline: self.Version.pack(fill=Tkinter.X,expand=1)
                if olpc or self.todo.set_main_menu=="test" or GUI_for_editing_only: self.showtest() # olpc: otherwise will just get a couple of options at the top and a lot of blank space (no way to centre it)
                else: focusButton(self.TestButton)
                del self.todo.set_main_menu
                self.restore_copyright()
            if hasattr(self.todo,"alert"):
                # we have to do it on THIS thread (especially on Windows / Cygwin; Mac OS and Linux might get away with doing it from another thread)
                tkMessageBox.showinfo(self.master.title(),self.todo.alert)
                del self.todo.alert
            if hasattr(self.todo,"question"):
                self.answer_given = tkMessageBox.askyesno(self.master.title(),self.todo.question)
                del self.todo.question
            if hasattr(self.todo,"set_label"):
                self.Label["text"] = self.todo.set_label
                del self.todo.set_label
            if hasattr(self.todo,"thindown"):
                self.thin_down_for_lesson()
                self.setLabel(self.todo.thindown)
                del self.todo.thindown
            if hasattr(self.todo,"add_briefinterrupt_button") and runner:
                self.BriefIntButton = addButton(self.CancelRow,localise("Brief interrupt"),self.briefInterrupt,{"side":"left"}) # on RHS of Cancel = reminescient of the stop and pause controls on a tape recorder
                focusButton(self.BriefIntButton)
                del self.todo.add_briefinterrupt_button
            if hasattr(self.todo,"remove_briefinterrupt_button"):
                if hasattr(self,"BriefIntButton"):
                    self.BriefIntButton.pack_forget() ; del self.BriefIntButton
                elif hasattr(self.todo,"add_briefinterrupt_button"): del self.todo.add_briefinterrupt_button # cancel pressed while still making lesson
                del self.todo.remove_briefinterrupt_button
            if hasattr(self.todo,"clear_text_boxes"):
                self.Text1.set("") ; self.Text2.set("") ; self.Entry1.focus()
                del self.todo.clear_text_boxes
            if hasattr(self.todo,"undoRecordFrom"):
                theRecorderControls.undoRecordFrom()
                del self.todo.undoRecordFrom
            if hasattr(self.todo,"input_response"): # WMstandard
                self.input_to_set.set(self.todo.input_response)
                del self.todo.input_response,self.input_to_set
            if hasattr(self.todo,"exit_ASAP"):
                self.master.destroy()
                self.pollInterval = 0
          finally: # (try to make sure GUI exceptions at least don't stop the poll loop)
            if self.pollInterval: self.after(self.pollInterval,self.poll)
        def briefInterrupt(self,*args):
            global emergency_lessonHold_to
            if emergency_lessonHold_to:
                emergency_lessonHold_to = 0
                self.setLabel("")
            elif finishTime-lessonLen + 20 >= time.time(): # (TODO customise the 20?)
                global askAgain_explain
                askAgain_explain = "A brief interrupt when you've only just started is never a good idea.  "
                self.cancel()
            else:
                emergency_lessonHold_to = time.time() + briefInterruptLength
                self.setLabel(localise("Emergency brief interrupt"))
        def make_lesson_row(self): # creates but doesn't pack.  May need to re-make to preserve tab order.  (Assumes any existing one is pack_forget)
            words,mins = str(maxNewWords),cond(int(maxLenOfLesson/60)==maxLenOfLesson/60.0,str(int(maxLenOfLesson/60)),str(maxLenOfLesson/60.0))
            if hasattr(self,"NumWords"): words=self.NumWords.get()
            if hasattr(self,"Minutes"): mins=self.Minutes.get()
            self.LessonRow = addRow(self.leftPanel)
            if GUI_for_editing_only: return
            self.NumWords,entry = addTextBox(self.LessonRow)
            entry["width"]=2
            addStatus(entry,"Limits the maximum number of NEW words\nthat are put in each lesson")
            self.NumWords.set(words)
            addLabel(self.LessonRow,localise(cond(fileExists(progressFile),"new ","")+"words in"))
            self.Minutes,self.MinsEntry = addTextBox(self.LessonRow)
            addStatus(self.MinsEntry,"Limits the maximum time\nthat a lesson is allowed to take")
            self.MinsEntry["width"]=3
            self.Minutes.set(mins)
            addLabel(self.LessonRow,localise("mins"))
            self.MakeLessonButton=addButton(self.LessonRow,localise("Start lesson"),self.makelesson,{"side":"left"},status="Press to create customized lessons\nusing the words in your collection")
            self.lastOutTo=-1 # so it updates the Start Lesson button if needed
            self.MakeLessonButton.bind('<FocusIn>',(lambda *args:app.after(10,lambda *args:app.MinsEntry.selection_clear())))
        def sync_listbox_etc(self):
            if not hasattr(self,"vocabList"):
                if hasattr(self,"needVocablist"): return # already waiting for main thread to make one
                while self.ListBox.get(0): self.ListBox.delete(0) # clear completely (otherwise the following would just do a least-disruptive update)
                self.ListBox.insert(0,"Updating list from "+vocabFile+"...")
                self.needVocablist=1
                return
            elif hasattr(self,"needVocablist"):
                del self.needVocablist
                self.ListBox.delete(0) # the Loading...
                self.lastText1=1 # so continues below
            text1,text2 = asUnicode(self.Text1.get()),asUnicode(self.Text2.get())
            if text1==self.lastText1 and text2==self.lastText2: return
            self.lastText1,self.lastText2 = text1,text2
            if WMstandard and text1=="(Push OK to type A-Z)": text1=""
            for control,current,restoreTo in self.toRestore:
                if not asUnicode(control.get())==current:
                    self.toRestore = [] ; break
            if text1 or text2: self.Cancel["text"] = localise(cond(self.ListBox.curselection(),"Cancel selection","Clear input boxes"))
            else: self.Cancel["text"] = localise(cond(olpc or GUI_for_editing_only,"Quit","Back to main menu"))
            h = hanzi_only(text1)
            if Tk_might_display_wrong_hanzi and not self.Label1["text"].endswith(wrong_hanzi_message) and (h or hanzi_only(text2)): self.Label1["text"]+=("\n"+wrong_hanzi_message)
            if h and not u"".join(fix_compatibility(text1).split())==hanzi_and_punc(text1):
                # There is hanzi in the L2 text, but it's not all hanzi.  This might mean they've pasted in a mixture of hanzi+pinyin from ruby markup (or maybe even hanzi+pinyin+English), so offer to trim it down to hanzi only.  (Allow spacing differences.)
                if not hasattr(self,"stripButton"): self.stripButton=addButton(self.TestEtcCol,localise("Delete non-hanzi"),self.stripText,status="If you pasted a mix of hanzi and\nother annotations, this can remove the annotations.")
            elif hasattr(self,"stripButton"):
                self.stripButton.pack_forget() ; del self.stripButton
            if synthCache:
                cacheManagementOptions = [] # (text, oldKey, newKey, oldFile, newFile)
                for t,l in [(text1.encode('utf-8'),secondLanguage),(text2.encode('utf-8'),firstLanguage)]:
                    k,f = synthcache_lookup("!synth:"+t+"_"+l,justQueryCache=1)
                    if f:
                      if (partials_langname(l) in synth_partials_voices or get_synth_if_possible(l,0)): # (no point having these buttons if there's no chance we can synth it by any method OTHER than the cache)
                        if k in synthCache_transtbl and k[0]=="_": cacheManagementOptions.append(("Keep in "+l+" cache",k,k[1:],0,0))
                        elif k[0]=="_": cacheManagementOptions.append(("Keep in "+l+" cache",0,0,f,f[1:]))
                        if k in synthCache_transtbl: cacheManagementOptions.append(("Reject from "+l+" cache",k,"__rejected_"+k,0,0))
                        else: cacheManagementOptions.append(("Reject from "+l+" cache",0,0,f,"__rejected_"+f))
                    else:
                      k,f = synthcache_lookup("!synth:__rejected_"+t+"_"+l,justQueryCache=1)
                      if not f: k,f = synthcache_lookup("!synth:__rejected__"+t+"_"+l,justQueryCache=1)
                      if f:
                        if k in synthCache_transtbl: cacheManagementOptions.append(("Undo "+l+" cache reject",k,k[11:],0,0))
                        else: cacheManagementOptions.append(("Undo "+l+" cache reject",0,0,f,f[11:]))
                      elif l==secondLanguage and mp3web and not ';' in t: cacheManagementOptions.append(("Get from "+mp3webName,0,0,0,0))
                if not hasattr(self,"cacheManagementOptions"):
                    self.cacheManagementOptions = []
                    self.cacheManagementButtons = []
                if not cacheManagementOptions==self.cacheManagementOptions:
                    for b in self.cacheManagementButtons: b.pack_forget()
                    self.cacheManagementOptions = cacheManagementOptions
                    self.cacheManagementButtons = []
                    for txt,a,b,c,d in cacheManagementOptions: self.cacheManagementButtons.append(addButton(self.TestEtcCol,txt,lambda e=self,a=a,b=b,c=c,d=d:e.doSynthcacheManagement(a,b,c,d),status="This button is for synthCache management.\nsynthCache is explained in advanced"+extsep+"txt"))
            if self.ListBox.curselection():
                if not (text1 or text2): self.ListBox.selection_clear(0,'end') # probably just added a new word while another was selected (added a variation) - clear selection to reduce confusion
                else: return # don't try to be clever with searches when editing an existing item (the re-ordering can be confusing)
            text1,text2 = text1.lower().replace(" ",""),text2.lower().replace(" ","") # ignore case and whitespace when searching
            l=map(lambda (x,y):x+"="+y, filter(lambda (x,y):text1 in x.lower().replace(" ","") and text2 in y.lower().replace(" ",""),self.vocabList)[-tkNumWordsToShow:])
            l.reverse() ; synchronizeListbox(self.ListBox,l) # show in reverse order, in case the bottom of the list box is off-screen
        def doSynthcacheManagement(self,oldKey,newKey,oldFname,newFname):
            # should be a quick operation - might as well do it in the GUI thread
            if (oldKey,oldFname) == (0,0): # special for mp3web
                self.menu_response="mp3web" ; return
            if oldKey in synthCache_transtbl:
                if newKey: synthCache_transtbl[newKey]=synthCache_transtbl[oldKey]
                else: del synthCache_transtbl[oldKey]
                open(synthCache+os.sep+transTbl,'w').write("".join([v+" "+k+"\n" for k,v in synthCache_transtbl.items()]))
            if oldFname:
                del synthCache_contents[oldFname]
                if newFname:
                    os.rename(synthCache+os.sep+oldFname,synthCache+os.sep+newFname)
                    synthCache_contents[newFname]=1
                else: os.remove(synthCache+os.sep+oldFname)
            self.lastText1 = 1 # ensure different so cache-management options get updated
        def restoreText(self,*args):
            for control,current,restoreTo in self.toRestore:
                if asUnicode(control.get())==current: control.set(restoreTo)
            self.toRestore = []
        def stripText(self,*args): self.Text1.set(fix_commas(hanzi_and_punc(asUnicode(self.Text1.get()))))
        def thin_down_for_lesson(self):
            if hasattr(self,"OutputRow"): self.OutputRow.pack_forget()
            if hasattr(self,"CopyFromButton"):
                self.CopyFromButton.pack_forget() ; del self.CopyFromButton
            self.LessonRow.pack_forget()
            if GUI_usersRow: GUI_usersRow.pack_forget()
            if hasattr(self,"BigPrintButton"):
                self.BigPrintButton.pack_forget() ; del self.BigPrintButton
            if hasattr(self,"TestButton"): self.TestButton.pack_forget()
            else:
                for i in [self.row1,self.row2,self.row3,self.row4,self.ListBox,self.rightPanel]: i.pack_forget()
                if hasattr(self,"alternateRightPanel"):
                    self.alternateRightPanel.pack_forget()
                    del self.alternateRightPanel
                if self.change_button_shown:
                    self.ChangeButton.pack_forget()
                    self.change_button_shown = 0
                del self.ListBox # so doesn't sync lists, or assume Cancel button is a Clear button
                if hasattr(self,"cacheManagementButtons"):
                    for b in self.cacheManagementButtons: b.pack_forget()
                    del self.cacheManagementButtons,self.cacheManagementOptions
                app.master.title(appTitle)
            self.CancelRow.pack_forget() ; self.Version.pack_forget()
            self.Label.pack() ; self.CancelRow.pack()
            self.Label["text"] = "Working..." # (to be replaced by time indication on real-time, not on output-to-file)
            self.Cancel["text"] = localise("Quit")
        def bigPrint0(self):
            self.master.option_add('*font',self.bigPrintFont)
            self.master.option_add('*Scrollbar*width',int(16*self.bigPrintMult)) # (works on some systems; usually ineffective on Mac)
            self.Label["font"]=self.bigPrintFont
            del self.bigPrintFont # (TODO do we want an option to undo it?  or would that take too much of the big print real-estate.)
            self.isBigPrint=1
        def bigPrint(self,*args):
            self.thin_down_for_lesson()
            self.Version["font"]=self.bigPrintFont
            self.bigPrint0()
            if self.rightPanel: # oops, need to re-construct it
                global extra_buttons_waiting_list
                extra_buttons_waiting_list = []
                make_extra_buttons_waiting_list()
                self.rightPanel = None
            self.check_window_position()
            self.todo.set_main_menu = 1
        def check_window_position(self,*args): # called when likely to be large print and filling the screen
            try: self.master.geometry("+"+str(int(self.winfo_rootx()))+"+0")
            except: pass
        def makelesson(self,*args):
            if hasattr(self,"userNo"): select_userNumber(intor0(self.userNo.get())) # in case some race condition stopped that from taking effect before (e.g. WinCE)
            try:  numWords=int(self.NumWords.get())
            except:
                self.todo.alert = localise("Error: maximum number of new words must be an integer") ; return
            try:  mins=float(self.Minutes.get())
            except:
                self.todo.alert = localise("Error: minutes must be a number") ; return
            problem=0 # following message boxes have to be resistant to "I'm just going to click 'yes' without reading it" users who subsequently complain that Gradint is ineffective.  Make the 'yes' option put them back into the parameters, and provide an extra 'proceed anyway' on 'no'.
            if numWords>=10:
                if tkMessageBox.askyesno(self.master.title(),localise("%s new words is a lot to remember at once.  Reduce to 5?") % (str(numWords),)):
                    numWords=5 ; self.NumWords.set("5")
                else: problem=1
            if mins>30:
                if tkMessageBox.askyesno(self.master.title(),localise("More than 30 minutes is rarely more helpful.  Reduce to 30?")):
                  mins=30;self.Minutes.set("30")
                else: problem=1
            if mins<20:
                if tkMessageBox.askyesno(self.master.title(),localise("Less than 20 minutes can be a rush.  Increase to 20?")):
                  mins=20;self.Minutes.set("20")
                else: problem=1
            if problem and not tkMessageBox.askyesno(self.master.title(),localise("Proceed anyway?")): return
            global maxNewWords,maxLenOfLesson
            d={}
            if not maxNewWords==numWords: d["maxNewWords"]=maxNewWords=numWords
            if not maxLenOfLesson==int(mins*60): d["maxLenOfLesson"]=maxLenOfLesson=int(mins*60)
            if d: updateSettingsFile("advanced"+dottxt,d)
            self.thin_down_for_lesson()
            self.Cancel["text"] = localise("Cancel lesson")
            self.menu_response = "go"
        def showtest(self,*args): # Can assume main menu is shown at the moment.
            title = localise(cond(self.wordsExist,"Manage word list","Create word list"))
            if hasattr(self,"userNo"):
                try: uname = lastUserNames[intor0(self.userNo.get())]
                except IndexError: uname="" # can happen if it's 0 but list is empty
                if uname:
                    title += (": "+uname)
                    select_userNumber(intor0(self.userNo.get())) # in case some race condition stopped that from taking effect before (e.g. WinCE)
            app.master.title(title)
            if hasattr(self,"BigPrintButton"):
                self.BigPrintButton.pack_forget() ; del self.BigPrintButton
            self.TestButton.pack_forget() ; del self.TestButton
            for i in [self.LessonRow,self.CancelRow,self.Version]: i.pack_forget()
            if GUI_usersRow: GUI_usersRow.pack_forget()
            self.row1 = addRow(self.leftPanel,1)
            self.row2 = addRow(self.leftPanel,1)
            self.row3 = addRow(self.leftPanel)
            self.row4 = addRow(self.leftPanel,1)
            self.Label1,self.Text1,self.Entry1 = addLabelledBox(self.row1,True)
            self.TestEtcCol = addRow(self.row1) # effectively adding a column to the end of the row, for "Speak" and any other buttons to do with 2nd-language text (although be careful not to add too many due to tabbing)
            self.TestTextButton = addButton(self.TestEtcCol,"",self.testText,status="Use this button to check how the computer\nwill pronounce words before you add them") # will set text in updateLanguageLabels
            self.Label2,self.Text2,self.Entry2 = addLabelledBox(self.row2,True)
            if not WMstandard:
              self.Entry1.bind('<Return>',self.testText)
              self.Entry1.bind('<F5>',self.debugText)
              self.Entry2.bind('<Return>',self.addText)
              for e in [self.Entry1,self.Entry2]: addStatus(e,"Enter a word or phrase to add or to test\nor to search your existing collection",mouseOnly=1)
            self.AddButton = addButton(self.row2,"",self.addText,status="Adds the pair to your vocabulary collection\nor adds extra revision if it's already there") # will set text in updateLanguageLabels
            self.L1Label,self.L1Text,self.L1Entry = addLabelledBox(self.row3,status="The abbreviation of your\nfirst (i.e. native) language")
            self.L2Label,self.L2Text,self.L2Entry = addLabelledBox(self.row3,status="The abbreviation of the other\nlanguage that you learn most")
            self.L1Entry["width"]=self.L2Entry["width"]=3
            self.L1Entry.bind('<Return>',lambda e:e.widget.after(10,lambda e=e:e.widget.event_generate('<Tab>')))
            self.L2Entry.bind('<Return>',self.changeLanguages)
            for e in [self.L1Entry,self.L2Entry]: e.bind('<Button-1>',(lambda e:e.widget.after(10,lambda e=e:selectAll(e))))
            self.ChangeLanguageButton = addButton(self.row3,"",self.changeLanguages,status="Use this button to set your\nfirst and second languages") # will set text in updateLanguageLabels
            self.ChangeLanguageButton.bind('<FocusIn>',(lambda *args:app.after(10,lambda *args:app.L2Entry.selection_clear())))
            self.AddButton.bind('<FocusIn>',(lambda *args:app.after(10,lambda *args:app.L1Entry.selection_clear()))) # for backwards tabbing
            if GUI_omit_settings and (vocabFile==user0[1] or fileExists(vocabFile)): self.row3.pack_forget()
            if textEditorCommand:
                self.RecordedWordsButton = addButton(self.row4,"",self.showRecordedWords,{"side":"left"},status="This button lets you manage recorded\n(as opposed to computer-voiced) words")
                row4right = addRightRow(self.row4)
                self.EditVocabButton = addButton(row4right,"",self.openVocabFile,{"side":"left"},status="This button lets you edit your\nvocab collection in "+textEditorName)
                if not GUI_omit_settings: addButton(row4right,"advanced"+dottxt,self.openAdvancedTxt,{"side":"left"},status="Press this button to change voices,\nlearn multiple languages, etc")
                self.make_lesson_row()
            else: # no text editor, but can at least have Recorded Words button now we have a built-in manager
                self.make_lesson_row()
                self.RecordedWordsButton = addButton(self.LessonRow,"",self.showRecordedWords,{"side":"right"},status="This button lets you manage recorded\n(as opposed to computer-voiced) words")
            if textEditorCommand and lastUserNames and lastUserNames[0]: self.CopyFromButton = addButton(cond(GUI_omit_settings,row4right,self.LessonRow),localise("Copy from..."),self.showCopyFrom,{"side":"left"},status="This button lets you copy recorded\nand computer-voiced words from other users") # TODO if not textEditorCommand then only reason why can't have this is row4right won't be defined, need to fix that (however probably don't want to bother on XO etc)
            self.remake_cancel_button(localise(cond(olpc or GUI_for_editing_only,"Quit","Back to main menu")))
            self.ChangeButton = addButton(self.CancelRow,"",self.changeItem,{"side":"left"},status="Press to alter or to delete\nthe currently-selected word in the list") ; self.ChangeButton.pack_forget() # don't display it until select a list item
            self.updateLanguageLabels()
            self.LessonRow.pack() ; self.CancelRow.pack()
            self.ListBox = Tkinter.Listbox(self.leftPanel, takefocus=0) # TODO takefocus=0 for now. bindUpDown?  but up/down/left/right also need to work IN the listbox, this could be tricky.  Also need to populate the input boxes when on the list box.
            self.ListBox.bind('<ButtonRelease-1>', self.getListItem)
            self.ListBox.bind('<ButtonRelease-3>', self.wrongMouseButton)
            addStatus(self.ListBox,"This is your collection of computer-voiced words.\nClick to hear, change or remove an item.")
            self.ListBox["width"]=1 # so it will also squash down if window is narrow
            if winCEsound: self.ListBox["font"]="Helvetica 12" # larger is awkward, but it doesn't have to be SO small!
            elif macsound and Tkinter.TkVersion>=8.6: self.ListBox["font"]=cond(hasattr(self,"isBigPrint"),"System 20","System 16") # 16 ok with magnification, clearer than 13
            self.ListBox.pack(fill=Tkinter.X,expand=1) # DON'T fill Y as well, because if you do we'll have to implement more items, and that could lose the clarity of incremental search
            if not GUI_omit_statusline: self.Version.pack(fill=Tkinter.X,expand=1)
            self.lastText1,self.lastText2=1,1 # (different from empty string, so it sync's)
            if not self.rightPanel:
                self.rightPanel = Tkinter.Frame(self)
                for i in range(min(max_extra_buttons,len(extra_buttons_waiting_list))): self.add_extra_button()
            if not hasattr(self,"userNo") or not intor0(self.userNo.get()): self.rightPanel.pack({"side":"left"})
            elif self.extra_button_callables:
                self.alternateRightPanel = Tkinter.Frame(self)
                self.alternateRightPanel.pack({"side":"left"})
                curWidth = self.winfo_width()
                addLabel(self.alternateRightPanel,"Only the first user can access the preset collections, but you can copy the vocab lists and recordings from each other once you've added them.")["wraplength"]=int(curWidth/2) # (presets are not really compatible with multiple users, unless re-write for copy-and-track-what's-done, which would take double the disk space on a one-person setup AND would have trouble upgrading existing users who have started into their presets)
            self.Entry1.focus()
        def add_extra_button(self):
            global extra_buttons_waiting_list
            extra_buttons_waiting_list[0].add()
            extra_buttons_waiting_list = extra_buttons_waiting_list[1:]
        def openVocabFile(self,*args): self.fileToEdit, self.menu_response = vocabFile,"edit"
        def openAdvancedTxt(self,*args): self.fileToEdit, self.menu_response = "advanced"+dottxt,"edit"
        def showRecordedWords(self,*args): doRecWords()
        def showCopyFrom(self,*args):
            m=Tkinter.Menu(None, tearoff=0, takefocus=0)
            for i in range(len(lastUserNames)):
                if lastUserNames[i] and not i==intor0(self.userNo.get()):
                    if fileExists(addUserToFname(user0[1],i)): m.add_command(label=u"Copy vocab list from "+lastUserNames[i],command=(lambda e=None,i=i:self.copyVocabFrom(i)))
                    m.add_command(label=u"Copy recordings to/from "+lastUserNames[i],command=(lambda e=None,i=i:self.setToOpen((addUserToFname(user0[0],i),addUserToFname(user0[0],intor0(self.userNo.get()))))))
            m.tk_popup(self.CopyFromButton.winfo_rootx(),self.CopyFromButton.winfo_rooty(),entry="0")
        def setToOpen(self,toOpen): self.menu_response,self.toOpen = "samplesCopy",toOpen
        def copyVocabFrom(self,userNo):
            # Copy any NEW vocab lines (including comments).  TODO could also insert them in the right place (like 'diff' without the deletions)
            select_userNumber(userNo,updateGUI=0)
            vCopyFrom = vocabLinesWithLangs()
            select_userNumber(intor0(self.userNo.get()),updateGUI=0)
            vCurrent = list2set(vocabLinesWithLangs())
            o=appendVocabFileInRightLanguages()
            langs = (secondLanguage,firstLanguage)
            for newLangs,line in vCopyFrom:
                if (newLangs,line) in vCurrent: continue # already got it
                if not newLangs==langs: o.write("SET LANGUAGES "+" ".join(list(newLangs))+"\n")
                o.write(line+"\n")
                langs = newLangs
            o.close()
            if hasattr(self,"vocabList"): del self.vocabList # re-read
        def setVariant(self,v):
            scriptVariants[firstLanguage] = v
            updateSettingsFile(settingsFile,{"scriptVariants":scriptVariants})
            if hasattr(self,"TestButton"):
                self.thin_down_for_lesson()
                self.todo.set_main_menu="keep-outrow"
            else: self.updateLanguageLabels()
        def changeLanguages(self,*args):
            global firstLanguage,secondLanguage
            firstLanguage1=asUnicode(self.L1Text.get()).encode('utf-8')
            secondLanguage1=asUnicode(self.L2Text.get()).encode('utf-8')
            if (firstLanguage,secondLanguage) == (firstLanguage1,secondLanguage1): # they didn't change anything
                langs = ESpeakSynth().describe_supported_languages()
                msg = (localise("To change languages, edit the boxes that say '%s' and '%s', then press the '%s' button.") % (firstLanguage,secondLanguage,localise("Change languages")))+"\n\n"+localise("Recorded words may be in ANY languages, and you may choose your own abbreviations for them.  However if you want to use the computer voice for anything then please use standard abbreviations.")
                if langs:
                    if tkMessageBox.askyesno(self.master.title(),msg+"  "+localise("Would you like to see a list of the standard abbreviations for languages that can be computer voiced?")): self.todo.alert = localise("Languages with computer voices (some better than others):")+"\n"+langs
                else: self.todo.alert = msg+"  "+localise("(Sorry, a list of these is not available on this system - check eSpeak installation.)")
                return
            need_redisplay = "@variants-"+GUI_languages.get(firstLanguage,firstLanguage) in GUI_translations or "@variants-"+GUI_languages.get(firstLanguage1,firstLanguage1) in GUI_translations # if EITHER old or new lang has variants, MUST reconstruct that row.  (TODO also do it anyway to get the "Speaker" etc updated?  but may cause unnecessary flicker if that's no big problem)
            firstLanguage,secondLanguage = firstLanguage1,secondLanguage1
            updateSettingsFile(settingsFile,{"firstLanguage":firstLanguage,"secondLanguage":secondLanguage})
            if need_redisplay:
                self.thin_down_for_lesson()
                self.todo.set_main_menu="test"
            else: self.updateLanguageLabels()
            if hasattr(self,"vocabList"): del self.vocabList # it will need to be re-made now
        def updateLanguageLabels(self):
            # TODO things like "To" and "Speaker" need updating dynamically with localise() as well, otherwise will be localised only on restart (unless the old or new lang has variants, in which case it will be repainted anyway above)
            self.Label1["text"] = (localise("Word in %s") % localise(secondLanguage))+":"
            self.Label2["text"] = (localise("Meaning in %s") % localise(firstLanguage))+":"
            self.L1Text.set(firstLanguage)
            self.L2Text.set(secondLanguage)
            self.L1Label["text"] = localise("Your first language")+":"
            self.L2Label["text"] = localise("second")+":"
            self.TestTextButton["text"] = localise("Speak") ; self.lastOutTo=-1 # so updates to "To WAV" etc if necessary
            if hasattr(self,"userNo") and intor0(self.userNo.get()): gui_vocabFile_name="vocab file" # don't expose which user number they are because that might change
            elif len(vocabFile)>15 and os.sep in vocabFile: gui_vocabFile_name=vocabFile[vocabFile.rindex(os.sep)+1:]
            else: gui_vocabFile_name=vocabFile
            if gui_vocabFile_name=="vocab.txt": gui_vocabFile_name=localise(gui_vocabFile_name)
            self.AddButton["text"] = localise("Add to %s") % gui_vocabFile_name
            self.ChangeLanguageButton["text"] = localise("Change languages")
            self.ChangeButton["text"] = localise("Change or delete item")
            if hasattr(self,"EditVocabButton"): self.EditVocabButton["text"] = cond(WMstandard,gui_vocabFile_name,localise(textEditorName)+" "+gui_vocabFile_name) # (save as much space as possible on WMstandard by omitting the "Edit " verb)
            if hasattr(self,"RecordedWordsButton"): self.RecordedWordsButton["text"] = localise("Recorded words")
        def wrongMouseButton(self,*args): self.todo.alert="Please use the OTHER mouse button when clicking on list and button controls." # Simulating it is awkward.  And we might as well teach them something.
        def getListItem(self,*args):
            sel = self.ListBox.curselection()
            if sel:
                item = self.ListBox.get(int(sel[0]))
                if not "=" in item: return # ignore clicks on the Loading message
                l2,l1 = item.split('=',1)
                self.Text1.set(l2) ; self.Text2.set(l1)
            elif not self.ListBox.size(): self.todo.alert="The synthesized words list is empty.  You need to add synthesized words before you can click in the list."
            else: self.todo.alert="Click on a list item to test, change or delete.  You can add a new item using the test boxes above." # Should never get here in Tk 8.4 (if click below bottom of list then last item is selected)
        def changeItem(self,*args):
            self.zap_newlines()
            sel = self.ListBox.curselection()
            l2,l1 = self.ListBox.get(int(sel[0])).split('=',1)
            self.toDelete = l2,l1
            if (asUnicode(self.Text1.get()),asUnicode(self.Text2.get())) == (l2,l1):
                if tkMessageBox.askyesno(self.master.title(),localise("You have not changed the test boxes.  Do you want to delete %s?") % (l2+"="+l1,)):
                    self.menu_response="delete"
            else: self.menu_response="replace"
        def testText(self,*args):
            self.zap_newlines() # (fullstop-quote-newline combinations have been known to confuse eSpeak)
            self.menu_response="test"
        def debugText(self,*args):
            # called when F5 is pressed on the 1st text box
            # currently adds Unicode values to the text, and shows as a dialogue
            # (for use when trying to diagnose people's copy/paste problems)
            setTo = []
            for c in asUnicode(self.Text1.get()): setTo.append(c+"["+hex(ord(c))[2:]+"]")
            setTo=u"".join(setTo)
            self.Text1.set(setTo) ; self.todo.alert = setTo
        def addText(self,*args):
            self.zap_newlines()
            self.menu_response="add"
        def zap_newlines(self): # in case someone pastes in text that contains newlines, better not keep them when adding to vocab
            text1,text2 = asUnicode(self.Text1.get()),asUnicode(self.Text2.get())
            # (also remove the simple visual markup that Wenlin sometimes adds)
            t1,t2=text1,text2
            for zap in ["\n","\r","<b>","</b>","<i>","</i>","<u>","</u>"]: t1,t2=t1.replace(zap,""),t2.replace(zap,"")
            t1,t2 = t1.strip(wsp), t2.strip(wsp)
            if not t1==text1: self.Text1.set(t1)
            if not t2==text2: self.Text2.set(t2)
        def getEncoder(self,*args):
            self.thin_down_for_lesson()
            self.menu_response="get-encoder"
        def setNotFirstTime(self): self.todo.not_first_time = 1
        def setLabel(self,t): self.todo.set_label = t
        def cancel(self,*args):
            if hasattr(self,"ListBox"): # it MIGHT be a 'clear' button
                text1,text2 = asUnicode(self.Text1.get()),asUnicode(self.Text2.get())
                if text1 or text2:
                    self.Text1.set("") ; self.Text2.set("")
                    if self.ListBox.curselection(): self.ListBox.selection_clear(int(self.ListBox.curselection()[0]))
                    self.Cancel["text"] = localise(cond(olpc or GUI_for_editing_only,"Quit","Back to main menu"))
                    return
                elif olpc or GUI_for_editing_only: pass # fall through to Quit
                else:
                    # (comment this out if you want the Quit button to really quit even from add/test words, but probably don't want this now there are other options on the main menu e.g. user switching)
                    self.thin_down_for_lesson()
                    self.todo.set_main_menu="keep-outrow" ; return
            if not self.cancelling:
                if emulated_interruptMain:
                    self.setLabel("Trying to interrupt main thread, please wait...")
                    global need_to_interrupt ; need_to_interrupt = 1
                else: thread.interrupt_main()
            self.cancelling = 1
    def appThread(appclass):
        global app ; appclass() # sets 'app' to itself on construction
        app.master.title(appTitle)
        app.wordsExist = words_exist()
        app.mainloop()
        closeBoxPressed = not hasattr(app.todo,"exit_ASAP")
        app = 0 # (not None - see 'app==None' below)
        if closeBoxPressed:
            if emulated_interruptMain:
                global need_to_interrupt ; need_to_interrupt = 1
                while RM_running: time.sleep(0.1) # ensure main thread is last to exit, sometimes needed
            else: thread.interrupt_main()
    def processing_thread():
        while not app: time.sleep(0.1) # make sure started
        # import cProfile as profile ; return profile.run('rest_of_main()',sort=2)
        rest_of_main()
    if Tkinter.TkVersion < 8.5: # we can do the processing in the main thread, so interrupt_main works
        thread.start_new_thread(appThread,(Application,))
        processing_thread()
    else: # GUI must have main thread
        global emulated_interruptMain ; emulated_interruptMain = 1
        thread.start_new_thread(processing_thread,())
        appThread(Application)

def hanzi_only(unitext): return u"".join(filter(lambda x:0x3000<ord(x)<0xa700 or ord(x)>=0x10000, list(unitext)))
def hanzi_and_punc(unitext): return u"".join(filter(lambda x:0x3000<ord(x)<0xa700 or ord(x)>=0x10000 or x in '.,?;:\'()[]!0123456789-', list(remove_tone_numbers(fix_compatibility(unitext))))) # no " as it could be from SGML markup
# (exclusion of 3000 in above is deliberate, otherwise get problems with hanzi spaces being taken out by fix-compat+strip hence a non-functional 'delete non-hanzi' button appears)
def guiVocabList(parsedVocab):
    # This needs to be fast.  Have tried writing interatively rather than filter and map, and assume stuff is NOT already unicode (so just decode rather than call ensure_unicode) + now assuming no !synth: (but can still run with .txt etc)
    sl2,fl2 = "_"+secondLanguage,"_"+firstLanguage
    sl3,fl3 = sl2+dottxt, fl2+dottxt # txt files
    # (sample files are omitted from the list)
    sl2Len,fl2Len = -len(sl2),-len(fl2)
    ret = []
    for a,b,c in parsedVocab:
        if c.endswith(sl2): c=c[:sl2Len]
        elif c.endswith(sl3): c=readText(c)
        else: continue
        if type(b)==type([]): b=b[cond(len(b)==3,1,-1)]
        if b.endswith(fl2): b=b[:fl2Len]
        elif b.endswith(fl3): b=readText(b)
        else: continue
        ret.append((unicode(c,"utf-8"),unicode(b,"utf-8")))
    return ret
def readText(l): # see utils/transliterate.py (running guiVocabList on txt files from scanSamples)
    l = samplesDirectory+os.sep+l
    if l in variantFiles: # oops. just read the 1st .txt variant
        if os.sep in l: lp=(l+os.sep)[:l.rfind(os.sep)]+os.sep
        else: lp = ""
        varList = filter(lambda x:x.endswith(dottxt),variantFiles[l])
        varList.sort() # so at least it consistently returns the same one.  TODO utils/ cache-synth.py list-synth.py synth-batchconvert-helper.py all use readText() now, can we get them to cache the other variants too?
        l = lp + varList[0]
    return u8strip(read(l)).strip(wsp)

def singular(number,s):
  s=localise(s)
  if firstLanguage=="en" and number==1 and s[-1]=="s": return s[:-1]
  return s
def localise(s):
  d = GUI_translations.get(s,{}) ; s2 = 0
  GUIlang = GUI_languages.get(firstLanguage,firstLanguage)
  if scriptVariants.get(GUIlang,0): s2 = d.get(GUIlang+str(scriptVariants[GUIlang]+1),0)
  if not s2: s2 = d.get(GUIlang,s)
  return s2
if Tk_might_display_wrong_hanzi: localise=lambda s:s
if winCEsound: # some things need more squashing
    del localise
    def localise(s):
        s=GUI_translations.get(s,{}).get(firstLanguage,s)
        return {"Your first language":"1st","second":"2nd","Start lesson":"Start"}.get(s,s)

def synchronizeListbox(listbox,masterList):
    mi=li=0 ; toDelete = []
    while True:
        l=listbox.get(li)
        if mi==len(masterList):
            if not l: break
            elif l in masterList: listbox.delete(li) # re-ordering - unconditionally delete
            else:
                toDelete.append(li) ; li += 1
            continue
        if masterList[mi]==l: mi,li=mi+1,li+1
        elif (not l) or (l in masterList[mi+1:]): # masterList has an extra item before l, or at the end
            listbox.insert(li,masterList[mi])
            mi,li=mi+1,li+1
        elif l in masterList[:mi]: listbox.delete(li) # re-ordering - unconditionally delete
        else:
            toDelete.append(li) ; li += 1
    toDelete.reverse() # last one first so don't disrupt numbers
    i=0
    while i<len(toDelete):
        if not listbox.get(tkNumWordsToShow): break
        listbox.delete(toDelete[i]) ; i += 1
    # When list shrinks small enough, move words down instead of deleting
    li=len(masterList)+len(toDelete)-i # i.e. just past current end of list
    while i<len(toDelete):
        if not toDelete[i]==li-1: # not in right place already
            listbox.insert(li,listbox.get(toDelete[i]))
            listbox.delete(toDelete[i])
        i += 1 ; li -= 1

# Tk stuff (must be done outside of main() so the imported modules are globally visible)
if useTK:
    # Find editor and file-manager commands for the GUI to use
    textEditorName="Edit" ; textEditorWaits=0
    textEditorCommand=explorerCommand=None
    if winsound or mingw32 or cygwin:
        textEditorName="Notepad" ; textEditorWaits=1
        # Try Notepad++ first, otherwise plain notepad
        textEditorCommand = programFiles+os.sep+"Notepad++"+os.sep+"notepad++.exe"
        if fileExists(textEditorCommand): textEditorCommand='"'+textEditorCommand+'" -multiInst -notabbar -nosession'
        else: textEditorCommand="notepad"
        explorerCommand="explorer"
    elif macsound:
        textEditorName="TextEdit"
        textEditorCommand="open -e"
        if got_program("bbedit"):
            textEditorName="bbedit"
            textEditorCommand="bbedit -w" ; textEditorWaits=1
        elif got_program("edit"): # TextWrangler
            textEditorName="edit"
            textEditorCommand="edit -w" ; textEditorWaits=1
        if sys.version.startswith("2.3.5") and "DISPLAY" in os.environ: explorerCommand = None # 'open' doesn't seem to work when running from within Python in X11 on 10.4
        else: explorerCommand="open"
    elif unix:
        if "KDE_FULL_SESSION" is os.environ and got_program("kfmclient"):
            # looks like we're in a KDE session and can use the kfmclient command
            textEditorCommand=explorerCommand="kfmclient exec"
        elif not olpc and got_program("gnome-open"):
            textEditorCommand=explorerCommand="gnome-open"
        elif got_program("nautilus"): explorerCommand="nautilus"
        elif got_program("rox"):
            # rox is available - try using that to open directories
            # (better not use it for editor as it might not be configured)
            # (TODO if both rox and gnome are available, can we tell which one the user prefers?)
            explorerCommand="rox"
        # anyway, see if we can find a nice editor
        for editor in ["leafpad","gedit","nedit","kedit","xedit"]:
            if got_program(editor):
                textEditorName=textEditorCommand=editor
                textEditorWaits = 1
                if textEditorName.endswith("edit"):
                    textEditorName=textEditorName[:-4]+"-"+textEditorName[-4:]
                    textEditorName=textEditorName[0].upper()+textEditorName[1:]
                break
    # End of finding editor - now start GUI
    try:
        import thread,Tkinter,tkMessageBox
        forceRadio=(macsound and 8.49<Tkinter.TkVersion<8.59) # indicatoron doesn't do very well in OS X 10.6 (Tk 8.5) unless we patched it
        if olpc:
            def interrupt_main(): os.kill(os.getpid(),2) # sigint
            thread.interrupt_main = interrupt_main
            # (os.kill is more reliable than interrupt_main() on OLPC, *but* on Debian Sarge (2.4 kernel) threads are processes so DON'T do this.)
        elif not hasattr(thread,"interrupt_main"): emulated_interruptMain = 1
        elif signal: # work around the "int object is not callable" thing on some platforms' interrupt_main
            def raise_int(*args): raise KeyboardInterrupt
            signal.signal(signal.SIGINT,raise_int)
    except RuntimeError:
        useTK = 0
        if __name__=="__main__": show_warning("Cannot start the GUI due to a Tk error")
    except ImportError:
        useTK = 0
        if __name__=="__main__" and not riscos_sound: show_warning("Cannot start the GUI because tkinter package is not installed on this system"+cond(fileExists("/var/lib/dpkg/status")," (try python-tk in Debian)","")+".")

def openDirectory(dir,inGuiThread=0):
    if winCEsound:
        if not dir[0]=="\\": dir=os.getcwd()+cwd_addSep+dir # must be absolute
        ctypes.cdll.coredll.ShellExecuteEx(ctypes.byref(ShellExecuteInfo(60,File=u"\\Windows\\fexplore",Parameters=u""+dir)))
    elif explorerCommand:
        if ' ' in dir: dir='"'+dir+'"'
        cmd = explorerCommand+" "+dir
        if winsound or mingw32: cmd="start "+cmd # (not needed on XP but is on Vista)
        elif unix: cmd += "&"
        os.system(cmd)
    else:
        msg = ""
        if not dir.startswith(os.sep): msg=" (in %s)" % os.getcwd()
        msg = "Don't know how to start the file explorer.  Please open the %s directory%s" % (dir,msg)
        if inGuiThread: tkMessageBox.showinfo(app.master.title(),msg)
        else: waitOnMessage(msg)

def sanityCheck(text,language,pauseOnError=0): # text is utf-8; returns error message if any
    if not text: return # always OK empty strings
    if pauseOnError:
        ret = sanityCheck(text,language)
        if ret: waitOnMessage(ret)
        return ret
    if language=="zh":
        allDigits = True
        for t in text:
            if ord(t)>127: return # got hanzi or tone marks
            if t in "12345": return # got tone numbers
            if t not in "0123456789. ": allDigits = False
        if allDigits: return
        return "Pinyin needs tones.  Please go back and add tone numbers to "+text+"."+cond(startBrowser("http://www.pristine.com.tw/lexicon.php?query="+fix_pinyin(text,[]).replace("1","1 ").replace("2","2 ").replace("3","3 ").replace("4","4 ").replace("5"," ").replace("  "," ").strip(wsp).replace(" ","+"))," Gradint has pointed your web browser at an online dictionary that might help.","")

def check_for_slacking():
    if fileExists(progressFile): checkAge(progressFile,localise("It has been %d days since your last Gradint lesson.  Please try to have one every day."))
    else:
        installDateFile = progressFile.replace("progress","installed")
        if not fileExists(installDateFile):
            try: open(installDateFile,"w")
            except: pass
        else: checkAge(installDateFile,localise("It has been %d days since you installed Gradint and you haven't had a lesson yet.  Please try to have one every day."))
def checkAge(fname,message):
    days = int((time.time()-os.stat(fname)[8])/3600/24)
    if days>=5 and (days%5)==0: waitOnMessage(message % days)

def s60_addVocab():
  label1,label2 = u""+localise("Word in %s") % localise(secondLanguage),u""+localise("Meaning in %s") % localise(firstLanguage)
  while True:
    result = appuifw.multi_query(label1,label2) # unfortunately multi_query can't take default items (and sometimes no T9!), but Form is too awkward (can't see T9 mode + requires 2-button save via Options) and non-multi query would be even more modal
    if not result: return # cancelled
    l2,l1 = result # guaranteed to both be populated
    while sanityCheck(l2.encode('utf-8'),secondLanguage,1):
        l2=appuifw.query(label1,"text",u"")
        if not l2: return # cancelled
    # TODO detect duplicates like Tk GUI does?
    appuifw.note(u"Added "+l2+"="+l1,"conf")
    appendVocabFileInRightLanguages().write((l2+"="+l1+"\n").encode("utf-8"))
def s60_changeLang():
    global firstLanguage,secondLanguage
    result = appuifw.multi_query(u""+localise("Your first language")+" (e.g. "+firstLanguage+")",u""+localise("second")+" (e.g. "+secondLanguage+")")
    if not result: return # cancelled
    l1,l2 = result
    firstLanguage,secondLanguage = l1.encode('utf-8').lower(),l2.encode('utf-8').lower()
    updateSettingsFile(settingsFile,{"firstLanguage":firstLanguage,"secondLanguage":secondLanguage})
def s60_runLesson():
    global maxLenOfLesson
    ml = appuifw.query(u"Max number of minutes","number",int(maxLenOfLesson/60))
    if not ml: return
    maxLenOfLesson = int(float(ml)*60)
    lesson_loop()
def s60_viewVocab():
    global justSynthesize
    doLabel("Reading your vocab list, please wait...")
    vList = map(lambda (l2,l1):l2+u"="+l1, guiVocabList(parseSynthVocab(vocabFile,1)))
    if not vList: return waitOnMessage("Your computer-voiced vocab list is empty.")
    while True:
      appuifw.app.body = None
      sel = appuifw.selection_list(vList,search_field=1)
      if sel==None: return
      l2,l1 = vList[sel].split("=",1)
      action = appuifw.popup_menu([u"Speak (just "+secondLanguage+")",u"Speak ("+secondLanguage+" and "+firstLanguage+")",u"Change "+secondLanguage,u"Change "+firstLanguage,u"Delete item",u"Cancel"], vList[sel])
      if action==0 or action==1:
        doLabel("Speaking...")
        justSynthesize = secondLanguage+" "+l2.encode('utf-8')
        if action==1: justSynthesize += ("#"+firstLanguage+" "+l1.encode('utf-8'))
        just_synthesize()
        justSynthesize = ""
      elif action==5: pass
      else:
          if action==4 and not getYN(u"Are you sure you want to delete "+vList[sel]+"?"): continue
          oldL1,oldL2 = l1,l2
          if action==2:
              first=1
              while first or (l2 and sanityCheck(l2.encode('utf-8'),secondLanguage,1)):
                  first=0 ; l2=appuifw.query(u""+secondLanguage,"text",l2)
              if not l2: continue
          elif action==3:
              l1 = appuifw.query(u""+firstLanguage,"text",l1)
              if not l1: continue
          doLabel("Processing")
          delOrReplace(oldL2,oldL1,l2,l1,cond(action==4,"delete","replace"))
          if action==4:
              del vList[sel]
              if not vList: return # empty
          else: vList[sel] = l2+"="+l1
def android_addVocab():
  while True:
    l2 = None
    while not l2 or sanityCheck(l2.encode('utf-8'),secondLanguage,1):
      l2 = android.dialogGetInput("Add word","Word in %s" % localise(secondLanguage)).result
      if not l2: return # cancelled
    l1 = android.dialogGetInput("Add word","Meaning in %s" % localise(firstLanguage)).result
    if not l1: return # cancelled
    # TODO detect duplicates like Tk GUI does?
    android.makeToast(u"Added "+l2+"="+l1)
    appendVocabFileInRightLanguages().write((l2+"="+l1+"\n").encode("utf-8"))
def android_changeLang():
    global firstLanguage,secondLanguage
    l1 = android.dialogGetInput("Gradint","Enter your first language",firstLanguage).result
    if not l1: return # cancelled
    l2 = android.dialogGetInput("Gradint","Enter your second language",secondLanguage).result
    if not l2: return # cancelled
    firstLanguage,secondLanguage = l1.encode('utf-8').lower(),l2.encode('utf-8').lower()
    updateSettingsFile(settingsFile,{"firstLanguage":firstLanguage,"secondLanguage":secondLanguage})

def delOrReplace(L2toDel,L1toDel,newL2,newL1,action="delete"):
    langs = [secondLanguage,firstLanguage]
    v=u8strip(read(vocabFile)).replace("\r\n","\n").replace("\r","\n")
    if paranoid_file_management:
        fname = os.tempnam()
        o = open(fname,"w")
    else: o=open(vocabFile,"w")
    found = 0
    if last_u8strip_found_BOM: o.write('\xef\xbb\xbf') # re-write it
    v=v.split("\n")
    if v and not v[-1]: v=v[:-1] # don't add an extra blank line at end
    for l in v:
        l2=l.lower()
        if l2.startswith("set language ") or l2.startswith("set languages "):
            langs=l.split()[2:] ; o.write(l+"\n") ; continue
        thisLine=map(lambda x:x.strip(wsp),l.split("=",len(langs)-1))
        if (langs==[secondLanguage,firstLanguage] and thisLine==[L2toDel.encode('utf-8'),L1toDel.encode('utf-8')]) or (langs==[firstLanguage,secondLanguage] and thisLine==[L1toDel.encode('utf-8'),L2toDel.encode('utf-8')]):
            # delete this line.  and maybe replace it
            found = 1
            if action=="replace":
                if langs==[secondLanguage,firstLanguage]: o.write(newL2.encode("utf-8")+"="+newL1.encode("utf-8")+"\n")
                else: o.write(newL1.encode("utf-8")+"="+newL2.encode("utf-8")+"\n")
        else: o.write(l+"\n")
    o.close()
    if paranoid_file_management:
        write(vocabFile,read(fname))
        os.remove(fname)
    return found

def maybeCanSynth(lang): return lang in synth_partials_voices or get_synth_if_possible(lang,0) or synthCache
def android_main_menu():
  while True:
    menu=[]
    if maybeCanSynth(secondLanguage):
        menu.append((u"Just speak a word",primitive_synthloop))
        doVocab = maybeCanSynth(firstLanguage)
        if doVocab: menu.append((u"Add word to my vocab",android_addVocab))
        menu.append((u"Make lesson from vocab",lesson_loop))
        # if doVocab: menu.append((u"View/change vocab",android_viewVocab)) # (TODO but lower priority because SL4A has an editor)
    else: menu.append((u"Make lesson",lesson_loop))
    menu += [(u"Record word(s) with mic",android_recordWord),(u"Change languages",android_changeLang)]
    menu.append((u"Quit",None))
    android.dialogCreateAlert("Gradint","Choose an action")
    android.dialogSetItems(map (lambda x:x[0], menu))
    android.dialogShow()
    function = menu[android.dialogGetResponse().result['item']][1]
    if function: function()
    else: break
def s60_main_menu():
  while True:
    appuifw.app.body = None # NOT text saying version no etc - has distracting blinking cursor
    menu=[]
    if maybeCanSynth(secondLanguage):
        menu.append((u"Just speak a word",primitive_synthloop))
        doVocab = maybeCanSynth(firstLanguage)
        if doVocab: menu.append((u"Add word to my vocab",s60_addVocab))
        menu.append((u"Make lesson from vocab",s60_runLesson))
        if doVocab: menu.append((u"View/change vocab",s60_viewVocab))
    else: menu.append((u"Make lesson",s60_runLesson))
    menu += [(u"Record word(s) with mic",s60_recordWord),(u"Change languages",s60_changeLang)]
    if len(menu)<5: menu.append((u"Quit",None)) # see comment below
    choice = appuifw.popup_menu(map (lambda x:x[0], menu),u"Choose an action:") # (selection_list can be better than popup_menu(l,u"Choose an action:") if over 5 items, but may need further trimming the width of each item) (the Quit item can go however - can cancel the menu instead.  Or keep it & don't mind it being off-screen c.f. in-vocab-list popup.)
    try: function = menu[choice][1]
    except: break
    if function: function()
    else: break

def downloadLAME():
    # Sourceforge keep making this harder!
    return not system("""if which curl >/dev/null 2>/dev/null; then export Curl="curl -L"; else export Curl="wget -O -"; fi
if ! test -e lame*.tar.gz; then
  export Link="$($Curl "http://sourceforge.net/project/showfiles.php?group_id=290&package_id=309"|grep tar.gz|head -1)"
  echo "Got HTML: $Link" 1>&2
  export Link="$(echo "$Link"|sed -e 's,href="/,href="http://sourceforge.net/,' -e 's/.*http:/http:/' -e 's/.tar.gz.*/.tar.gz/')"
  echo "Following link to $Link" 1>&2
  if ! $Curl "$Link" > lame.tar.gz; then
    rm -f lame.tar.gz; exit 1
  fi
  if grep downloads.sourceforge lame.tar.gz 2>/dev/null; then
    export Link="$(cat lame.tar.gz|grep downloads.sourceforge|head -1)"
    echo "Got HTML 2: $Link" 1>&2
    export Link="$(echo "$Link"|sed -e 's/.*http/http/' -e 's,.*/projects,http://sourceforge.net/projects,' -e 's/".*//')"
    echo "Following link 2 to $Link" 1>&2
    if ! $Curl "$Link" > lame.tar.gz; then
      rm -f lame.tar.gz; exit 1
    fi
  fi
fi""")

def gui_event_loop():
    app.todo.set_main_menu = 1 ; braveUser = 0
    global disable_once_per_day
    if disable_once_per_day==2:
      disable_once_per_day = cond(getYN(localise("Do you want Gradint to start by itself and remind you to practise?")),0,1)
      updateSettingsFile("advanced"+dottxt,{"disable_once_per_day":disable_once_per_day})
      if disable_once_per_day: # signal the background process to stop next time
        try: os.remove("background"+dottxt)
        except: pass
    if orig_onceperday&2: check_for_slacking()
    while app:
        while not hasattr(app,"menu_response"):
            if warnings_printed: waitOnMessage("") # If running gui_event_loop, better put any warnings in a separate dialogue now, rather than waiting for user to get one via 'make lesson' or some other method
            if hasattr(app,"needVocablist") and not hasattr(app,"vocabList"):
                v = guiVocabList(parseSynthVocab(vocabFile,1)) # (in non-GUI thread because can take a while when large)
                if app: app.vocabList = v # check again because there's a race condition if close the app while parseSynthVocab is running
                else: return
                del v
            if emulated_interruptMain: check_for_interrupts()
            time.sleep(0.3)
        menu_response = app.menu_response
        if menu_response=="input": # WMstandard
            app.todo.input_response=raw_input()
        elif menu_response=="go":
            gui_outputTo_start()
            if not soundCollector: app.todo.add_briefinterrupt_button = 1
            try: lesson_loop()
            except PromptException,prEx: waitOnMessage("Problem finding prompts:\n"+prEx.message) # and don't quit, user may be able to fix
            except KeyboardInterrupt: pass # probably pressed Cancel Lesson while it was still being made (i.e. before handleInterrupt)
            if app and not soundCollector: app.todo.remove_briefinterrupt_button = 1 # (not app if it's closed by the close box)
            gui_outputTo_end()
            if not app: return # (closed by the close box)
            else: app.todo.set_main_menu = 1
        elif menu_response=="edit":
            if not braveUser and fileExists(vocabFile) and open(vocabFile).readline().find("# This is vocab.txt.")==-1: braveUser=1
            if winCEsound:
                if braveUser or getYN("You must read what it says and keep to the same format.  Continue?"):
                    braveUser = 1
                    # WinCE Word does not save non-Western characters when saving plain text (even if there's a Unicode "cookie")
                    waitOnMessage("WARNING: Word may not save non-Western characters properly.  Try an editor like MADE instead (need to set its font).") # TODO Flinkware MADE version 2.0.0 has been known to insert spurious carriage returns at occasional points in large text files
                    if not app.fileToEdit[0]=="\\": app.fileToEdit=os.getcwd()+cwd_addSep+app.fileToEdit # must be absolute
                    if not fileExists(app.fileToEdit): open(app.fileToEdit,"w") # at least make sure it exists
                    ctypes.cdll.coredll.ShellExecuteEx(ctypes.byref(ShellExecuteInfo(60,File=u""+app.fileToEdit)))
                    waitOnMessage("When you've finished editing "+app.fileToEdit+", close it and start gradint again.")
                    return
            elif textEditorCommand:
                if braveUser or getYN("Open "+app.fileToEdit+" in "+textEditorName+"?\n(You must read what it says and keep to the same format.)"):
                    braveUser = 1 ; fileToEdit=app.fileToEdit
                    if not fileExists(fileToEdit): open(fileToEdit,"w") # at least make sure it exists
                    if textEditorWaits:
                        oldContents = read(fileToEdit)
                        if paranoid_file_management: # run the editor on a temp file instead (e.g. because gedit can fail when saving over ftpfs)
                            fileToEdit=os.tempnam()+dottxt
                            open(fileToEdit,"w").write(oldContents)
                    cmd = textEditorCommand+" "+fileToEdit
                    if textEditorWaits:
                        if macsound: app.todo.thindown="Waiting for you to close the "+textEditorName+" window"
                        else: app.todo.thindown="Waiting for you to quit "+textEditorName
                        t = time.time()
                        system(cmd)
                        if time.time() < t+3: waitOnMessage(textEditorName+" returned control to Gradint in less than 3 seconds.  Perhaps you already had an instance running and it loaded the file remotely.  Press OK when you have finished editing the file.")
                        newContents = read(fileToEdit)
                        if not newContents==oldContents:
                            if paranoid_file_management: write(app.fileToEdit,newContents)
                            if app.fileToEdit==vocabFile:
                                app.wordsExist=1 ; del app.vocabList # re-read
                            else: waitOnMessage("The changes you made to "+app.fileToEdit+" will take effect when you quit Gradint and start it again.")
                        del oldContents,newContents
                        if paranoid_file_management: os.remove(fileToEdit) # the temp file
                        app.todo.set_main_menu = "test" # back to the Add/Test screen
                    else: # not textEditorWaits
                        if winsound or mingw32: cmd="start "+cmd
                        elif unix: cmd += "&"
                        os.system(cmd)
                        waitOnMessage("Gradint has started "+textEditorName+", and will now quit.\nWhen you have finished editing "+app.fileToEdit+", save it and start gradint again.")
                        return
            else: waitOnMessage("Don't know how to start the text editor.  Please edit %s yourself (in %s)" % (app.fileToEdit,os.getcwd()))
        elif menu_response=="samples":
            setup_samplesDir_ifNec()
            openDirectory(samplesDirectory)
        elif menu_response=="samplesCopy":
            for i in app.toOpen:
                setup_samplesDir_ifNec(i)
                openDirectory(i)
            del app.toOpen
            waitOnMessage("Gradint has opened both of the recorded words folders, so you can copy things across.")
        elif menu_response=="test":
            text1 = asUnicode(app.Text1.get()).encode('utf-8') ; text2 = asUnicode(app.Text2.get()).encode('utf-8')
            if not text1 and not text2: app.todo.alert=u"Before pressing the "+localise("Speak")+u" button, you need to type the text you want to hear into the box."
            else:
              msg=sanityCheck(text1,secondLanguage)
              if msg: app.todo.alert=u""+msg
              else:
                app.set_watch_cursor = 1 ; app.toRestore = []
                global justSynthesize ; justSynthesize = ""
                def doControl(text,lang,control):
                    global justSynthesize
                    restoreTo = asUnicode(control.get())
                    if text:
                        if can_be_synthesized("!synth:"+text+"_"+lang): justSynthesize += ("#"+lang+" "+text)
                        else: app.todo.alert="Cannot find a synthesizer that can say '"+text+"' in language '"+lang+"' on this system"
                        t=transliterates_differently(text,lang)
                        if t: # (don't go straight into len() stuff, it could be None)
                          if unix and len(t)>300 and hasattr(app,"isBigPrint"): app.todo.alert="Transliteration suppressed to work around Ubuntu bug 731424" # https://bugs.launchpad.net/ubuntu/+bug/731424
                          else:
                            control.set(t) ; app.toRestore.append((control,t,restoreTo))
                doControl(text1,secondLanguage,app.Text1)
                def doSynth(openDir=True):
                    gui_outputTo_start() ; just_synthesize() ; gui_outputTo_end(openDir)
                    global justSynthesize ; justSynthesize = ""
                    if app: app.unset_watch_cursor = 1 # otherwise was closed by the close box
                if text1 and text2:
                  if app and hasattr(app,"outputTo") and app.outputTo.get() and not app.outputTo.get()=="0":
                    if getYN("Save %s and %s to separate files?" % (secondLanguage,firstLanguage)): doSynth(False)
                  elif ask_teacherMode: # Do the L2, then ask if actually WANT the L1 as well (might be useful on WinCE etc, search-and-demonstrate-L2)
                    doSynth()
                    if app and not getYN("Also speak the %s?" % firstLanguage):
                      if app: del app.menu_response
                      continue
                doControl(text2,firstLanguage,app.Text2)
                doSynth()
        elif menu_response=="mp3web":
          url=[] ; text1 = asUnicode(app.Text1.get())
          for c in list(text1.encode("utf-8")):
            if ord(',')<=ord(c)<=ord('9') or ord('a')<=ord(c.lower())<=ord('z'): url.append(c)
            else: url.append("%"+hex(ord(c))[2:])
          def scanDirs():
           dd={} ; found=0
           for d in downloadsDirs:
            if isDirectory(d):
             found=1
             for f in os.listdir(d): dd[d+os.sep+f]=1
           return dd,found
          oldLs,found = scanDirs()
          if downloadsDirs and not found: app.todo.alert=localise("Please set downloadsDirs in advanced"+dottxt)
          elif not url: app.todo.alert=localise("You need to type a word in the box before you can press this button")
          elif not startBrowser(mp3web.replace("$Word","".join(url)).replace("$Lang",secondLanguage)): app.todo.alert = localise("Can't start the web browser")
          elif downloadsDirs:
            waitOnMessage(localise("If the word is there, download it. When you press OK, Gradint will check for downloads."))
            if not app: break
            found=0
            for f in scanDirs()[0].keys():
              if not f in oldLs and (f.lower().endswith(dotmp3) or f.lower().endswith(dotwav)) and getYN("Use "+f[f.rfind(os.sep)+1:]+"?"): # TODO don't ask this question too many times if there are many and they're all 'no'
                system("mp3gain -r -s r -k -d 10 \""+f+"\"") # (if mp3gain command is available; ignore errors if not (TODO document in advanced.txt)) (note: doing here not after the move, in case synthCache is over ftpfs mount or something)
                uf=scFile=text1.encode("utf-8")+"_"+secondLanguage+f[-4:].lower()
                try:
                  if winCEsound: raise IOError
                  else: o=open(synthCache+os.sep+scFile,"wb")
                except IOError:
                  uf=unicode2filename(text1+"_"+secondLanguage+f[-4:].lower())
                  o=open(synthCache+os.sep+uf,"wb")
                  synthCache_transtbl[scFile]=uf
                  open(synthCache+os.sep+transTbl,'a').write(uf+" "+scFile+"\n")
                synthCache_contents[uf]=1
                o.write(open(f,"rb").read()) ; o.close() ; os.remove(f)
                app.lastText1 = 1 # ensure different
                found=1 ; break
            if not found: app.todo.alert="No new sounds found"
        elif menu_response=="get-encoder":
          if winsound or mingw32:
            assert 0, "Windows Media Encoder no longer available for new installations"
            #if getYN("Gradint can use Windows Media Encoder to make WMA files, which can be played on most pocket MP3 players and mobiles etc.  Do you want to go to the Microsoft site to install Windows Media Encoder now?"):
            #  if not startBrowser('http://www.microsoft.com/windows/windowsmedia/forpros/encoder/default.mspx'): app.todo.alert = "There was a problem starting the web browser.  Please install manually (see notes in advanced.txt)."
            #  else:
            #    app.setLabel("Waiting for you to install Media Encoder")
            #    while not fileExists(programFiles+"\\Windows Media Components\\Encoder\\WMCmd.vbs"): time.sleep(1)
          else:
            if getYN("Do you really want to download and compile the LAME MP3 encoder? (this may take a while)"):
              app.setLabel("Downloading...") ; worked=0
              while True:
                if downloadLAME():
                  worked=1 ; break
                if not getYN("Download failed.  Try again?"): break
              if worked:
                app.setLabel("Compiling...")
                if system("""tar -zxvf lame*.tar.gz && cd lame-* && if ./configure && make; then ln -s $(pwd)/frontend/lame ../lame || true; else cd .. ; rm -rf lame*; exit 1; fi"""): app.todo.alert = "Compile failed"
          app.todo.set_main_menu = 1
        elif (menu_response=="add" or menu_response=="replace") and not (app.Text1.get() and app.Text2.get()): app.todo.alert="You need to type text in both boxes before adding the word/meaning pair to "+vocabFile
        elif menu_response=="add" and hasattr(app,"vocabList") and (asUnicode(app.Text1.get()),asUnicode(app.Text2.get())) in app.vocabList:
            # Trying to add a word that's already there - do we interpret this as a progress adjustment?
            app.set_watch_cursor = 1
            t1,t2 = asUnicode(app.Text1.get()),asUnicode(app.Text2.get())
            lang2,lang1=t1.lower(),t2.lower() # because it's .lower()'d in progress.txt
            d = ProgressDatabase(0)
            l1find = "!synth:"+lang1.encode('utf-8')+"_"+firstLanguage
            found = 0
            msg=(u""+localise("%s=%s is already in %s.")) % (t1,t2,vocabFile)
            for listToCheck in [d.data,d.unavail]:
              if found: break
              for item in listToCheck:
                if (item[1]==l1find or (type(item[1])==type([]) and l1find in item[1])) and item[2]=="!synth:"+lang2.encode('utf-8')+"_"+secondLanguage:
                    if not item[0]: break # not done yet - as not-found
                    newItem0 = reviseCount(item[0])
                    app.unset_watch_cursor = 1
                    if getYN(msg+" "+localise("Repeat count is %d. Reduce this to %d for extra revision?" % (item[0],newItem0))):
                        app.set_watch_cursor = 1
                        listToCheck.remove(item)
                        listToCheck.append((newItem0,item[1],item[2]))
                        d.save() ; app.unset_watch_cursor = 1
                        app.todo.clear_text_boxes = 1
                    found = 1 ; break
            if not found:
                app.unset_watch_cursor = 1
                app.todo.alert=msg+" "+localise("Repeat count is 0, so we cannot reduce it for extra revision.")
        elif menu_response=="add":
            text1 = asUnicode(app.Text1.get()).encode('utf-8') ; text2 = asUnicode(app.Text2.get()).encode('utf-8')
            msg=sanityCheck(text1,secondLanguage)
            if msg: app.todo.alert=u""+msg
            else:
                o=appendVocabFileInRightLanguages()
                o.write(text1+"="+text2+"\n") # was " = " but it slows down parseSynthVocab
                o.close()
                if paranoid_file_management:
                    if filelen(vocabFile)<filelen(vocabFile+"~") or chr(0) in open(vocabFile).read(1024): app.todo.alert="Vocab file corruption! You'd better restore the ~ backup."
                if hasattr(app,"vocabList"): app.vocabList.append((ensure_unicode(text1),ensure_unicode(text2)))
                app.todo.clear_text_boxes=app.wordsExist=1
        elif menu_response=="delete" or menu_response=="replace":
            app.set_watch_cursor = 1
            lang2,lang1 = app.toDelete
            t1,t2 = asUnicode(app.Text1.get()),asUnicode(app.Text2.get()) # take it now in case the following takes a long time and user tries to change
            if winCEsound: # hack because no watch cursor and can take time
                app.Text1.set("Please wait") ; app.Text2.set("wait...")
            found = delOrReplace(lang2,lang1,t1,t2,menu_response)
            if found and menu_response=="replace": # maybe hack progress.txt as well (taken out of the above loop for better failsafe)
                d = ProgressDatabase(0)
                lang2,lang1=lang2.lower(),lang1.lower() # because it's .lower()'d in progress.txt
                l1find = "!synth:"+lang1.encode('utf-8')+"_"+firstLanguage
                for item in d.data:
                    if (item[1]==l1find or (type(item[1])==type([]) and l1find in item[1])) and item[2]=="!synth:"+lang2.encode('utf-8')+"_"+secondLanguage and item[0]:
                        app.unset_watch_cursor = 1
                        if not getYN(localise("You have repeated %s=%s %d times.  Do you want to pretend you already repeated %s=%s %d times?") % (lang2,lang1,item[0],t2,t1,item[0])):
                            app.set_watch_cursor = 1 ; break
                        d.data.remove(item)
                        l1replace = "!synth:"+t2.encode('utf-8')+"_"+firstLanguage
                        if type(item[1])==type([]):
                            l = item[1]
                            l[l.index(l1find)] = l1replace
                        else: l=l1replace
                        item = (item[0],l,"!synth:"+t1.encode('utf-8')+"_"+secondLanguage)
                        d.data.append(item)
                        app.set_watch_cursor = 1
                        for i2 in d.unavail:
                            if i2[1:]==item[1:]:
                                d.unavail.remove(i2) # because we updated the item above - don't want duplicates
                                break
                        d.save()
                        break
            del app.vocabList # re-read
            app.todo.clear_text_boxes=1
            app.unset_watch_cursor = 1
            if not found: app.todo.alert = "OOPS: Item to delete/replace was not found in "+vocabFile
        if app: del app.menu_response

def vocabLinesWithLangs(): # used for merging different users' vocab files
    langs = [secondLanguage,firstLanguage] ; ret = []
    try: v=u8strip(read(vocabFile)).replace("\r","\n")
    except IOError: v=""
    for l in v.split("\n"):
        l2=l.lower()
        if l2.startswith("set language ") or l2.startswith("set languages "): langs=l.split()[2:]
        elif l: ret.append((tuple(langs),l)) # TODO what about blank lines? (currently they'd be considered duplicates)
    return ret

def appendVocabFileInRightLanguages():
    # check if we need a SET LANGUAGE
    langs = [secondLanguage,firstLanguage]
    try: v=u8strip(read(vocabFile)).replace("\r","\n")
    except IOError: v=""
    for l in v.split("\n"):
        l2=l.lower()
        if l2.startswith("set language ") or l2.startswith("set languages "): langs=l.split()[2:]
    o=open(vocabFile,"a")
    if not v.endswith("\n"): o.write("\n")
    if not langs==[secondLanguage,firstLanguage]: o.write("SET LANGUAGES "+secondLanguage+" "+firstLanguage+"\n")
    return o

def transliterates_differently(text,lang):
    global last_partials_transliteration ; last_partials_transliteration=None
    global partials_are_sporadic ; o=partials_are_sporadic ; partials_are_sporadic = None # don't want to touch the counters here
    if synthcache_lookup("!synth:"+text+"_"+lang):
        partials_are_sporadic = o
        if last_partials_transliteration and not last_partials_transliteration==text: return last_partials_transliteration
        else: return # (don't try to translit. if was in synth cache - will have no idea which synth did it)
    partials_are_sporadic = o
    synth=get_synth_if_possible(lang,0) # not to_transliterate=True this time because we want the synth that actually synth'd it (may have done it differently from the transliterating synth)
    if not synth or not synth.can_transliterate(lang): return
    translit=synth.transliterate(lang,text,forPartials=0)
    if translit and not translit==text: return translit

def gui_outputTo_start():
    if hasattr(app,"outputTo") and app.outputTo.get() and not app.outputTo.get()=="0":
        global outputFile,gui_output_directory,oldGID ; outputFile=None
        if type(gui_output_directory)==type([]):
            oldGID = gui_output_directory
            for d in gui_output_directory:
                if d and d[-1]=="*" and len(os.listdir(d[:-1]))==1: d=d[:-1]+os.listdir(d[:-1])[0]
                if isDirectory(d):
                    gui_output_directory = d ; break
        if type(gui_output_directory)==type([]): gui_output_directory=gui_output_directory[-1]
        try: os.mkdir(gui_output_directory)
        except: pass
        gui_output_counter = 1 # now local because we also got prefix
        if justSynthesize:
            if '#' in justSynthesize[1:]: prefix="" # multiple languages
            else: # prefix the language that's being synth'd
                prefix=justSynthesize.split()[0]
                if prefix.startswith('#'): prefix=prefix[1:]
        else: prefix = "lesson"
        while not outputFile or fileExists(outputFile):
            outputFile=gui_output_directory+os.sep+prefix+str(gui_output_counter)+extsep+app.outputTo.get()
            gui_output_counter += 1
        global write_to_stdout ; write_to_stdout = 0
        global out_type ; out_type = app.outputTo.get()
        global need_run_media_encoder
        if out_type=="wma" or (out_type=="aac" and not (got_program("neroAacEnc") or got_program("faac"))):
            need_run_media_encoder = (out_type,outputFile)
            out_type="wav" ; outputFile=os.tempnam()+dotwav
        else: need_run_media_encoder = 0
        setSoundCollector(SoundCollector())
        global waitBeforeStart, waitBeforeStart_old
        waitBeforeStart_old = waitBeforeStart ; waitBeforeStart = 0
def gui_outputTo_end(openDir=True):
    global outputFile, waitBeforeStart, oldGID, gui_output_directory
    if outputFile:
        no_output = not soundCollector.tell() # probably 'no words to put in the lesson'
        setSoundCollector(None)
        if no_output: os.remove(outputFile)
        elif need_run_media_encoder:
            t,f = need_run_media_encoder
            oldF = f
            if cygwin:
                o=outputFile.replace("/","\\")
                if o.lower().startswith("\\cygdrive\\"): o=o[10]+":"+o[11:] # reverse \cygdrive paths back to DOS (in case used for temp dirs etc)
                if o.startswith("\\"): o="C:\\cygwin"+o # e.g. c:\cygwin\tmp
                f=f.replace("/","\\")
            else: o=outputFile
            if t=="wma":
                pFiles = programFiles
                if cygwin: pFiles=os.environ.get("ProgramFiles","C:\\Program Files") # re-generate it (don't want Cygwin path version)
                # NB we're passing this to cmd, NOT bash:
                cmd = "cscript \""+pFiles+"\\Windows Media Components\\Encoder\\WMCmd.vbs\" -input \""+o+"\" -output \""+f+"\" -profile a20_1 -a_content 1"
            elif t=="aac": cmd="afconvert \""+o+"\" -d aac \""+f+"\"" # could also use "afconvert file.wav -d samr file.amr", but amr is bigger than aac and not as good; don't know if anyone has a device that plays amr but not aac.
            else: assert 0
            if cygwin:
                assert not "'" in cmd, "apostrophees in pathnames could cause trouble on cygwin"
                cmd="echo '"+cmd+" && exit' | cmd" # seems the only way to get it to work on cygwin
            system(cmd)
            os.remove(outputFile)
            if not fileExists(oldF):
                m = "This computer's "+t.upper()+" encoder failed to write any output.  Try a different format."
                if t=="wma": m += " (This condition can be caused by some program changing the registry entries for VBS scripts.)"
                app.todo.alert = m
                no_output = 1
        outputFile=None
        waitBeforeStart = waitBeforeStart_old
        if openDir and not no_output: openDirectory(gui_output_directory)
        try: gui_output_directory = oldGID
        except: pass

def main():
    global useTK,justSynthesize,waitBeforeStart,traceback,appTitle,app,warnings_toprint
    if useTK:
        if justSynthesize and not justSynthesize[-1]=='*': appTitle=cond('#' in justSynthesize,"Gradint","Reader") # not "language lesson"
        startTk()
    else:
        app = None # not False anymore
        if not appuifw and not android: # REALLY output them to stderr
            for w in warnings_toprint: show_warning(w)
        warnings_toprint = [] ; rest_of_main()
def rest_of_main():
    global useTK,justSynthesize,waitBeforeStart,traceback,appTitle,saveProgress,RM_running
    exitStatus = 0 ; RM_running = 1

    try:
        try: ceLowMemory
        except NameError: ceLowMemory=0
        if ceLowMemory and getYN("Low memory! Python may crash. Turn off progress saving for safety?"): saveProgress=0
        
        if justSynthesize=="-": primitive_synthloop()
        elif justSynthesize and justSynthesize[-1]=='*':
            justSynthesize=justSynthesize[:-1]
            waitBeforeStart = 0
            just_synthesize() ; lesson_loop()
        elif justSynthesize: just_synthesize()
        elif app and waitBeforeStart: gui_event_loop()
        elif appuifw: s60_main_menu()
        elif android: android_main_menu()
        else: lesson_loop()
    except SystemExit: pass
    except KeyboardInterrupt: pass
    except PromptException,prEx:
        waitOnMessage("\nProblem finding prompts:\n"+prEx.message+"\n")
        exitStatus = 1
    except:
        w="\nSomething has gone wrong with my program.\nThis is not your fault.\nPlease let me know what it says.\nThanks.  Silas\n"+exc_info()
        try: import traceback
        except:
            w += "Cannot import traceback\n"
            traceback = None
        if traceback and useTK: traceback.print_exc() # BEFORE waitOnMessage, in case Tk is stuck (hopefully the terminal is visible)
        try: tracebackFile=open("last-gradint-error"+extsep+"txt","w")
        except: tracebackFile=None
        if tracebackFile:
            try:
                tracebackFile.write(time.asctime()+":\n"+w+"\n")
                if traceback: traceback.print_exc(None,tracebackFile)
                tracebackFile.close()
                if traceback: w += "Details have been written to "+os.getcwd()+os.sep+"last-gradint-error"+extsep+"txt" # do this only if there's a traceback, otherwise little point
            except: pass
        try: # audio warning in case was away from computer.  Do this last as it may overwrite the exception.
            global soundCollector
            if app: soundCollector=0
            if not soundCollector and get_synth_if_possible("en",0): synth_event("en","Error in graddint program.").play() # if possible, give some audio indication of the error (double D to try to force correct pronunciation if not eSpeak, e.g. S60)
        except: pass
        waitOnMessage(w.strip())
        if not useTK:
            if tracebackFile: sys.stderr.write(read("last-gradint-error"+extsep+"txt"))
            elif traceback: traceback.print_exc() # will be wrong if there was an error in speaking
        exitStatus = 1
        if appuifw: raw_input() # so traceback stays visible
    # It is not guaranteed that __del__() methods are called for objects that still exist when the interpreter exits.  So:
    global viable_synths,getsynth_cache,theMp3FileCache
    del viable_synths,getsynth_cache,theMp3FileCache
    if app:
        app.todo.exit_ASAP=1
        while app: time.sleep(0.2)
    elif not app==None: pass # (gets here if WAS 'app' but was closed - DON'T output anything to stderr in this case)
    elif appuifw: appuifw.app.set_exit()
    elif riscos_sound: show_info("You may now close this Task Window.\n")
    elif not android: show_info("\n") # in case got any \r'd string there - don't want to confuse the next prompt
    RM_running = 0
    if exitStatus: sys.exit(exitStatus)

if __name__=="__main__": main() # Note: calling main() is the ONLY control logic that can happen under the 'if __name__=="__main__"' block; everything else should be in main() itself.  This is because gradint-wrapper.exe under Windows calls main() from the exe and does not call this block