From fc297655b7b84c058985200d4f712e49dba9ac29 Mon Sep 17 00:00:00 2001 From: Wade Brainerd Date: Fri, 21 Nov 2008 18:01:52 +0000 Subject: Fix bug with old lessons still hearing keypresses. Change timer to 1 second. Use Unicode in word wrap regex. --- diff --git a/TODO b/TODO index 06d79be..42bd20f 100644 --- a/TODO +++ b/TODO @@ -13,7 +13,7 @@ First Release - Better flow at the end of a level. Report the result on the Lesson screen: Need more work, Medal received, etc. - Ability of lessons to list medals in other lessons as prerequisites. Disable unavailable lessons. - Some sort of lesson sorting criteria. -+ Split into file-per-screen. +- Split into file-per-screen. + Scroll lessons list to the first non-medaled lesson at startup. Or just remember scroll position. + Status message on the main screen. "You unlocked a new lesson!" for example. Eventually have the turtle 'say' it. - Implement a long text copying lesson and fix bugs in the scrolling and typing. diff --git a/keyboard.py b/keyboard.py index 5dc92c3..90b959b 100644 --- a/keyboard.py +++ b/keyboard.py @@ -245,10 +245,19 @@ class Keyboard(gtk.EventBox): self.shift_down = False + # Connect keyboard grabbing and releasing callbacks. + self.connect('realize', self._realize_cb) + self.connect('unrealize', self._unrealize_cb) + + def _realize_cb(self, widget): # Setup keyboard event snooping in the root window. - root_window.add_events(gtk.gdk.KEY_PRESS_MASK | gtk.gdk.KEY_RELEASE_MASK) - root_window.connect('key-press-event', self._key_press_cb) - root_window.connect('key-release-event', self._key_release_cb) + self.root_window.add_events(gtk.gdk.KEY_PRESS_MASK | gtk.gdk.KEY_RELEASE_MASK) + self.key_press_cb_id = self.root_window.connect('key-press-event', self._key_press_cb) + self.key_release_cb_id = self.root_window.connect('key-release-event', self._key_release_cb) + + def _unrealize_cb(self, widget): + self.root_window.disconnect(self.key_press_cb_id) + self.root_window.disconnect(self.key_release_cb_id) def _build_key_list(self, layout): """Builds a list of Keys objects from a layout description. diff --git a/lessonscreen.py b/lessonscreen.py index e1871ce..b09f925 100644 --- a/lessonscreen.py +++ b/lessonscreen.py @@ -113,21 +113,26 @@ class LessonScreen(gtk.VBox): self.keyboard = keyboard.Keyboard(self.activity) self.keyboard.set_layout(keyboard.DEFAULT_LAYOUT) - self.activity.add_events(gtk.gdk.KEY_PRESS_MASK) - self.key_press_cb_id = self.activity.connect('key-press-event', self.key_press_cb) - self.pack_start(hbox, False, False, 10) self.pack_start(frame, True, True) self.pack_start(self.keyboard, True) + # Connect keyboard grabbing and releasing callbacks. + self.connect('realize', self.realize_cb) + self.connect('unrealize', self.unrealize_cb) + self.show_all() self.begin_lesson() - - gobject.timeout_add(250, self.timer_cb) - def __del__(self): - print "Disconnecting keypress callback." + # Initialize stats update timer. + gobject.timeout_add(1000, self.timer_cb) + + def realize_cb(self, widget): + self.activity.add_events(gtk.gdk.KEY_PRESS_MASK) + self.key_press_cb_id = self.activity.connect('key-press-event', self.key_press_cb) + + def unrealize_cb(self, widget): self.activity.disconnect(self.key_press_cb_id) def update_stats(self): @@ -168,7 +173,8 @@ class LessonScreen(gtk.VBox): self.advance_step() def wrap_line(self, line): - words = re.split('(\W+)', line) + r = re.compile('(\W+)', re.UNICODE) + words = r.split(line) new_lines = [] cur_line = '' diff --git a/typingturtle.py b/typingturtle.py index 8a3fd37..5e5342d 100755 --- a/typingturtle.py +++ b/typingturtle.py @@ -13,7 +13,6 @@ # # You should have received a copy of the GNU General Public License # along with Typing Turtle. If not, see . -#!/usr/bin/env python """Typing Turtle - Interactive typing tutor for the OLPC XO.""" # Import standard Python modules. @@ -37,612 +36,8 @@ log = logging.getLogger('Typing Turtle') log.setLevel(logging.DEBUG) logging.basicConfig() -# Import onscreen keyboard. -import keyboard - -# Paragraph symbol unicode character. -PARAGRAPH_CODE = u'\xb6' - -# Maximium width of a text line in text lesson mode. -LINE_WIDTH = 80 - -# Requirements for earning medals. -# Words per minute goals came from http://en.wikipedia.org/wiki/Words_per_minute. -MEDALS = [ - { 'name': 'bronze', 'wpm': 25, 'accuracy': 75 }, - { 'name': 'silver', 'wpm': 35, 'accuracy': 85 }, - { 'name': 'gold', 'wpm': 45, 'accuracy': 95 } -] - -class MedalScreen(gtk.EventBox): - def __init__(self, medal, activity): - gtk.EventBox.__init__(self) - - self.modify_bg(gtk.STATE_NORMAL, self.get_colormap().alloc_color('#ffffff')) - - self.medal = medal - self.activity = activity - - cert0 = gtk.Label() - cert0.set_markup("" + _('Certificate of\nAchievement') + "") - - cert1 = gtk.Label() - cert1.set_markup("" + - (_('This certifies that on %(date)s,\n%(nick)s earned a %(type)s medal\nin Typing Turtle lesson %(lesson)s.') % medal) + - "") - - wpmlabel = gtk.Label() - wpmlabel.set_markup("" + (_('Words Per Minute: %(wpm)d') % medal) + "" ) - - accuracylabel = gtk.Label() - accuracylabel.set_markup("" + (_('Accuracy: %(accuracy)d%%') % medal) + "" ) - - statbox = gtk.HBox() - statbox.pack_start(wpmlabel, True) - statbox.pack_start(accuracylabel, True) - - oklabel = gtk.Label() - oklabel.set_markup("" + _('Go Back') + '') - okbtn = gtk.Button() - okbtn.add(oklabel) - okbtn.connect('clicked', self.ok_cb) - - btnbox = gtk.HBox() - btnbox.pack_start(okbtn, True, False) - - vbox = gtk.VBox() - - vbox.pack_start(cert0, True, False, 0) - vbox.pack_start(cert1, False, False, 0) - vbox.pack_start(gtk.HSeparator(), False, False, 20) - vbox.pack_start(statbox, False, False, 0) - vbox.pack_start(gtk.HSeparator(), False, False, 20) - vbox.pack_start(btnbox, False, False, 40) - - self.add(vbox) - - self.show_all() - - def ok_cb(self, widget): - self.activity.pop_screen() - -class LessonScreen(gtk.VBox): - def __init__(self, lesson, activity): - gtk.VBox.__init__(self) - - self.lesson = lesson - self.activity = activity - - # Build the user interface. - title = gtk.Label() - title.set_markup("" + lesson['name'] + "") - title.set_alignment(1.0, 0.0) - - stoplabel = gtk.Label(_('Go Back')) - stopbtn = gtk.Button() - stopbtn.add(stoplabel) - stopbtn.connect('clicked', self.stop_cb) - - # TODO- These will be replaced by graphical displays using gtk.DrawingArea. - self.wpmlabel = gtk.Label() - self.accuracylabel = gtk.Label() - - #self.wpmarea = gtk.DrawingArea() - #self.wpmarea.connect('expose-event', self.wpm_expose_cb) - #self.accuracyarea = gtk.DrawingArea() - #self.accuracyarea.connect('expose-event', self.accuracy_expose_cb) - - gobject.timeout_add(250, self.timer_cb) - - hbox = gtk.HBox() - hbox.pack_start(stopbtn, False, False, 10) - hbox.pack_start(self.wpmlabel, True, False, 10) - hbox.pack_start(self.accuracylabel, True, False, 10) - hbox.pack_end(title, False, False, 10) - - # Set up font styles. - self.tagtable = gtk.TextTagTable() - instructions_tag = gtk.TextTag('instructions') - #instructions_tag.props.size = 10000 - instructions_tag.props.justification = gtk.JUSTIFY_CENTER - - self.tagtable.add(instructions_tag) - text_tag = gtk.TextTag('text') - text_tag.props.family = 'Monospace' - self.tagtable.add(text_tag) - - correct_copy_tag = gtk.TextTag('correct-copy') - correct_copy_tag.props.family = 'Monospace' - correct_copy_tag.props.foreground = '#0000ff' - self.tagtable.add(correct_copy_tag) - - incorrect_copy_tag = gtk.TextTag('incorrect-copy') - incorrect_copy_tag.props.family = 'Monospace' - incorrect_copy_tag.props.foreground = '#ff0000' - self.tagtable.add(incorrect_copy_tag) - - # Set up the scrolling lesson text view. - self.lessonbuffer = gtk.TextBuffer(self.tagtable) - self.lessontext = gtk.TextView(self.lessonbuffer) - self.lessontext.set_editable(False) - self.lessontext.set_left_margin(20) - self.lessontext.set_right_margin(20) - self.lessontext.set_wrap_mode(gtk.WRAP_WORD) - - self.lessonscroll = gtk.ScrolledWindow() - self.lessonscroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS) - self.lessonscroll.add(self.lessontext) - - frame = gtk.Frame() - frame.add(self.lessonscroll) - - self.keyboard = keyboard.Keyboard(self.activity) - self.keyboard.set_layout(keyboard.DEFAULT_LAYOUT) - - activity.add_events(gtk.gdk.KEY_PRESS_MASK) - activity.connect('key-press-event', self.key_press_cb) - - self.pack_start(hbox, False, False, 10) - self.pack_start(frame, True, True) - self.pack_start(self.keyboard, True) - - self.show_all() - - self.begin_lesson() - - def update_stats(self): - if not self.start_time or self.lesson_finished: - return - - self.total_time = time.time() - self.start_time - if self.total_time >= 1.0: - self.wpm = 60 * (self.correct_keys / 5) / self.total_time - else: - self.wpm = 1.0 - self.accuracy = 100.0 * self.correct_keys / self.total_keys - - self.accuracylabel.set_markup(_('Accuracy: %(accuracy)d%%') % { 'accuracy' : int(self.accuracy) } ) - self.wpmlabel.set_markup(_('WPM: %(wpm)d') % { 'wpm': int(self.wpm) } ) - - def timer_cb(self): - self.update_stats() - return True - - def begin_lesson(self): - self.lesson_finished = False - - self.medal = None - - self.total_keys = 0 - self.correct_keys = 0 - self.incorrect_keys = 0 - - self.start_time = None - - self.next_step_idx = 0 - self.advance_step() - - def wrap_line(self, line): - words = re.split('(\W+)', line) - - new_lines = [] - cur_line = '' - for w in words: - # TODO: Handle single word longer than a line. - if not w.isspace() and len(cur_line) + len(w) > LINE_WIDTH: - if len(cur_line): - new_lines.append(cur_line) - cur_line = '' - cur_line += w - - if len(cur_line): - new_lines.append(cur_line) - - return new_lines - - def advance_step(self): - # Clear step related variables. - self.step = None - - self.text = None - self.line = None - self.line_marks = None - - # End lesson if this is the last step. - if self.next_step_idx >= len(self.lesson['steps']): - print "Lesson finished." - self.lesson_finished = True - self.show_lesson_report() - return - - # TODO - Play 'step finished' sound here. - - self.step = self.lesson['steps'][self.next_step_idx] - self.next_step_idx = self.next_step_idx + 1 - - # Clear the text buffer and output the instructions. - self.lessonbuffer.insert_with_tags_by_name( - self.lessonbuffer.get_end_iter(), '\n\n' + self.step['instructions'] + '\n', 'instructions') - - self.text = unicode(self.step['text']) - - # Split text into lines. - self.lines = self.text.splitlines(True) - - # Substitute paragraph codes. - self.lines = [l.replace('\n', PARAGRAPH_CODE) for l in self.lines] - - # Split by line length in addition to by paragraphs. - for i in range(0, len(self.lines)): - line = self.lines[i] - if len(line) > LINE_WIDTH: - self.lines[i:i+1] = self.wrap_line(line) - - # Center single line steps. - indent = '' - #if len(self.lines) == 1: - # indent = ' ' * ((LINE_LENGTH - len(self.lines[0]))/2) - - # Fill text buffer with text lines, each followed by room for the user to type. - self.line_marks = {} - line_idx = 0 - for l in self.lines: - - # Add the text to copy. - self.lessonbuffer.insert_with_tags_by_name( - self.lessonbuffer.get_end_iter(), '\n' + indent + l.encode('utf-8') + '\n' + indent, 'text') - - # Leave a marker where we will later insert text. - self.line_marks[line_idx] = self.lessonbuffer.create_mark(None, self.lessonbuffer.get_end_iter(), True) - - line_idx += 1 - - self.line_idx = 0 - self.begin_line() - - def begin_line(self): - self.line = self.lines[self.line_idx] - self.line_mark = self.line_marks[self.line_idx] - - self.char_idx = 0 - - self.hilite_next_key() - - def key_press_cb(self, widget, event): - # Ignore hotkeys. - if event.state & (gtk.gdk.CONTROL_MASK | gtk.gdk.MOD1_MASK): - return - - # Extract information about the key pressed. - key = gtk.gdk.keyval_to_unicode(event.keyval) - if key != 0: key = chr(key) - key_name = gtk.gdk.keyval_name(event.keyval) - - # Simply wait for a return keypress on the lesson finished screen. - if self.lesson_finished: - # TODO: Wait a second first. - if key_name == 'Return': - self.end_lesson() - return - - # Convert Return keys to paragraph symbols. - if key_name == 'Return': - key = PARAGRAPH_CODE - - print "key_press_cb: key=%s key_name=%s event.keyval=%d" % (key, key_name, event.keyval) - - # Timer starts with first keypress. - if not self.start_time: - self.start_time = time.time() - - # Handle backspace by deleting text and optionally moving up lines. - if key_name == 'BackSpace': - # Move to previous line if at the end of the current one. - if self.char_idx == 0 and self.line_idx > 0: - self.line_idx -= 1 - self.begin_line() - - self.char_idx = len(self.line) - - # Then delete the current character. - if self.char_idx > 0: - self.char_idx -= 1 - - iter = self.lessonbuffer.get_iter_at_mark(self.line_mark) - iter.forward_chars(self.char_idx) - - iter_end = iter.copy() - iter_end.forward_char() - - self.lessonbuffer.delete(iter, iter_end) - - self.hilite_next_key() - - # Process normal key presses. - elif key != 0: - - # Check to see if they pressed the correct key. - if key == self.line[self.char_idx]: - tag_name = 'correct-copy' - self.correct_keys += 1 - self.total_keys += 1 - - else: - # TODO - Play 'incorrect key' sound here. - - tag_name = 'incorrect-copy' - self.incorrect_keys += 1 - self.total_keys += 1 - - # Insert the key into the bufffer. - iter = self.lessonbuffer.get_iter_at_mark(self.line_mark) - iter.forward_chars(self.char_idx) - - self.lessonbuffer.insert_with_tags_by_name(iter, key, tag_name) - - # Advance to the next character (or else). - self.char_idx += 1 - if self.char_idx >= len(self.line): - self.line_idx += 1 - if self.line_idx >= len(self.lines): - self.advance_step() - else: - self.begin_line() - return - - self.update_stats() - - self.hilite_next_key() - - return False - - def hilite_next_key(self): - # Hilite the next key on the virtual keyboard. - self.keyboard.clear_hilite() - if len(self.line) > 0: - key = self.keyboard.find_key_by_letter(self.line[self.char_idx]) - if key: - key.set_hilite(True) - - # Move the cursor to the insert location. - iter = self.lessonbuffer.get_iter_at_mark(self.line_mark) - iter.forward_chars(self.char_idx) - self.lessonbuffer.place_cursor(iter) - - # Gain focus (this causes the cursor line to draw). - self.lessontext.grab_focus() - - # Scroll the TextView so the cursor is on screen. - self.lessontext.scroll_to_mark(self.lessonbuffer.get_insert(), 0) - - def show_lesson_report(self): - self.update_stats() - - lesson_name = self.lesson['name'] - - # Add to the lesson history. - report = { - 'lesson': lesson_name, - 'time': self.total_time, - 'wpm': self.wpm, - 'accuracy': self.accuracy - } - self.activity.add_history(report) - - # Show the medal screen, if one should be given. - got_medal = None - for medal in MEDALS: - if self.wpm >= medal['wpm'] and self.accuracy >= medal['accuracy']: - got_medal = medal['name'] - - if got_medal: - # Award the medal. - medal = { - 'lesson': lesson_name, - 'type': got_medal, - 'date': datetime.date.today().strftime('%B %d, %Y'), - 'nick': self.activity.owner.props.nick, - 'time': self.total_time, - 'wpm': report['wpm'], - 'accuracy': report['accuracy'] - } - self.medal = medal - - # Compare this medal with any existing medals for this lesson. - # Only record the best one. - add_medal = True - if self.activity.data['medals'].has_key(lesson_name): - old_medal = self.activity.data['medals'][lesson_name] - - order = ' '.join([m['name'] for m in MEDALS]) - add_idx = order.index(medal['type']) - old_idx = order.index(old_medal['type']) - - if add_idx < old_idx: - add_medal = False - elif add_idx == old_idx: - if medal['accuracy'] < old_medal['accuracy']: - add_medal = False - elif medal['accuracy'] == old_medal['accuracy']: - if medal['wpm'] < old_medal['wpm']: - add_medal = False - - if add_medal: - # Upgrade the player's level if needed. - if self.lesson['level'] > self.activity.data['level']: - self.activity.data['level'] = self.lesson['level'] - self.activity.data['motd'] = 'newlevel' - - self.activity.data['medals'][lesson_name] = medal - self.activity.mainscreen.update_medals() - - # Display results to the user. - text = '\n' - - congrats = [ - _('Good job!'), - _('Well done!'), - _('Nice work!'), - _('Way to go!') - ] - text += random.choice(congrats) + '\n\n' - - text += _('You finished the lesson in %(time)d seconds, with %(errors)d errors.\n\n') % \ - { 'time': int(self.total_time), 'errors': self.incorrect_keys } - text += _('Your words per minute (WPM) was %(wpm)d, and your accuracy was %(accuracy)d%%.\n\n') % \ - report - - if self.medal: - # TODO: Play medal sound here. - - text += _('Congratulations! You earned a %(type)s medal!\n\nPress Enter to see your certificate.') % \ - medal - - else: - # Comment on what the user needs to do better. - need_wpm = report['wpm'] < MEDALS[0]['wpm'] - need_accuracy = report['accuracy'] < MEDALS[0]['accuracy'] - - if need_accuracy and need_wpm: - text += _('You need to practice this lesson more before moving on. If you are having a hard time, ' - 'repeat the earlier lessons until you have mastered them completely before trying this one ' - 'again.\n\n') - - elif need_accuracy: - text += _('You almost got a medal! Next time, try not to make as many errors!\n\n') - - elif need_wpm: - text += _('You almost got a medal! Next time, try to type a little faster!\n\n') - - text += _('Press Enter to return to the main screen.') - - self.lessonbuffer.set_text('') - self.lessonbuffer.insert_with_tags_by_name( - self.lessonbuffer.get_end_iter(), - text, - 'instructions') - - def end_lesson(self): - self.activity.pop_screen() - - # Show the new medal if there was one. - if self.medal: - self.activity.push_screen(MedalScreen(self.medal, self.activity)) - - def stop_cb(self, widget): - self.activity.pop_screen() - -class MainScreen(gtk.VBox): - def __init__(self, activity): - gtk.VBox.__init__(self) - - self.activity = activity - - # Build background. - title = gtk.Label() - title.set_markup("" + _('Typing Turtle') + "") - - subtitle = gtk.Label() - subtitle.set_markup(_('Welcome to Typing Turtle! To begin, select a lesson from the list below.')) - - spacer = gtk.HBox() - - # Lessons header. - headerbox = gtk.VBox() - label = gtk.Label() - label.set_alignment(0.0, 0.5) - label.set_markup(""+_('Available Lessons')+"") - headerbox.pack_start(label, False) - headerbox.pack_start(gtk.HSeparator(), False) - - # Build lessons list. - self.lessonbox = gtk.VBox() - self.lessonbox.set_spacing(10) - - bundle_path = sugar.activity.activity.get_bundle_path() - code = locale.getlocale(locale.LC_ALL)[0] - path = bundle_path + '/lessons/' + code + '/' - - # Find all .lesson files in ./lessons/en_US/ for example. - lessons = [] - for f in os.listdir(path): - fd = open(path + f, 'r') - try: - lesson = json.read(fd.read()) - lessons.append(lesson) - finally: - fd.close() - - lessons.sort(lambda x, y: x['level'] - y['level']) - - for l in lessons: - label = gtk.Label() - label.set_alignment(0.0, 0.5) - label.set_markup("" + l['name'] + "\n" + l['description']) - - btn = gtk.Button() - btn.lesson = l - btn.add(label) - btn.connect('clicked', self.lesson_clicked_cb) - - medalimage = gtk.Image() - - medalbtn = gtk.Button() - medalbtn.lesson = l - medalbtn.add(medalimage) - medalbtn.connect('clicked', self.medal_clicked_cb) - - hbox = gtk.HBox() - hbox.pack_start(btn, True, True, 10) - hbox.pack_end(medalbtn, False, False) - - hbox.button = btn - hbox.medalbutton = medalbtn - hbox.lesson = l - hbox.medalimage = medalimage - - self.lessonbox.pack_start(hbox, False) - - self.lessonscroll = gtk.ScrolledWindow() - self.lessonscroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) - self.lessonscroll.add_with_viewport(self.lessonbox) - - self.pack_start(title, False, True, 10) - self.pack_start(subtitle, False) - self.pack_start(spacer, False, False, 50) - self.pack_start(headerbox, False) - self.pack_start(self.lessonscroll, True) - - self.update_medals() - - def update_medals(self): - for l in self.lessonbox: - # Disable the lesson button unless available. - lesson_available = self.activity.data['level'] >= l.lesson['requiredlevel'] - l.button.set_sensitive(lesson_available) - l.medalbutton.set_sensitive(lesson_available) - - # Update the medal image. - medal_type = 'none' - if self.activity.data['medals'].has_key(l.lesson['name']): - medal_type = self.activity.data['medals'][l.lesson['name']]['type'] - - bundle = sugar.activity.activity.get_bundle_path() - images = { - 'none': bundle+'/images/no-medal.jpg', - 'bronze': bundle+'/images/bronze-medal.jpg', - 'silver': bundle+'/images/silver-medal.jpg', - 'gold': bundle+'/images/gold-medal.jpg' - } - l.medalimage.set_from_file(images[medal_type]) - - def lesson_clicked_cb(self, widget): - self.activity.push_screen(LessonScreen(widget.lesson, self.activity)) - - def medal_clicked_cb(self, widget): - if self.activity.data['medals'].has_key(widget.lesson['name']): - medal = self.activity.data['medals'][widget.lesson['name']] - self.activity.push_screen(MedalScreen(medal, self.activity)) +# Import activity modules. +import mainscreen, lessonscreen, medalscreen # This is the main Typing Turtle activity class. # @@ -672,7 +67,7 @@ class TypingTurtle(sugar.activity.activity.Activity): self.set_canvas(self.screenbox) # Start with the main screen. - self.mainscreen = MainScreen(self) + self.mainscreen = mainscreen.MainScreen(self) self.push_screen(self.mainscreen) self.show_all() -- cgit v0.9.1