# -*- coding: iso-8859-1 -*-
import os
import time
import math
from tkinter import *
from tkinter.font import Font
import tkinter.filedialog
import tkinter.messagebox
# parameters
encodings = ['latin-1','utf-8'] # available text file encodings (default first)
width,height=300,300 # meters canvas dimensions
len1,len2 = 0.85,0.3 # needle dimensions, relative to meter ray
ray = int(0.7*width/2) # meter circle
x0,y0 = width/2,width/2 # position of circle center inside canvas
min_speed,max_speed = 0,400 # minimum and maximum speed, in characters/minute
step_speed = 50 # step between speed marks on meter
min_err,max_err,step_err = 0,10,1 # same for error rate in %
# localisation
langs = {'en':'English','fr':'Français'}
try: # to guess language from locale
import locale
default_lang = locale.getdefaultlocale()[0][:2]
langs[default_lang]
except:
default_lang = 'en'
_ = {'title':{'en':'Typing skills meter','fr':'Dactylomètre'},
'exo':{'en':'Exercices','fr':'Exercices'},
'opts':{'en':'Options','fr':'Options'},
'paste':{'en':'Paste','fr':'Coller'},
'open':{'en':'Open...','fr':'Ouvrir...'},
'clear':{'en':'Clear','fr':'Effacer'},
'restart':{'en':'Start again','fr':'Recommencer'},
'encoding':{'en':'Encoding','fr':'Encodage'},
'speed':{'en':'Speed','fr':'Vitesse'},
'err':{'en':'Errors','fr':'Erreurs'},
'cpm':{'en':'Chars/min','fr':'Cars/min'}
}
def set_libs(lang):
root.title(_['title'][lang])
menu.entryconfig(1,label=_['exo'][lang])
menu.entryconfig(2,label=_['opts'][lang])
speed.itemconfig(speed.title,text=_['speed'][lang])
speed.itemconfig(speed.unit,text=_['cpm'][lang])
errors.itemconfig(errors.title,text=_['err'][lang])
errors.itemconfig(errors.unit,text='%')
for i,entry in enumerate(['open','paste','clear','restart']):
file_menu.entryconfig(i,label=_[entry][lang])
root = Tk()
font = Font(family="Courier New",size=12,weight='bold') # text font
meter_font = Font(family="Arial",size=12,weight='normal')
result_box = None
exo = None
t0 = None
class Exercise:
def __init__(self,text=""):
self.text = text
model.delete(1.0,END)
model.insert(END,self.text)
def reset(self):
self.start = None
self.line,self.col = 1,0
self.nb_errors = 0
speed.draw_needle(0)
errors.draw_needle(0)
for tag in 'done','error','old_error':
model.tag_remove(tag,1.0,END)
model.mark_set(INSERT,1.0)
model.focus()
class Meter(Canvas):
def draw(self,vmin,vmax,step,title,unit):
self.vmin = vmin
self.vmax = vmax
x0 = width/2
y0 = width/2
ray = int(0.7*width/2)
self.title = self.create_text(width/2,20,fill="#000",
font=meter_font)
self.create_oval(x0-ray*1.1,y0-ray*1.1,x0+ray*1.1,y0+ray*1.1,
fill="#DDD")
self.create_oval(x0-ray,y0-ray,x0+ray,y0+ray,fill="#000")
coef = 0.1
self.create_oval(x0-ray*coef,y0-ray*coef,x0+ray*coef,y0+ray*coef,
fill="white")
for i in range(1+int((vmax-vmin)/step)):
v = vmin + step*i
angle = (5+6*((v-vmin)/(vmax-vmin)))*math.pi/4
self.create_line(x0+ray*math.sin(angle)*0.9,
y0 - ray*math.cos(angle)*0.9,
x0+ray*math.sin(angle)*0.98,
y0 - ray*math.cos(angle)*0.98,fill="#FFF",width=2)
self.create_text(x0+ray*math.sin(angle)*0.75,
y0 - ray*math.cos(angle)*0.75,
text=v,fill="#FFF",font=meter_font)
if i==int(vmax-vmin)/step:
continue
for dv in range(1,5):
angle = (5+6*((v+dv*(step/5)-vmin)/(vmax-vmin)))*math.pi/4
self.create_line(x0+ray*math.sin(angle)*0.94,
y0 - ray*math.cos(angle)*0.94,
x0+ray*math.sin(angle)*0.98,
y0 - ray*math.cos(angle)*0.98,fill="#FFF")
self.unit = self.create_text(width/2,y0+0.8*ray,fill="#FFF",
font=meter_font)
self.needle = self.create_line(x0-ray*math.sin(5*math.pi/4)*len2,
y0+ray*math.cos(5*math.pi/4)*len2,
x0+ray*math.sin(5*math.pi/4)*len1,
y0-ray*math.cos(5*math.pi/4)*len1,
width=2,fill="#FFF")
def draw_needle(self,v):
v = max(v,self.vmin)
v = min(v,self.vmax)
angle = (5+6*((v-self.vmin)/(self.vmax-self.vmin)))*math.pi/4
self.coords(self.needle,x0-ray*math.sin(angle)*len2,
y0+ray*math.cos(angle)*len2,
x0+ray*math.sin(angle)*len1,
y0-ray*math.cos(angle)*len1)
def paste():
global exo
try:
txt = root.clipboard_get()
model.insert(END,txt)
exo = Exercise(txt)
exo.reset()
root.clipboard_clear()
except:
return
def open_text():
global exo
exo_dir = os.path.join(os.getcwd(),'texts')
if not os.path.exists(exo_dir):
os.mkdir(exo_dir)
filename = tkinter.filedialog.askopenfilename(initialdir=exo_dir)
if filename:
src = open(filename,encoding=encoding.get())
try:
model_txt = '\n'.join([l.rstrip() for l in src.readlines()])
except UnicodeDecodeError:
tkinter.messagebox.showerror('Encoding error',
message=("Can't open file %s with encoding %s")
%(os.path.basename(filename),encoding.get()))
return
exo = Exercise(model_txt)
exo.reset()
def clear():
global exo
model.delete(1.0,END)
exo = None
def start_again():
global result_box
if exo is not None:
exo.reset()
if result_box is not None:
result_box.destroy()
result_box = None
def type_key(event):
global exo,result_box, t0
if exo is None:
return 'break'
pos = model.index("%s.%s" %(exo.line,exo.col))
nbcars = len(model.get(0.0,pos))
if exo is None:
if nbcars>0:
model_txt = model.get(0.0,END+"-1 chars")
model_txt = model_txt.encode('utf-8')
exo = Exercise(model_txt)
else:
return
if exo.start is None:
exo.start = time.time()
elif nbcars>3:
carspm = 60*nbcars/(time.time()-exo.start)
speed.draw_needle(carspm)
pos = model.index("%s.%s" %(exo.line,exo.col))
pos_see = model.index("%s+30c" %pos)
model.see(pos_see)
good = model.get(model.index("%s.%s" %(exo.line,exo.col)))
typed = event.char
if event.keysym in ["Shift_L","Shift_R","Multi_key"]:
return 'break'
flag = (typed=='\r' and good=='\n') or (good == typed)
if not flag: # error
exo.nb_errors += 1
err_txt = "%s error" %exo.nb_errors
if exo.nb_errors > 1:
err_txt += "s"
model.tag_add('error',pos)
else:
exo.col += 1
if good == "\n":
exo.line += 1
exo.col = 0
if 'error' in model.tag_names(pos):
model.tag_remove('error',pos)
model.tag_add('old_error',pos)
else:
model.tag_add('done',pos)
model.mark_set(INSERT,model.index('%s+1c' %pos))
if nbcars:
err_rate = 100*round(float(exo.nb_errors)/float(nbcars),4)
errors.draw_needle(err_rate)
if model.index("%s.%s" %(exo.line,exo.col)) == model.index(END+"-1 chars"):
result_box = Toplevel(root)
Label(result_box,text="The exercice is finished").pack()
Button(result_box,text="Start again",command=start_again).pack()
Button(result_box,text="Other text",command=open_text).pack()
return 'break'
encoding = StringVar(root)
encoding.set(encodings[0])
lang = StringVar(root)
lang.set(default_lang)
menu = Menu(root)
file_menu = Menu(menu,tearoff=False)
for command in open_text,paste,clear,start_again:
file_menu.add_command(command=command)
menu.add_cascade(label="Exercices",menu=file_menu)
options_menu = Menu(menu,tearoff=False)
menuLang = Menu(options_menu,tearoff=0)
for lang in langs:
menuLang.add_command(label=langs[lang],command=lambda x=lang:set_libs(x))
options_menu.add_cascade(menu=menuLang,label='Language')
menuEncoding = Menu(options_menu,tearoff=0)
for enc in encodings:
menuEncoding.add_command(label=enc,command=lambda x=enc:encoding.set(x))
options_menu.add_cascade(menu=menuEncoding,label='Encoding')
menu.add_cascade(label="Options",menu=options_menu)
root.config(menu=menu)
# meters zone
meters = Frame(root,width=2*width,height=width,bg="white")
speed = Meter(meters,width=width,height=height)
speed.draw(min_speed,max_speed,step_speed,"Speed","Chars/min")
speed.pack(side=LEFT)
errors = Meter(meters,width=width,height=width)
errors.draw(min_err,max_err,step_err,"Errors","%")
errors.pack()
meters.pack(anchor=S,fill=Y,expand=True)
# text zone
model = Text(root,width=80,height=20,wrap=WORD,font=font,padx=10,pady=10,relief=RIDGE)
model.tag_config('done',foreground="#303030",background="#D0D0D0")
model.tag_config('error',foreground="#FF0000",background="#FFFFFF")
model.tag_config('old_error',foreground="#FF0000",background="#D0D0D0")
model.bind('<Key>',type_key)
model.pack()
set_libs(default_lang)
root.mainloop()