Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/lessonscreen.py
diff options
context:
space:
mode:
authorWade Brainerd <wadetb@gmail.com>2008-11-21 17:51:32 (GMT)
committer Wade Brainerd <wadetb@gmail.com>2008-11-21 17:51:32 (GMT)
commitde9ee0339ee37a8ada6ff7dfbbebe103bb493026 (patch)
tree2a04a96825efe6bb11f843a4b084293be7fc4cbe /lessonscreen.py
parentafb05ac5b16412962103f9eaac9cd2ca44b80d1b (diff)
Split into file per screen.
Diffstat (limited to 'lessonscreen.py')
-rw-r--r--lessonscreen.py478
1 files changed, 478 insertions, 0 deletions
diff --git a/lessonscreen.py b/lessonscreen.py
new file mode 100644
index 0000000..e1871ce
--- /dev/null
+++ b/lessonscreen.py
@@ -0,0 +1,478 @@
+# Copyright 2008 by Kate Scheppke and Wade Brainerd.
+# This file is part of Typing Turtle.
+#
+# Typing Turtle 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.
+#
+# Typing Turtle 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.
+#
+# You should have received a copy of the GNU General Public License
+# along with Typing Turtle. If not, see <http://www.gnu.org/licenses/>.
+
+# Import standard Python modules.
+import logging, os, math, time, copy, json, locale, datetime, random, re
+from gettext import gettext as _
+
+# Import PyGTK.
+import gobject, pygtk, gtk, pango
+
+# Import Sugar UI modules.
+import sugar.activity.activity
+from sugar.graphics import *
+
+# Import activity modules.
+import keyboard, medalscreen
+
+# 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 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("<span size='20000'><b>" + lesson['name'] + "</b></span>")
+ 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)
+
+ 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)
+
+ 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)
+
+ self.show_all()
+
+ self.begin_lesson()
+
+ gobject.timeout_add(250, self.timer_cb)
+
+ def __del__(self):
+ print "Disconnecting keypress callback."
+ self.activity.disconnect(self.key_press_cb_id)
+
+ def update_stats(self):
+ if self.lesson_finished:
+ return
+
+ if self.start_time:
+ 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.wpmlabel.set_markup(_('<b>WPM:</b> %(wpm)d') % { 'wpm': int(self.wpm) } )
+
+ if self.total_keys:
+ self.accuracy = 100.0 * self.correct_keys / self.total_keys
+
+ self.accuracylabel.set_markup(_('<b>Accuracy:</b> %(accuracy)d%%') % { 'accuracy' : int(self.accuracy) } )
+
+ 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.MedalScreen(self.medal, self.activity))
+
+ def stop_cb(self, widget):
+ self.activity.pop_screen()