FAQ | This is a LIVE service | Changelog

Skip to content
Snippets Groups Projects
frontend.py 114 KiB
Newer Older
Silas S. Brown's avatar
Silas S. Brown committed
# This file is part of the source code of
Silas S. Brown's avatar
Silas S. Brown committed
# gradint v0.9973 (c) 2002-2011 Silas S. Brown. GPL v3+.
Silas S. Brown's avatar
Silas S. Brown committed
#    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: 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)
Silas S. Brown's avatar
Silas S. Brown committed
        appuifw.query(u""+msg,'query')
Silas S. Brown's avatar
Silas S. Brown committed
    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
Silas S. Brown's avatar
Silas S. Brown committed
        while app and hasattr(app.todo,"alert"): time.sleep(0.5)
Silas S. Brown's avatar
Silas S. Brown committed
        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")
Silas S. Brown's avatar
Silas S. Brown committed
    warnings_printed = []
Silas S. Brown's avatar
Silas S. Brown committed

def getYN(msg,defaultIfEof="n"):
    if appuifw:
        appuifw.app.body = None
        return appuifw.query(u""+msg,'query')
    elif app:
        app.todo.question = localise(msg)
Silas S. Brown's avatar
Silas S. Brown committed
        while app and not hasattr(app,"answer_given"): time.sleep(0.5)
        if not app: raise SystemExit
Silas S. Brown's avatar
Silas S. Brown committed
        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():
Silas S. Brown's avatar
Silas S. Brown committed
    global justSynthesize,warnings_printed
Silas S. Brown's avatar
Silas S. Brown committed
    lang = None
Silas S. Brown's avatar
Silas S. Brown committed
    interactive = appuifw 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)
Silas S. Brown's avatar
Silas S. Brown committed
    while True:
        if appuifw:
Silas S. Brown's avatar
Silas S. Brown committed
            if not justSynthesize: justSynthesize=""
Silas S. Brown's avatar
Silas S. Brown committed
            justSynthesize=appuifw.query(u"Say:","text",u""+justSynthesize)
Silas S. Brown's avatar
Silas S. Brown committed
            if justSynthesize: justSynthesize=justSynthesize.encode("utf-8")
            else: break
Silas S. Brown's avatar
Silas S. Brown committed
        else:
Silas S. Brown's avatar
Silas S. Brown committed
            try: justSynthesize=raw_input(interactive)
Silas S. Brown's avatar
Silas S. Brown committed
            except EOFError: break
            if (winCEsound or riscos_sound) and not justSynthesize: break # because no way to send EOF (and we won't be taking i/p from a file)
Silas S. Brown's avatar
Silas S. Brown committed
        oldLang = lang
Silas S. Brown's avatar
Silas S. Brown committed
        if justSynthesize: lang = just_synthesize(interactive,lang)
Silas S. Brown's avatar
Silas S. Brown committed
        # 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
            # else they'll have already been printed
            warnings_printed = []
Silas S. Brown's avatar
Silas S. Brown committed
        if not lang: lang=oldLang
Silas S. Brown's avatar
Silas S. Brown committed

def startBrowser(url): # true if success
Silas S. Brown's avatar
Silas S. Brown committed
  if winCEsound: return None # user might be paying per byte! + difficult to switch back if no Alt-Tab program
Silas S. Brown's avatar
Silas S. Brown committed
  try: import webbrowser
  except: webbrowser=0
  if webbrowser:
      g=webbrowser.get()
Silas S. Brown's avatar
Silas S. Brown committed
      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)):
Silas S. Brown's avatar
Silas S. Brown committed
          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)
Silas S. Brown's avatar
Silas S. Brown committed
  # (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)
Silas S. Brown's avatar
Silas S. Brown committed

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
Silas S. Brown's avatar
Silas S. Brown committed
    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")
Silas S. Brown's avatar
Silas S. Brown committed
    # 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)
Silas S. Brown's avatar
Silas S. Brown committed
    if not app and needCountItems and cancelledFiles: show_info("(%d cancelled items)...\n" % len(cancelledFiles))
Silas S. Brown's avatar
Silas S. Brown committed
    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).
Silas S. Brown's avatar
Silas S. Brown committed
    # (NB addStatus now takes effect only when the list box is displayed anyway, so OK for buttons that might also be displayed without it)
Silas S. Brown's avatar
Silas S. Brown committed
    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)
Silas S. Brown's avatar
Silas S. Brown committed
def makeButton(parent,text,command):
Silas S. Brown's avatar
Silas S. Brown committed
    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)
Silas S. Brown's avatar
Silas S. Brown committed
    bindUpDown(button,True)
    return button
def addButton(parent,text,command,packing=None,status=None):
    button = makeButton(parent,text,command)
Silas S. Brown's avatar
Silas S. Brown committed
    if status: addStatus(button,status)
    if packing=="nopack": pass
Silas S. Brown's avatar
Silas S. Brown committed
    elif type(packing)==type(""): button.pack(side=packing)
Silas S. Brown's avatar
Silas S. Brown committed
    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)
    ctrl=cond(macsound,"<Command-","<Control-")
    m.add_command(label="Cut",command=(lambda e=e: e.widget.event_generate(ctrl+'x>')))
    m.add_command(label="Copy",command=(lambda e=e: e.widget.event_generate(ctrl+'-c>')))
    m.add_command(label="Paste",command=(lambda e=e: e.widget.event_generate(ctrl+'-v>')))
    m.add_command(label="Delete",command=(lambda e=e: e.widget.event_generate('<Delete>')))
    m.add_command(label="Select All",command=(lambda e=e: 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>')
Silas S. Brown's avatar
Silas S. Brown committed
def selectAllButNumber(e): # hack for recording.py - select all but any number at the start
Silas S. Brown's avatar
Silas S. Brown committed
    e.widget.event_generate('<Home>')
    for i in list(e.widget.theText.get()):
Silas S. Brown's avatar
Silas S. Brown committed
        if "0"<=i<="9" or i=="_": e.widget.event_generate('<Right>')
Silas S. Brown's avatar
Silas S. Brown committed
        else: return e.widget.event_generate('<Shift-End>')
def addTextBox(row,wide=0):
    text = Tkinter.StringVar(row)
    entry = Tkinter.Entry(row,textvariable=text)
    if winsound or mingw32 or cygwin or macsound: entry.bind('<Button-3>',CXVMenu)
    if macsound: entry.bind('<Control-Button-1>',CXVMenu)
Silas S. Brown's avatar
Silas S. Brown committed
    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
Silas S. Brown's avatar
Silas S. Brown committed
            text.set("(Push OK to type A-Z)") # (if changing this message, change it below too)
Silas S. Brown's avatar
Silas S. Brown committed
            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.
Silas S. Brown's avatar
Silas S. Brown committed
        def timeStamp(entry): entry.buttonPressTime=time.time()
        entry.bind('<ButtonPress-1>',lambda e:timeStamp(entry))
Silas S. Brown's avatar
Silas S. Brown committed
        global lastDblclkAdvisory,lastDblclk
        lastDblclkAdvisory=lastDblclk=0
Silas S. Brown's avatar
Silas S. Brown committed
        def pasteInstructions(t):
            if t>=0.5: # they probably want tap-and-hold, which we don't do properly
Silas S. Brown's avatar
Silas S. Brown committed
                global lastDblclkAdvisory
                if t<2 and (lastDblclkAdvisory>time.time()-30 or lastDblclk>time.time()-90): return # reduce repeated false +ves
                lastDblclkAdvisory=time.time()
Silas S. Brown's avatar
Silas S. Brown committed
                app.todo.alert="Double-click in the box if you want to replace it with the clipboard contents"
Silas S. Brown's avatar
Silas S. Brown committed
        def doPaste(text,entry):
            text.set(entry.selection_get(selection="CLIPBOARD"))
            global lastDblclk ; lastDblclk=time.time()
Silas S. Brown's avatar
Silas S. Brown committed
        entry.bind('<ButtonRelease-1>',lambda e:pasteInstructions(time.time()-getattr(entry,"buttonPressTime",time.time())))
Silas S. Brown's avatar
Silas S. Brown committed
        entry.bind('<Double-Button-1>',lambda e:doPaste(text,entry))
Silas S. Brown's avatar
Silas S. Brown committed
    # 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))))
Silas S. Brown's avatar
Silas S. Brown committed
    bindUpDown(entry,False)
Silas S. Brown's avatar
Silas S. Brown committed
    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
Silas S. Brown's avatar
Silas S. Brown committed
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)
Silas S. Brown's avatar
Silas S. Brown committed
def addLabelledBox(row,wide=0,status=None):
Silas S. Brown's avatar
Silas S. Brown committed
    label = addLabel(row,"") # will set contents later
    text,entry = addTextBox(row,wide)
Silas S. Brown's avatar
Silas S. Brown committed
    if status: addStatus(entry,status)
Silas S. Brown's avatar
Silas S. Brown committed
    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
Silas S. Brown's avatar
Silas S. Brown committed
    def getRow(row):
      if not row:
Silas S. Brown's avatar
Silas S. Brown committed
        row = Tkinter.Frame(parent)
        row.pack(fill=Tkinter.X,expand=1)
Silas S. Brown's avatar
Silas S. Brown committed
      return row
    GUIlang = GUI_languages.get(firstLanguage,firstLanguage)
    if "@variants-"+GUIlang in GUI_translations: # the firstLanguage has script variants
        row=getRow(row)
Silas S. Brown's avatar
Silas S. Brown committed
        if not hasattr(app,"scriptVariant"): app.scriptVariant = Tkinter.StringVar(app)
        count = 0
Silas S. Brown's avatar
Silas S. Brown committed
        for variant in GUI_translations["@variants-"+GUIlang]:
Silas S. Brown's avatar
Silas S. Brown committed
            Tkinter.Radiobutton(row, text=u" "+variant+u" ", variable=app.scriptVariant, value=str(count), indicatoron=forceRadio).pack({"side":"left"})
Silas S. Brown's avatar
Silas S. Brown committed
            count += 1
Silas S. Brown's avatar
Silas S. Brown committed
        app.scriptVariant.set(str(scriptVariants.get(GUIlang,0)))
Silas S. Brown's avatar
Silas S. Brown committed
    if synth_partials_voices and guiVoiceOptions:
Silas S. Brown's avatar
Silas S. Brown committed
        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)
Silas S. Brown's avatar
Silas S. Brown committed
    if not gotSox: return row # can't do any file output without sox
Silas S. Brown's avatar
Silas S. Brown committed
    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
Silas S. Brown's avatar
Silas S. Brown committed
    row=getRow(row)
Silas S. Brown's avatar
Silas S. Brown committed
    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
Silas S. Brown's avatar
Silas S. Brown committed
    def addFiletypeButton(fileType):
Silas S. Brown's avatar
Silas S. Brown committed
        ftu = fileType.upper()
Silas S. Brown's avatar
Silas S. Brown committed
        t = Tkinter.Radiobutton(rightrow, text=cond(forceRadio,""," ")+ftu+" ", variable=app.outputTo, value=fileType, indicatoron=forceRadio)
Silas S. Brown's avatar
Silas S. Brown committed
        bindUpDown(t,True)
Silas S. Brown's avatar
Silas S. Brown committed
        addStatus(t,"Select this to save a lesson or\na phrase to a%s %s file" % (cond(ftu[0] in "AEFHILMNORSX","n",""),ftu))
Silas S. Brown's avatar
Silas S. Brown committed
        t.pack({"side":"left"})
Silas S. Brown's avatar
Silas S. Brown committed
    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"})
Silas S. Brown's avatar
Silas S. Brown committed
    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)
Silas S. Brown's avatar
Silas S. Brown committed
    addStatus(t,"Select this to send all sounds to\nthe speaker, not to files on disk")
Silas S. Brown's avatar
Silas S. Brown committed
    bindUpDown(t,True)
Silas S. Brown's avatar
Silas S. Brown committed
    t.pack({"side":"left"})
Silas S. Brown's avatar
Silas S. Brown committed
    if got_program("lame"): addFiletypeButton("mp3")
    if got_windows_encoder: addFiletypeButton("wma")
    if 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)
Silas S. Brown's avatar
Silas S. Brown committed
    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)
    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")
Silas S. Brown's avatar
Silas S. Brown committed
    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 = []
Silas S. Brown's avatar
Silas S. Brown committed
    try: oldLines=u8strip(read(fname)).replace("\r\n","\n").split("\n")
Silas S. Brown's avatar
Silas S. Brown committed
    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))

Silas S. Brown's avatar
Silas S. Brown committed
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)
Silas S. Brown's avatar
Silas S. Brown committed

Silas S. Brown's avatar
Silas S. Brown committed
def setupScrollbar(parent,rowNo):
Silas S. Brown's avatar
Silas S. Brown committed
    s = Tkinter.Scrollbar(parent,takefocus=0)
Silas S. Brown's avatar
Silas S. Brown committed
    s.grid(row=rowNo,column=cond(winCEsound or olpc,0,1),sticky="ns"+cond(winCEsound or olpc,"w","e"))
Silas S. Brown's avatar
Silas S. Brown committed
    c=Tkinter.Canvas(parent,bd=0,width=200,height=100,yscrollcommand=s.set)
Silas S. Brown's avatar
Silas S. Brown committed
    c.grid(row=rowNo,column=cond(winCEsound or olpc,1,0),sticky="nsw")
Silas S. Brown's avatar
Silas S. Brown committed
    s.config(command=c.yview)
    scrolledFrame=Tkinter.Frame(c) ; c.create_window(0,0,window=scrolledFrame,anchor="nw")
Silas S. Brown's avatar
Silas S. Brown committed
    # 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)
    scrolledFrame.bind_all('<Button-4>',lambda *args:c.yview("scroll","-1","units"))
    scrolledFrame.bind_all('<Button-5>',lambda *args:c.yview("scroll","1","units"))
Silas S. Brown's avatar
Silas S. Brown committed
    # 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?)
Silas S. Brown's avatar
Silas S. Brown committed
    return scrolledFrame, c

# GUI presets buttons:
shortDescriptionName = "short-description"+dottxt
longDescriptionName = "long-description"+dottxt
class ExtraButton(object):
    def __init__(self,directory):
Silas S. Brown's avatar
Silas S. Brown committed
        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)
Silas S. Brown's avatar
Silas S. Brown committed
        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
Silas S. Brown's avatar
Silas S. Brown committed
        self.button = addButton(app.rightPanel,localise("Add ")+unicode(self.shortDescription,"utf-8"),self,{"fill":"x"})
Silas S. Brown's avatar
Silas S. Brown committed
        self.button["anchor"]="w"
Silas S. Brown's avatar
Silas S. Brown committed
    def __call__(self,*args):
Silas S. Brown's avatar
Silas S. Brown committed
        if not tkMessageBox.askyesno(app.master.title(),unicode(self.longDescription,"utf-8")+"\n"+localise("Add this to your collection?")): return
Silas S. Brown's avatar
Silas S. Brown committed
        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)
Silas S. Brown's avatar
Silas S. Brown committed
        except: os.mkdir(samplesDirectory)
Silas S. Brown's avatar
Silas S. Brown committed
        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")
Silas S. Brown's avatar
Silas S. Brown committed
            o.write(u8strip(read(newName+os.sep+"add-to-vocab"+dottxt)).strip(wsp)+"\n")
Silas S. Brown's avatar
Silas S. Brown committed
            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)
Silas S. Brown's avatar
Silas S. Brown committed
        if fileExists(newName+os.sep+"add-to-languages"+dottxt):
            changed = 0
Silas S. Brown's avatar
Silas S. Brown committed
            for lang in u8strip(read(newName+os.sep+"add-to-languages"+dottxt)).strip(wsp).split():
Silas S. Brown's avatar
Silas S. Brown committed
                if not lang in [firstLanguage,secondLanguage]+otherLanguages:
                    otherLanguages.append(lang) ; changed = 1
Silas S. Brown's avatar
Silas S. Brown committed
            if changed: sanitise_otherLanguages(), updateSettingsFile("advanced"+dottxt,{"otherLanguages":otherLanguages,"possible_otherLanguages":possible_otherLanguages})
Silas S. Brown's avatar
Silas S. Brown committed
            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)
Silas S. Brown's avatar
Silas S. Brown committed
        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()
Silas S. Brown's avatar
Silas S. Brown committed
        app.wordsExist = 1
Silas S. Brown's avatar
Silas S. Brown committed
        if tkMessageBox.askyesno(app.master.title(),unicode(self.shortDescription,"utf-8")+which_collection+"\n"+localise("Do you want to start learning immediately?")): app.makelesson()
Silas S. Brown's avatar
Silas S. Brown committed

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)
Silas S. Brown's avatar
Silas S. Brown committed
        except: continue
Silas S. Brown's avatar
Silas S. Brown committed
        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))

Silas S. Brown's avatar
Silas S. Brown committed
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)

Silas S. Brown's avatar
Silas S. Brown committed
if WMstandard: GUI_omit_statusline = 1 # unlikely to be room (and can disrupt nav)

Silas S. Brown's avatar
Silas S. Brown committed
def startTk():
    class Application(Tkinter.Frame):
        def __init__(self, master=None):
            Tkinter.Frame.__init__(self, master)
            class EmptyClass: pass
            self.todo = EmptyClass() ; self.toRestore = []
            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)
Silas S. Brown's avatar
Silas S. Brown committed
            elif WMstandard: self.master.option_add('*font','Helvetica 7') # TODO on ALL WMstandard devices?
Silas S. Brown's avatar
Silas S. Brown committed
            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.)
            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()
Silas S. Brown's avatar
Silas S. Brown committed
                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
Silas S. Brown's avatar
Silas S. Brown committed
                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)
Silas S. Brown's avatar
Silas S. Brown committed
                if largeNominalSize >= nominalSize+3:
Silas S. Brown's avatar
Silas S. Brown committed
                    self.bigPrintFont = f % largeNominalSize
Silas S. Brown's avatar
Silas S. Brown committed
                    if GUI_always_big_print:
                        self.master.option_add('*font',self.bigPrintFont)
Silas S. Brown's avatar
Silas S. Brown committed
                        self.Label["font"]=self.bigPrintFont
Silas S. Brown's avatar
Silas S. Brown committed
                        del self.bigPrintFont ; self.isBigPrint=1
Silas S. Brown's avatar
Silas S. Brown committed
                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"))
Silas S. Brown's avatar
Silas S. Brown committed
            self.Cancel.focus() # (default focus if we don't add anything else, e.g. reader)
Silas S. Brown's avatar
Silas S. Brown committed
            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)
Silas S. Brown's avatar
Silas S. Brown committed
            addStatus(self.Version,self.copyright_string)
Silas S. Brown's avatar
Silas S. Brown committed
            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
Silas S. Brown's avatar
Silas S. Brown committed
            if GUI_omit_statusline or not hasattr(self,"ListBox"): return # status changes on main screen can cause too much jumping
Silas S. Brown's avatar
Silas S. Brown committed
            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
Silas S. Brown's avatar
Silas S. Brown committed
            if not winCEsound: self.balance_statusline,self.pollInterval = self.pollInterval,10
Silas S. Brown's avatar
Silas S. Brown committed
        def restore_statusline(self,*args): # ONLY from callbacks
Silas S. Brown's avatar
Silas S. Brown committed
            if not hasattr(self,"ListBox"): return
Silas S. Brown's avatar
Silas S. Brown committed
            # 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):
Silas S. Brown's avatar
Silas S. Brown committed
            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
Silas S. Brown's avatar
Silas S. Brown committed
            self.sync_listbox_etc()
            if self.ListBox.curselection():
                if not self.change_button_shown:
                    self.ChangeButton.pack()
                    self.change_button_shown = 1
Silas S. Brown's avatar
Silas S. Brown committed
                    self.Cancel["text"] = localise("Cancel selection")
Silas S. Brown's avatar
Silas S. Brown committed
            else:
                if self.change_button_shown:
                    self.ChangeButton.pack_forget()
                    self.change_button_shown = 0
Silas S. Brown's avatar
Silas S. Brown committed
                    self.lastText1 = 1 # force update
Silas S. Brown's avatar
Silas S. Brown committed
            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:
Silas S. Brown's avatar
Silas S. Brown committed
            global voiceOption
Silas S. Brown's avatar
Silas S. Brown committed
            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)
Silas S. Brown's avatar
Silas S. Brown committed
            if hasattr(self,"voiceOption") and not self.voiceOption.get()==voiceOption:
              voiceOption=self.voiceOption.get() ; updateSettingsFile(settingsFile,{"voiceOption":voiceOption})
Silas S. Brown's avatar
Silas S. Brown committed
            if hasattr(self,"outputTo"):
Silas S. Brown's avatar
Silas S. Brown committed
              outTo = self.outputTo.get()
              if outTo=="0": outTo=""
Silas S. Brown's avatar
Silas S. Brown committed
              if hasattr(self,"TestTextButton"):
Silas S. Brown's avatar
Silas S. Brown committed
                if outTo: self.TestTextButton["text"]=localise("To")+" "+outTo.upper()
Silas S. Brown's avatar
Silas S. Brown committed
                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"):
Silas S. Brown's avatar
Silas S. Brown committed
                if outTo: self.MakeLessonButton["text"]=localise("Make")+" "+outTo.upper()
Silas S. Brown's avatar
Silas S. Brown committed
                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
Silas S. Brown's avatar
Silas S. Brown committed
            if hasattr(self.todo,"not_first_time"):
                self.Cancel["text"] = "Stop lesson"
                del self.todo.not_first_time
Silas S. Brown's avatar
Silas S. Brown committed
            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
Silas S. Brown's avatar
Silas S. Brown committed
                self.Label.pack_forget()
Silas S. Brown's avatar
Silas S. Brown committed
                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
Silas S. Brown's avatar
Silas S. Brown committed
                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.
Silas S. Brown's avatar
Silas S. Brown committed
                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"))
Silas S. Brown's avatar
Silas S. Brown committed
                if not GUI_omit_statusline: self.Version.pack(fill=Tkinter.X,expand=1)
Silas S. Brown's avatar
Silas S. Brown committed
                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)
Silas S. Brown's avatar
Silas S. Brown committed
                else: focusButton(self.TestButton)
Silas S. Brown's avatar
Silas S. Brown committed
                del self.todo.set_main_menu
Silas S. Brown's avatar
Silas S. Brown committed
                self.restore_copyright()
Silas S. Brown's avatar
Silas S. Brown committed
            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
Silas S. Brown's avatar
Silas S. Brown committed
                focusButton(self.BriefIntButton)
Silas S. Brown's avatar
Silas S. Brown committed
                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
Silas S. Brown's avatar
Silas S. Brown committed
            if hasattr(self.todo,"clear_text_boxes"):
                self.Text1.set("") ; self.Text2.set("") ; self.Entry1.focus()
                del self.todo.clear_text_boxes
Silas S. Brown's avatar
Silas S. Brown committed
            if hasattr(self.todo,"undoRecordFrom"):
                theRecorderControls.undoRecordFrom()
                del self.todo.undoRecordFrom
Silas S. Brown's avatar
Silas S. Brown committed
            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
Silas S. Brown's avatar
Silas S. Brown committed
            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
Silas S. Brown's avatar
Silas S. Brown committed
            addStatus(entry,"Limits the maximum number of NEW words\nthat are put in each lesson")
Silas S. Brown's avatar
Silas S. Brown committed
            self.NumWords.set(words)
            addLabel(self.LessonRow,localise(cond(fileExists(progressFile),"new ","")+"words in"))
Silas S. Brown's avatar
Silas S. Brown committed
            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
Silas S. Brown's avatar
Silas S. Brown committed
            self.Minutes.set(mins)
            addLabel(self.LessonRow,localise("mins"))
Silas S. Brown's avatar
Silas S. Brown committed
            self.MakeLessonButton=addButton(self.LessonRow,localise("Start lesson"),self.makelesson,{"side":"left"},status="Press to create customized lessons\nusing the words in your collection")
Silas S. Brown's avatar
Silas S. Brown committed
            self.MakeLessonButton.bind('<FocusIn>',(lambda *args:app.after(10,lambda *args:app.MinsEntry.selection_clear())))
Silas S. Brown's avatar
Silas S. Brown committed
        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
Silas S. Brown's avatar
Silas S. Brown committed
            text1,text2 = asUnicode(self.Text1.get()),asUnicode(self.Text2.get())
Silas S. Brown's avatar
Silas S. Brown committed
            if text1==self.lastText1 and text2==self.lastText2: return
Silas S. Brown's avatar
Silas S. Brown committed
            self.lastText1,self.lastText2 = text1,text2
            if WMstandard and text1=="(Push OK to type A-Z)": text1=""
Silas S. Brown's avatar
Silas S. Brown committed
            for control,current,restoreTo in self.toRestore:
Silas S. Brown's avatar
Silas S. Brown committed
                if not asUnicode(control.get())==current:
Silas S. Brown's avatar
Silas S. Brown committed
                    self.toRestore = [] ; break
Silas S. Brown's avatar
Silas S. Brown committed
            if text1 or text2: self.Cancel["text"] = localise(cond(self.ListBox.curselection(),"Cancel selection","Clear input boxes"))
Silas S. Brown's avatar
Silas S. Brown committed
            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)
Silas S. Brown's avatar
Silas S. Brown committed
                    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)
Silas S. Brown's avatar
Silas S. Brown committed
                        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))
Silas S. Brown's avatar
Silas S. Brown committed
                    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))
Silas S. Brown's avatar
Silas S. Brown committed
                if not hasattr(self,"cacheManagementOptions"):
                    self.cacheManagementOptions = []
                    self.cacheManagementButtons = []
                if not cacheManagementOptions==self.cacheManagementOptions:
                    for b in self.cacheManagementButtons: b.pack_forget()
Silas S. Brown's avatar
Silas S. Brown committed
                    self.cacheManagementOptions = cacheManagementOptions
Silas S. Brown's avatar
Silas S. Brown committed
                    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
Silas S. Brown's avatar
Silas S. Brown committed
            if (oldKey,oldFname) == (0,0): # special for mp3web
                self.menu_response="mp3web" ; return
Silas S. Brown's avatar
Silas S. Brown committed
            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:
Silas S. Brown's avatar
Silas S. Brown committed
                if asUnicode(control.get())==current: control.set(restoreTo)
Silas S. Brown's avatar
Silas S. Brown committed
            self.toRestore = []
Silas S. Brown's avatar
Silas S. Brown committed
        def stripText(self,*args): self.Text1.set(fix_commas(hanzi_and_punc(asUnicode(self.Text1.get()))))
Silas S. Brown's avatar
Silas S. Brown committed
        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
                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 bigPrint(self,*args):
            self.thin_down_for_lesson()
            self.master.option_add('*font',self.bigPrintFont)
            self.Version["font"]=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.)
Silas S. Brown's avatar
Silas S. Brown committed
            self.isBigPrint=1
Silas S. Brown's avatar
Silas S. Brown committed
            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
Silas S. Brown's avatar
Silas S. Brown committed
            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:
Silas S. Brown's avatar
Silas S. Brown committed
                self.todo.alert = localise("Error: maximum number of new words must be an integer") ; return
Silas S. Brown's avatar
Silas S. Brown committed
            try:  mins=float(self.Minutes.get())
            except:
Silas S. Brown's avatar
Silas S. Brown committed
                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
Silas S. Brown's avatar
Silas S. Brown committed
            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.
Silas S. Brown's avatar
Silas S. Brown committed
            title = localise(cond(self.wordsExist,"Manage word list","Create word list"))
Silas S. Brown's avatar
Silas S. Brown committed
            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)
Silas S. Brown's avatar
Silas S. Brown committed
            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
Silas S. Brown's avatar
Silas S. Brown committed
            self.Label2,self.Text2,self.Entry2 = addLabelledBox(self.row2,True)
Silas S. Brown's avatar
Silas S. Brown committed
            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)
Silas S. Brown's avatar
Silas S. Brown committed
            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
Silas S. Brown's avatar
Silas S. Brown committed
            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")
Silas S. Brown's avatar
Silas S. Brown committed
            self.L1Entry["width"]=self.L2Entry["width"]=3
Silas S. Brown's avatar
Silas S. Brown committed
            self.L1Entry.bind('<Return>',lambda e:e.widget.after(10,lambda e=e:e.widget.event_generate('<Tab>')))
            self.L2Entry.bind('<Return>',self.changeLanguages)
Silas S. Brown's avatar
Silas S. Brown committed
            for e in [self.L1Entry,self.L2Entry]: e.bind('<Button-1>',(lambda e:e.widget.after(10,lambda e=e:selectAll(e))))
Silas S. Brown's avatar
Silas S. Brown committed
            self.ChangeLanguageButton = addButton(self.row3,"",self.changeLanguages,status="Use this button to set your\nfirst and second languages") # will set text in updateLanguageLabels
Silas S. Brown's avatar
Silas S. Brown committed
            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
Silas S. Brown's avatar
Silas S. Brown committed
            if GUI_omit_settings and (vocabFile==user0[1] or fileExists(vocabFile)): self.row3.pack_forget()
Silas S. Brown's avatar
Silas S. Brown committed
            if textEditorCommand:
Silas S. Brown's avatar
Silas S. Brown committed
                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)
Silas S. Brown's avatar
Silas S. Brown committed
                if not GUI_omit_settings: addButton(row4right,"advanced"+dottxt,self.openAdvancedTxt,{"side":"left"},status="Press this button to learn multiple languages\nor change advanced settings for synthesis etc")
Silas S. Brown's avatar
Silas S. Brown committed
                self.make_lesson_row()
Silas S. Brown's avatar
Silas S. Brown committed
            else: # no text editor, but can at least have Recorded Words button now we have a built-in manager
Silas S. Brown's avatar
Silas S. Brown committed
                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")
Silas S. Brown's avatar
Silas S. Brown committed
            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)
Silas S. Brown's avatar
Silas S. Brown committed
            self.remake_cancel_button(localise(cond(olpc or GUI_for_editing_only,"Quit","Back to main menu")))
Silas S. Brown's avatar
Silas S. Brown committed
            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
Silas S. Brown's avatar
Silas S. Brown committed
            self.updateLanguageLabels()
            self.LessonRow.pack() ; self.CancelRow.pack()
Silas S. Brown's avatar
Silas S. Brown committed
            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.
Silas S. Brown's avatar
Silas S. Brown committed
            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!
Silas S. Brown's avatar
Silas S. Brown committed
            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
Silas S. Brown's avatar
Silas S. Brown committed
            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
Silas S. Brown's avatar
Silas S. Brown committed
            if not GUI_omit_statusline: self.Version.pack(fill=Tkinter.X,expand=1)
Silas S. Brown's avatar
Silas S. Brown committed
            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"
Silas S. Brown's avatar
Silas S. Brown committed
        def showRecordedWords(self,*args): doRecWords()
Silas S. Brown's avatar
Silas S. Brown committed
        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()):
Silas S. Brown's avatar
Silas S. Brown committed
                    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()))))))
Silas S. Brown's avatar
Silas S. Brown committed
            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
Silas S. Brown's avatar
Silas S. Brown committed
            firstLanguage1=asUnicode(self.L1Text.get()).encode('utf-8')
            secondLanguage1=asUnicode(self.L2Text.get()).encode('utf-8')
Silas S. Brown's avatar
Silas S. Brown committed
            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 that can be computer voiced:")+"\n"+langs
                else: self.todo.alert = msg+"  "+localise("(Sorry, a list of these is not available on this system - check eSpeak installation.)")
                return
Silas S. Brown's avatar
Silas S. Brown committed
            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)
Silas S. Brown's avatar
Silas S. Brown committed
            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):
Silas S. Brown's avatar
Silas S. Brown committed
            # 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)
Silas S. Brown's avatar
Silas S. Brown committed
            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")
            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
Silas S. Brown's avatar
Silas S. Brown committed
            if gui_vocabFile_name=="vocab.txt": gui_vocabFile_name=localise(gui_vocabFile_name)
Silas S. Brown's avatar
Silas S. Brown committed
            self.AddButton["text"] = localise("Add to %s") % gui_vocabFile_name
            self.ChangeLanguageButton["text"] = localise("Change languages")
            self.ChangeButton["text"] = localise("Change or delete item")
Silas S. Brown's avatar
Silas S. Brown committed
            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)
Silas S. Brown's avatar
Silas S. Brown committed
            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
Silas S. Brown's avatar
Silas S. Brown committed
            if (asUnicode(self.Text1.get()),asUnicode(self.Text2.get())) == (l2,l1):
Silas S. Brown's avatar
Silas S. Brown committed
                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 = []
Silas S. Brown's avatar
Silas S. Brown committed
            for c in asUnicode(self.Text1.get()): setTo.append(c+"["+hex(ord(c))[2:]+"]")
Silas S. Brown's avatar
Silas S. Brown committed
            setTo=u"".join(setTo)
            self.Text1.set(setTo) ; self.todo.alert = setTo
        def addText(self,*args):
            self.zap_newlines()
            self.menu_response="add"