Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/app/static/doc/myosa
diff options
context:
space:
mode:
Diffstat (limited to 'app/static/doc/myosa')
-rw-r--r--app/static/doc/myosa/META-INF/container.xml6
-rw-r--r--app/static/doc/myosa/ch000_table_of_contents.xhtml3
-rw-r--r--app/static/doc/myosa/ch001_introduction.xhtml45
-rw-r--r--app/static/doc/myosa/ch002_what-is-sugar.xhtml61
-rw-r--r--app/static/doc/myosa/ch003_what-is-a-sugar-activity.xhtml22
-rw-r--r--app/static/doc/myosa/ch004_what-do-i-need-to-know-to-write-a-sugar-activity.xhtml41
-rw-r--r--app/static/doc/myosa/ch006_setting-up-a-development-environment.xhtml133
-rw-r--r--app/static/doc/myosa/ch007_creating-your-first-activity.xhtml49
-rw-r--r--app/static/doc/myosa/ch008_a-standalone-python-program-for-reading-etexts.xhtml244
-rw-r--r--app/static/doc/myosa/ch009_inherit-from-sugaractivityactivity.xhtml279
-rw-r--r--app/static/doc/myosa/ch010_package-the-activity.xhtml208
-rw-r--r--app/static/doc/myosa/ch011_add-refinements.xhtml625
-rw-r--r--app/static/doc/myosa/ch012_add-your-activity-code-to-version-control.xhtml336
-rw-r--r--app/static/doc/myosa/ch013_going-international-with-pootle.xhtml137
-rw-r--r--app/static/doc/myosa/ch014_distribute-your-activity.xhtml92
-rw-r--r--app/static/doc/myosa/ch015_debugging-sugar-activities.xhtml259
-rw-r--r--app/static/doc/myosa/ch017_making-shared-activities.xhtml2185
-rw-r--r--app/static/doc/myosa/ch018_adding-text-to-speech.xhtml695
-rw-r--r--app/static/doc/myosa/ch019_fun-with-the-journal.xhtml1085
-rw-r--r--app/static/doc/myosa/ch020_making-activities-using-pygame.xhtml536
-rw-r--r--app/static/doc/myosa/ch021_making-new-style-toolbars.xhtml1150
-rw-r--r--app/static/doc/myosa/ch023_where-to-go-from-here.xhtml46
-rw-r--r--app/static/doc/myosa/ch024_about-the-authors.xhtml31
-rw-r--r--app/static/doc/myosa/ch025_credits.xhtml176
-rw-r--r--app/static/doc/myosa/content.opf126
-rw-r--r--app/static/doc/myosa/mimetype1
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-CoverImage600x450-en.pngbin0 -> 205403 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-ReadEtexts_01_1-en.jpgbin0 -> 34804 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-ReadEtexts_02-en.jpgbin0 -> 45789 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-ReadEtexts_03-en.jpgbin0 -> 33653 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-ReadEtexts_04-en.jpgbin0 -> 31931 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-ReadEtexts_05-en.jpgbin0 -> 32213 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-SCommander1-en.jpgbin0 -> 37783 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-SCommander2-en.jpgbin0 -> 48574 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-SpeakActivity-en.pngbin0 -> 36205 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-analyzeactivity_1-en.jpgbin0 -> 62877 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-batallanaval-en.jpgbin0 -> 39257 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-collab1_1-en.jpgbin0 -> 31212 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-collab2_1-en.jpgbin0 -> 14702 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-collab3_1-en.jpgbin0 -> 19833 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-collab4-en.jpgbin0 -> 23415 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-collab5-en.jpgbin0 -> 9911 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-collab6-en.jpgbin0 -> 6280 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-demoiselle1-en.jpgbin0 -> 26578 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-demoiselle2_1-en.jpgbin0 -> 26965 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-eric-en.jpgbin0 -> 81814 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-espeak-en.jpgbin0 -> 54496 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-espeak2_1-en.jpgbin0 -> 59308 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-espeak3-en.jpgbin0 -> 87646 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-gimp1-en.jpgbin0 -> 23869 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-git1-en.jpgbin0 -> 11035 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-git10-en.jpgbin0 -> 75087 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-git11_1-en.jpgbin0 -> 78179 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-git12-en.jpgbin0 -> 38527 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-git13-en.jpgbin0 -> 18516 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-git14-en.jpgbin0 -> 67245 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-git2-en.jpgbin0 -> 32774 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-git3-en.jpgbin0 -> 49867 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-git4-en.jpgbin0 -> 10053 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-git5-en.jpgbin0 -> 54709 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-git6-en.jpgbin0 -> 57414 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-git7-en.jpgbin0 -> 69235 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-git8-en.jpgbin0 -> 70787 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-git9-en.jpgbin0 -> 47536 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-inkscape-en.jpgbin0 -> 57170 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-journal_main_screen-en.pngbin0 -> 61870 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-logactivity1-en.jpgbin0 -> 89934 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-logactivity2-en.jpgbin0 -> 55389 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-newtoolbar1_1-en.jpgbin0 -> 5352 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-newtoolbar2-en.jpgbin0 -> 10164 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-newtoolbar3-en.jpgbin0 -> 8582 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-newtoolbar4-en.jpgbin0 -> 8952 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-newtoolbar5-en.jpgbin0 -> 69720 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-oldtoolbar-en.jpgbin0 -> 6277 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-pootle1-en.jpgbin0 -> 18451 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-pootle2-en.jpgbin0 -> 42319 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-pootle3-en.jpgbin0 -> 50011 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-scribble-en.jpgbin0 -> 26461 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-spe-en.jpgbin0 -> 43229 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-sugargame_1-en.jpgbin0 -> 9868 bytes
-rw-r--r--app/static/doc/myosa/static/ActivitiesGuideSugar-thegimp-en.jpgbin0 -> 73629 bytes
-rw-r--r--app/static/doc/myosa/static/Floss-100-en.gifbin0 -> 1290 bytes
-rw-r--r--app/static/doc/myosa/static/MYOSA_Cover.pngbin0 -> 229733 bytes
-rw-r--r--app/static/doc/myosa/toc.ncx170
84 files changed, 8741 insertions, 0 deletions
diff --git a/app/static/doc/myosa/META-INF/container.xml b/app/static/doc/myosa/META-INF/container.xml
new file mode 100644
index 0000000..93a63d0
--- /dev/null
+++ b/app/static/doc/myosa/META-INF/container.xml
@@ -0,0 +1,6 @@
+<?xml version='1.0' encoding='utf-8'?>
+<container xmlns="urn:oasis:names:tc:opendocument:xmlns:container" version="1.0">
+ <rootfiles>
+ <rootfile media-type="application/oebps-package+xml" full-path="content.opf"/>
+ </rootfiles>
+</container>
diff --git a/app/static/doc/myosa/ch000_table_of_contents.xhtml b/app/static/doc/myosa/ch000_table_of_contents.xhtml
new file mode 100644
index 0000000..1f9f2ad
--- /dev/null
+++ b/app/static/doc/myosa/ch000_table_of_contents.xhtml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>Table Of Contents</h1><h2 _moz_dirty="">Sugar Activities</h2><p _moz_dirty=""><a _moz_dirty="" href="ch001_introduction.xhtml">Introduction</a></p><p _moz_dirty=""><a _moz_dirty="" href="ch002_what-is-sugar.xhtml">What Is Sugar?</a></p><p _moz_dirty=""><a _moz_dirty="" href="ch003_what-is-a-sugar-activity.xhtml">What Is A Sugar Activity?</a></p><p _moz_dirty=""><a _moz_dirty="" href="ch004_what-do-i-need-to-know-to-write-a-sugar-activity.xhtml">What Do I Need To Know To Write A Sugar Activity?</a></p><h2 _moz_dirty="">Programming<br _moz_dirty=""/></h2><p _moz_dirty=""><a _moz_dirty="" href="ch006_setting-up-a-development-environment.xhtml">Setting Up A Development Environment</a></p><p _moz_dirty=""><a _moz_dirty="" href="ch007_creating-your-first-activity.xhtml">Creating Your First Activity</a><br _moz_dirty=""/><br _moz_dirty=""/><a _moz_dirty="" href="ch008_a-standalone-python-program-for-reading-etexts.xhtml">A Standalone Python Program For Reading ETexts</a></p><p _moz_dirty=""><a _moz_dirty="" href="ch009_inherit-from-sugaractivityactivity.xhtml">Inherit From sugar.activity.Activity</a></p><p _moz_dirty=""><a _moz_dirty="" href="ch010_package-the-activity.xhtml">Package The Activity</a></p><p _moz_dirty=""><a _moz_dirty="" href="ch011_add-refinements.xhtml">Add Refinements</a></p><p _moz_dirty=""><a _moz_dirty="" href="ch012_add-your-activity-code-to-version-control.xhtml">Add Your Activity Code To Version Control</a></p><p _moz_dirty=""><a _moz_dirty="" href="ch013_going-international-with-pootle.xhtml">Going International With Pootle</a></p><p _moz_dirty=""><a _moz_dirty="" href="ch014_distribute-your-activity.xhtml">Distribute Your Activity</a></p><p _moz_dirty=""><a _moz_dirty="" href="ch015_debugging-sugar-activities.xhtml">Debugging Sugar Activities</a></p><h2 _moz_dirty="">Advanced Topics</h2><p _moz_dirty=""><a _moz_dirty="" href="ch017_making-shared-activities.xhtml">Making Shared Activities</a></p><p _moz_dirty=""><a _moz_dirty="" href="ch018_adding-text-to-speech.xhtml">Adding Text To Speech</a></p><p _moz_dirty=""><a _moz_dirty="" href="ch019_fun-with-the-journal.xhtml">Fun With The Journal</a></p><p _moz_dirty=""><a _moz_dirty="" href="ch020_making-activities-using-pygame.xhtml">Making Activities Using Pygame</a></p><p _moz_dirty=""><a _moz_dirty="" href="ch021_making-new-style-toolbars.xhtml">Making New Style Toolbars</a></p><h2 _moz_dirty="">Appendix</h2><p _moz_dirty=""><a _moz_dirty="" href="ch023_where-to-go-from-here.xhtml">Where To Go From Here?</a></p><p _moz_dirty=""><a _moz_dirty="" href="ch024_about-the-authors.xhtml">About The Authors</a></p><p _moz_dirty=""><a _moz_dirty="" href="ch025_credits.xhtml">Credits</a></p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch001_introduction.xhtml b/app/static/doc/myosa/ch001_introduction.xhtml
new file mode 100644
index 0000000..b397a37
--- /dev/null
+++ b/app/static/doc/myosa/ch001_introduction.xhtml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1 style="text-align: center;">Make Your Own Sugar Activities!</h1><div _moz_dirty="" style="text-align: center;"><span _moz_dirty="" style="font-style: italic;">by James Simmons</span><br _moz_dirty="" type="_moz"/></div><br _moz_dirty="" type="_moz"/><h1 _moz_dirty="">Introduction
+</h1>
+<p>"This book is a record of a pleasure trip. If it were a record of a solemn scientific expedition, it would have about it that gravity, that profundity, and that impressive incomprehensibility which are so proper to works of that kind, and withal so attractive."
+</p>
+<p>From the Preface to <em>The Innocents Abroad</em>, by Mark Twain
+ <br/></p>
+<p>
+</p>
+<p>The purpose of this book is to teach you what you need to know to write Activities for Sugar, the operating environment developed for the One Laptop Per Child project. This book does not assume that you know how to program a computer, although those who do will find useful information in it.  My primary goal in writing it is to encourage non programmers, including children and their teachers, to create their own Sugar Activities.  Because of this goal I will include some details that other books would leave out and leave out things that others would include.  Impressive incomprehensibility will be kept to a minimum.
+ <br/></p>
+<p>If you just want to learn how to write computer programs Sugar provides many Activities to help you do that: Etoys, Turtle Art, Scratch, and Pippy. None of these are really suitable for creating Activities so I won't cover them in this book, but they're a great way to learn about programming. If you decide after playing with these that you'd like to try writing an Activity after all you'll have a good foundation of knowledge to build on.
+</p>
+<p>When you have done some programming then you'll know how satisfying it can be to use a program that you made yourself, one that does <em>exactly </em>what you want it to do.  Creating a Sugar Activity takes that enjoyment to the next level.  A useful Sugar Activity can be translated by volunteers into every language, be downloaded hundreds of times a week and used every day by students all over the world.
+</p>
+<p><img width="600" height="450" alt="Some Sugar Activities!" src="static/ActivitiesGuideSugar-CoverImage600x450-en.png"/><br/></p>
+<p>A book that teaches <em>everything</em> you need to know to write Activities would be really, really long and would duplicate material that is already available elsewhere. Because of this, I am going to write this as sort of a guided tour of Activity development. That means, for example, that I'll teach you what Python is and why it's important to learn it but I won't teach you the Python language itself. There are excellent tutorials on the Internet that will do that, and I'll refer you to those tutorials.
+</p>
+<p>There is much sample code in this book, but there is no need for you to type it in to try it out.  All of the code is in a Git repository that you can download to your own computer.  If you've never used Git there is a chapter that explains what it is and how to use it.
+ <br/></p>
+<p>I started writing Activities shortly after I received my XO laptop. When I started I didn't know <em>any</em> of the material that will be in this book. I had a hard time knowing where to begin. What I did have going for me though was a little less than 30 years as a professional programmer. As a result of that I think like a programmer. A good programmer can take a complex task and divide it up into manageable pieces. He can figure out how things <em>must</em> work, and from that figure out how they <em>do</em> work. He knows how to ask for help and where. If there is no obvious place to begin he can begin <em>somewhere</em> and eventually get where he needs to go.
+</p>
+<p>Because I went through this process I think I can be a pretty good guide to writing Sugar Activities.  Along the way I hope to also teach you how to think like a programmer does.
+</p>
+<p>From time to time I may add chapters to this book.  Sugar is a great application platform and this book can only begin to tell you what is possible.  It is my hope that future versions of the book will have guest chapters on more advanced topics written by other experienced Activity developers.
+</p>
+<h2>Formats For This Book
+</h2>
+<p>This book is part of the <em>FLOSS Manuals</em> project and is available for online viewing at their website:
+</p>
+<p><a href="http://en.flossmanuals.net">http://en.flossmanuals.net/</a>
+ <br/></p>
+<p>You can also purchase a printed and bound version of this book at <em>Lulu.com</em>:
+</p>
+<p><a href="http://www.lulu.com/product/paperback/make-your-own-sugar-activities/12995552?productTrackingContext=search_results/search_shelf/center/1">http://stores.lulu.com/flossmanuals</a>
+</p>
+<p>The <em>Internet Archive</em> has this book available as a full color PDF, as well as EPUB, MOBI, and DjVu versions, all of which you can download for free:
+</p>
+<p><a href="http://www.archive.org/details/MakeYourOwnSugarActivities">http://www.archive.org/details/MakeYourOwnSugarActivities</a>  
+</p>
+<p> The <em>Amazon Kindle Store</em> has exactly the same MOBI version as the Internet Archive does.
+</p>
+<p>If you choose to read this book on a Kindle be aware that the Kindle's narrow screen is not well suited for displaying program listings.  I suggest you refer to the FLOSS Manuals website to see what the code looks like properly formatted.
+ <br/></p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch002_what-is-sugar.xhtml b/app/static/doc/myosa/ch002_what-is-sugar.xhtml
new file mode 100644
index 0000000..096ac7e
--- /dev/null
+++ b/app/static/doc/myosa/ch002_what-is-sugar.xhtml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>What is Sugar?
+</h1>
+<p>Sugar is the user interface designed for the XO laptop. It can now be installed on most PCs, including older models that can't run the latest Windows software. You can also install it on a thumb drive (Sugar on a Stick) and boot your PC from that.
+</p>
+<p>When the XO laptop first came out some people questioned the need for a new user interface. Wouldn't it be better for children to learn something more like what they would use as adults? Why not give them Microsoft Windows instead?
+</p>
+<p>This would be a reasonable question if the goal was to train children to use computers and nothing else. It would be even more reasonable if we could be sure that the software they would use as adults looked and worked like the Microsoft Windows of today.&#160; These are of course not reasonable assumptions.
+</p>
+<p>The OLPC project is not just about teaching computer literacy. It is about teaching <em>everything</em>: reading, writing, arithmetic, history, science, arts and crafts, computer programming, music composition, and everything else. Not only do we expect the child to use the computer for her school work, we expect her to take it home and use it for her own explorations into subjects that interest her.&#160;
+</p>
+<p>This is a great deal more than anyone has done with computers for education, so it is reasonable to rethink how children should work with computers. Sugar is the result of that rethinking.
+</p>
+<p>Sugar has the following unique features:
+</p>
+<h2>The Journal
+</h2>
+<p>The Journal is where all the student's work goes. Instead of files and folders there is a list of Journal entries. The list is sorted in descending order by the date and time it was last worked on. In a way it's like the "Most Recently Used" document menu in Windows, except instead of containing just the last few items it contains everything and is the normal way to save and resume work on something.
+</p>
+<p>The Journal makes it easy to organize your work.&#160; Any work you do is saved to the Journal.&#160; Anything you download from the web goes in the Journal.&#160; If you've ever downloaded a file using a web browser, then had to look for it afterwards because it went in some directory other than the one you expected, or if you ever had to help your parents when they were in a similar situation, you can understand the value of the Journal.
+ <br/></p>
+<p>The Journal has metadata for each item in it. Metadata is information about information. Every Journal entry has a title, a description, a list of keywords, and a screen shot of what it looked like the last time it was used. It has an activity id that links it to the Activity that created it, and it may have a MIME type as well (which is a way of identifying Journal entries so that items not created by an Activity may still be used by an Activity that supports that MIME type).
+</p>
+<p>In addition to these common metadata items a Journal entry may be given custom metadata by an Activity. For instance, the <strong>Read</strong> Activity uses custom metadata to save the page number you were reading when you quit the Activity. When you resume reading later the Activity will put you on that page again.
+</p>
+<p>In addition to work created by Activities, the Journal can contain Activities themselves. To install an Activity you can use the <strong>Browse</strong> Activity to visit the website <a href="http://activities.sugarlabs.org/">http://activities.sugarlabs.org</a> and download it. It will automatically be saved to the Journal and be ready for use. If you don't want the Activity any more, simply delete it from the Journal and it's <em>completely gone</em>. No uninstall programs, no dialog boxes telling you that such and such a .DLL doesn't seem to be needed anymore and do you want to delete it? No odd bits and pieces left behind.
+</p>
+<h2>Collaboration
+</h2>
+<p>The second unique feature Sugar is Collaboration. Collaboration means that Activities can be used by more than one person at the same time. While not every Activity needs collaboration and not every Activity that could use it supports it, a really first rate Activity will provide some way to interact with other Sugar users on the network. For instance, all the e-book reading Activities provide a way of giving a copy of the book you're reading (with any notes you added to it) to a friend or to the whole class. The <strong>Write</strong> Activity lets several students work on the same document together. The <strong>Distance</strong> Activity lets two students see how far apart from each other they are.
+</p>
+<p>There are five views of the system you can switch to at the push of a button (Function Keys F1-4). They are:
+</p>
+<ul><li>The Neighborhood View</li>
+ <li> The Friends View</li>
+ <li> The Activity Ring</li>
+ <li> The Journal</li>
+</ul><p>Of these Views, the first two are used for Collaboration.
+</p>
+<p>The Neighborhood View shows icons for everyone on the network. Every icon looks like a stick figure made by putting an "O" above an "X". Each icon has a name, chosen by the student when she sets up her computer. Every icon is displayed in two colors, also chosen by the student. In addition to these "XO" icons there will be icons representing mesh networks and others representing WiFi hot spots. Finally there will be icons representing active Activities that their owners wish to share.
+</p>
+<p>To understand how this works consider the <strong>Chat</strong> Activity. The usual way applications do chat is to have all the participants start up a chat client and visit a particular chat room at the same time. With Sugar it's different. One student starts the Chat Activity on her own computer and goes to the Neighborhood View to invite others on the network to participate. They will see a Chat icon in their own Neighborhood View and they can accept. The act of accepting starts up their own Chat Activity and connects them to the other participants.
+</p>
+<p>The Friends View is similar to the Neighborhood View, but only contains icons for people you have designated as Friends. Collaboration can be offered at three levels: with individual persons, with the whole Neighborhood, and with Friends. Note that the student alone decides who her Friends are. There is no need to ask to be someone's Friend.&#160; It's more like creating a mailing list in email.
+ <br/></p>
+<h2>Security
+</h2>
+<p>Protecting computers from malicious users is very important, and if the computers belong to students it is doubly important. It is also more difficult, because we can't expect young students to remember passwords and keep them secret. Since Sugar runs on top of Linux viruses aren't much of a problem, but malicious Activities definitely are. If an Activity was allowed unrestricted access to the Journal, for instance, it could wipe it out completely. Somebody could write an Activity that seems to be harmless and amusing, but perhaps after some random number of uses it could wipe out a student's work.
+</p>
+<p>The most common way to prevent a program from doing malicious things is to make it run in a sandbox. A sandbox is a way to limit what a program is allowed to do. With the usual kind of sandbox you either have an untrusted program that can't do much of anything or a trusted program that is not restricted at all. An application becomes trusted when a third party vouches for it by giving it a <em>signature</em>. The signature is a mathematical operation done on the program that only remains valid if the program is not modified.
+</p>
+<p>Sugar has a more sophisticated sandbox for Activities than that. No Activity needs to be trusted or is trusted. Every Activity can only work with the Journal in a limited, indirect way. Each Activity has directories specific to it that it can write to, and all other directories and files are limited to read-only access. In this way no Activity can interfere with the workings of any other Activity. In spite of this, an Activity can be made to do what it needs to do.
+</p>
+<h2>Summary
+</h2>
+<p>Sugar is an operating environment designed to support the education of children. It organizes a child's work without needing files and folders. It supports collaboration between students. Finally, it provides a robust security model that prevents malicious programs from harming a student's work.
+</p>
+<p>It would not be surprising to see these features someday adopted by other desktop environments.
+</p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch003_what-is-a-sugar-activity.xhtml b/app/static/doc/myosa/ch003_what-is-a-sugar-activity.xhtml
new file mode 100644
index 0000000..28280ce
--- /dev/null
+++ b/app/static/doc/myosa/ch003_what-is-a-sugar-activity.xhtml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>What is a Sugar Activity?
+</h1>
+<p>A Sugar Activity is a self-contained Sugar application packaged in a .xo bundle.
+</p>
+<p>An .xo bundle is an archive file in the Zip format.&#160; It contains:
+</p>
+<ul><li>A MANIFEST file listing everything in the bundle</li>
+ <li>An <strong>activity.info</strong> file that has attributes describing the Activity as name=value pairs.&#160; These attributes include the Activity name, its version number, an identifier, and other things we will discuss when we write your first Activity.</li>
+ <li>An icon file (in SVG format)</li>
+ <li>Files containing translations of the text strings the Activity uses into many languages</li>
+ <li>The program code to run the Activity</li>
+</ul><p>A Sugar Activity will generally have some Python code that extends a Python class called Activity.&#160; It may also make use of code written in other languages if that code is written in a way that allows it to be used from Python (this is called having <strong>Python bindings</strong>).&#160; It is even possible to write a Sugar Activity without using Python at all, but this is beyond the scope of this book.
+</p>
+<p>There are only a few things that an Activity can depend on being included with every version of Sugar. These include modules like Evince (PDF and other document viewing), Gecko (rendering web pages), and Python libraries like PyGTK and PyGame.&#160; Everything needed to run the Activity that is <em>not</em> supplied by Sugar must go in the bundle file.&#160; A question sometimes heard on the mailing lists is "How do I make Sugar install <em>X</em> the first time my Activity is run?"&#160; The answer: you don't.&#160; If you need <em>X</em> it needs to go in the bundle.&#160;
+</p>
+<p>You can install an Activity by copying or downloading it to the Journal. You uninstall it by removing it from the Journal. There is no <em>Install Shield</em> to deal with, no deciding where you want the files installed, no possibility that installing a new Activity will make an already installed Activity stop working.
+</p>
+<p>An Activity generally creates and reads objects in the Journal.&#160; A first rate Activity will provide some way for the Activity to be shared by multiple users.
+</p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch004_what-do-i-need-to-know-to-write-a-sugar-activity.xhtml b/app/static/doc/myosa/ch004_what-do-i-need-to-know-to-write-a-sugar-activity.xhtml
new file mode 100644
index 0000000..1faa0ca
--- /dev/null
+++ b/app/static/doc/myosa/ch004_what-do-i-need-to-know-to-write-a-sugar-activity.xhtml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>What Do I Need To Know To Write A Sugar Activity?&#160;
+</h1>
+<p>If you are going to write Sugar Activities you should learn something about the topics described in this chapter. There is no need to become an expert in any of them, but you should bookmark their websites and skim through their tutorials. This will help you to understand the code samples we'll be looking at.
+</p>
+<h2>Python
+</h2>
+<p>Python is the most used language for writing Activities.&#160; While you can use other languages, most Activities have at least some Python in them.&#160; Sugar provides a Python API that simplifies creating Activities.&#160; While it is possible to write Activities using no Python at all (like <strong>Etoys</strong>), it is unusual.&#160;
+ <br/></p>
+<p>All of the examples in this book are written entirely in Python.
+ <br/></p>
+<p>There are compiled languages and interpreted languages. In a compiled language the code you write is translated into the language of the chip it will run on and it is this translation that is actually run by the OS. In an interpreted language there is a program called an interpreter that reads the code you write and does what the code tells it to do. (This is over simplified, but close enough to the truth for this chapter).
+</p>
+<p>Python is an interpreted language. There are advantages to having a language that is compiled and there are advantages to having an interpreted language. The advantages Python has for developing Activities are:
+</p>
+<ul><li>It is portable. In other words, you can make your program run on any chip and any OS without making a version specific to each one. Compiled programs only run on the OS and chip they are compiled for.</li>
+ <li> Since the source code is the thing being run, you can't give someone a Python program without giving them the source code. You can learn a lot about Activity programming by studying other people's code, and there is plenty of it to study.</li>
+ <li> It is an easy language for new programmers to learn, but has language features that experienced programmers need.</li>
+ <li> It is widely used. One of the best known Python users is Google. They use it enough that they have started a project named &#8220;Unladen Swallow&#8221; to make Python programs run faster.</li>
+</ul><p>The big advantage of a compiled language is that it can run much faster than an interpreted language. However, in actual practice a Python program can perform as well as a compiled program. To understand why this is you need to understand how a Python program is made.
+</p>
+<p>Python is known as a &#8220;glue&#8221; language. The idea is that you have components written in various languages (usually C and C++) and they have Python bindings. Python is used to &#8220;glue&#8221; these components together to create applications. In most applications the bulk of the application's function is done by these compiled components, and the application spends relatively little time running the Python code that glues the components together.
+</p>
+<p>In addition to Activities using Python most of the Sugar environment itself is written in Python.
+</p>
+<p>If you have programmed in other languages before there is a good tutorial for learning Python at the Python website: <a href="http://docs.python.org/tutorial/">http://docs.python.org/tutorial/</a>.&#160; If you're just starting out in programming you might check out <em>Invent Your Own Computer Games With Python</em>, which you can read for free at <a href="http://inventwithpython.com/" target="_top">http://inventwithpython.com/</a>.
+</p>
+<h2>PyGTK
+</h2>
+<p>GTK+ is a set of components for creating user interfaces. These components include things like buttons, scroll bars, list boxes, and so on. It is used by GNOME desktop environment and the applications that run under it. Sugar Activities use a special GNOME theme that give GTK+ controls a unique look.
+</p>
+<p>PyGTK is a set of Python bindings that let you use GTK+ components in Python programs. There is a tutorial showing how to use it at the PyGTK website: <a href="http://www.pygtk.org/tutorial.html">http://www.pygtk.org/tutorial.html</a>.
+</p>
+<h2>PyGame
+</h2>
+<p>The alternative to using PyGTK for your Activity is PyGame. PyGame can create images called sprites and move them around on the screen. As you might expect, PyGame is mostly used for writing games. It is less commonly used in Activities than PyGTK.
+</p>
+<p>The tutorial to learn about PyGame is at the PyGame website: <a href="http://www.pygame.org/wiki/tutorials">http://www.pygame.org/wiki/tutorials</a>. The website also has a bunch of pygame projects you can download and try out.
+</p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch006_setting-up-a-development-environment.xhtml b/app/static/doc/myosa/ch006_setting-up-a-development-environment.xhtml
new file mode 100644
index 0000000..2556f82
--- /dev/null
+++ b/app/static/doc/myosa/ch006_setting-up-a-development-environment.xhtml
@@ -0,0 +1,133 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>Setting Up a Sugar Development Environment
+</h1>
+<p>It is not currently practical to develop Activities for the XO on the XO. It's not so much that you can't do it, but that it's easier and more productive to do your development and testing on another machine running a more conventional OS. This gives you access to better tools and it also enables you to simulate collaboration between two computers running Sugar using only one computer.
+</p>
+<h2>Install Linux Or Use A Virtual Machine?
+</h2>
+<p>Even though Sugar runs on Linux it is possible to run a complete instance of Sugar in a virtual machine that runs on Windows.&#160; A virtual machine is a way to run one operating system on top of another one. The operating system being run is fooled into thinking it has the whole computer to itself. (Computer industry pundits will tell you that using virtual machines is the newest new thing out there. Old timers like me know that IBM was doing it on their mainframe computers back in the 1970's).
+</p>
+<p>For awhile this was actually the recommended way to develop Activities. The version of Linux that Sugar used was different enough from regular Linux distributions that even Linux users were running Sugar in a virtual machine on top of Linux.
+</p>
+<p>The situation has improved, and most current Linux distributions have a usable Sugar environment.
+</p>
+<p>If you're used to Windows you might think that running Sugar in a VM from Windows instead of installing Linux might be the easier option. In practice it is not. Linux running in a VM is still Linux, so you're still going to have to learn some things about Linux to do Activity development. Also, running a second OS in a VM requires a really powerful machine with gigabytes of memory. On the other hand, I do my Sugar development using Linux on an IBM NetVista Pentium IV I bought used for a little over a hundred dollars, shipping included. It is more than adequate.
+</p>
+<p>Installing Linux is not the test of manhood it once was. Anyone can do it. The GNOME desktop provided with Linux is very much like Windows so you'll feel right at home using it.
+</p>
+<p>When you install Linux you have the option to do a dual boot, running Linux and Windows on the same computer (but not at the same time). This means you set aside a disk partition for use by Linux and when you start the computer a menu appears asking which OS you want to start up. The Linux install will even create the partition for you, and a couple of gigabytes is more than enough disk space. Sharing a computer with a Linux installation will not affect your Windows installation at all.
+ <br/></p>
+<p>Sugar Labs has been working to get Sugar included with all Linux distributions. If you already have a favorite distribution, chances are the latest version of it includes Sugar. Fedora, openSuse, Debian, and Ubuntu all include Sugar. If you already use Linux, see if Sugar is included in your distribution. If not, Fedora is what is used by the XO computer so Fedora 10 or later might be your best bet. You can download the Fedora install CD or DVD here: <a href="https://fedoraproject.org/get-fedora">https://fedoraproject.org/get-fedora</a>.
+</p>
+<p>It is worth pointing out that all of the other tools I'm recommending are included in every Linux distribution, and they can be installed with no more effort than checking a check box. The same tools often will run on Windows, but installing them there is more work than you would expect for Windows programs.
+</p>
+<p>If you are unwilling to install and learn about Linux but still want to develop Activities one option you have is to develop a standalone Python program that uses PyGame of PyGTK and make it do what you'd like your Activity to do.&#160; You could then turn over your program to someone else who could convert it into a Sugar Activity.&#160; You could develop such a Python program on Windows or on the Macintosh.
+ <br/></p>
+<p>If you want to do development on a Macintosh running Sugar in a virtual machine may be a more attractive option. If you want to try it details will be found here: <a href="http://wiki.laptop.org/go/Developers/Setup">http://wiki.laptop.org/go/Developers/Setup.</a>&#160; It may also be possible to install Fedora Linux on an Intel or Power PC Macintosh as a dual boot, just like you can do with Windows.&#160; Check the Fedora website for details.
+</p>
+<p>Another option for Mac users is to use <em>Sugar on a Stick</em> as a test environment.&#160; You can learn about that here: <a href="http://wiki.sugarlabs.org/go/Sugar_on_a_Stick">http://wiki.sugarlabs.org/go/Sugar_on_a_Stick</a>.
+</p>
+<div class="objavi-forcebreak">
+</div>
+<h2>What About Using sugar-jhbuild?
+</h2>
+<p><strong>Sugar-jhbuild</strong> is a script that downloads the source code for the latest version of all the Sugar modules and compiles it into a subdirectory of your home directory.&#160; It doesn't actually install Sugar on your system.&#160; Instead, you run it out of the directory you installed it in.&#160; Because of the way it is built and run it doesn't interfere with the modules that make up your normal desktop. If you are developing Sugar itself, or if you are developing Activities that depend on the very latest Sugar features you'll need to run sugar-jhbuild.
+</p>
+<p>Running this script is a bit more difficult than just installing the Sugar packages that come with the distribution.&#160; You'll need to install Git and Subversion, run a Git command from the terminal to download the sugar-jhbuild script, then run the script with several different options which download more code, ask you to install more packages, and ultimately compile everything.&#160; It may take you a couple of hours to do all the steps.&#160; When you're done you'll have an up to date test environment that you can run as an alternative to <strong>sugar-emulator</strong>.&#160; There is no need to uninstall sugar-emulator; both can coexist.
+</p>
+<p>You run it with these commands:
+</p>
+<pre>cd sugar-jhbuild
+./sugar-jhbuild run sugar-emulator
+</pre>
+<p>Should you consider using it? The short answer is no. A longer answer is <em>probably not yet</em>.
+</p>
+<p>If you want your Activities to reach the widest possible audience you <em>don't</em> want the latest Sugar. In fact, if you want a test environment that mimics what is on most XO computers right now you need to use Fedora 10. Because updating operating systems in the field can be a major undertaking for a school most XO's will be running Sugar .82 or older for quite some time.
+</p>
+<p>Of course it is also important to have developers that want to push the boundaries of what Sugar can do. If after developing some Activities you decide you need to be one of them you can learn about running sugar-jhbuild here: <a href="http://wiki.sugarlabs.org/go/DevelopmentTeam/Jhbuild">http://wiki.sugarlabs.org/go/DevelopmentTeam/Jhbuild.</a>
+</p>
+<p>Strictly speaking sugar-jhbuild is just the script that downloads and compiles Sugar.&#160; If you wanted to be correct you would say "Run the copy of <strong>sugar-emulator</strong> you made with sugar-jhbuild".&#160; Most Sugar developers would just say "Run sugar-jhbuild" and that's what I'll say in this book.
+ <br/></p>
+<div class="objavi-forcebreak">
+</div>
+<h2>Python
+</h2>
+<p>We'll be doing all the code samples in Python so you'll need to have Python installed.&#160; Python comes with every Linux distribution.&#160; You can download installers for Windows and the Macintosh at <a href="http://www.python.org">http://www.python.org/</a>.
+ <br/></p>
+<h2>Eric
+</h2>
+<p>Developers today expect their languages to be supported by an <strong>Integrated Development Environment</strong> and Python is no exception. An <strong>IDE</strong> helps to organize your work and provides text editing and a built in set of programming and debugging tools.
+</p>
+<p><img alt="Eric the Python IDE" src="static/ActivitiesGuideSugar-eric-en.jpg" height="491" width="640"/>&#160;
+</p>
+<p>There are two Python IDE's I have tried: Eric and Idle. Eric is the fancier of the two and I recommend it. Every Linux distribution should include it. It looks like it might work on Windows too. You can learn more about it at the Eric website: <a href="http://eric-ide.python-projects.org/">http://eric-ide.python-projects.org/</a>.
+</p>
+<div class="objavi-forcebreak">
+</div>
+<h2>SPE (Stani's Python Editor)
+</h2>
+<p> This is an IDE I discovered while writing this book.&#160; It comes with Fedora and in addition to being a Python editor it will make <strong>UML</strong> diagrams of your code and show <strong>PyDoc</strong> for it.&#160; Here is SPE showing a UML diagram for one of the Activities in this book:
+</p>
+<p><img alt="spe.jpg" src="static/ActivitiesGuideSugar-spe-en.jpg" height="354" width="600"/></p>
+<p>If you're an experienced developer you might find this a useful alternative to Eric.&#160; If you're just starting out Eric should meet your needs pretty well.
+ <br/></p>
+<h2>Other IDE's
+ <br/></h2>
+<p>There is also a commercial Python IDE called Wingware, which has a version you can use for free.&#160; You can learn more about it at <a href="http://www.wingware.com">http://www.wingware.com/</a>.
+ <br/></p>
+<div class="objavi-forcebreak">
+</div>
+<h2>Inkscape
+</h2>
+<p>Inkscape is a tool for creating images in SVG format. Sugar uses SVG for Activity icons and other kinds of artwork. The &#8220;XO&#8221; icon that represents each child in the Neighborhood view is an SVG file that can be modified.
+</p>
+<p><img alt="Using Inkscape to create an Activity icon" src="static/ActivitiesGuideSugar-inkscape-en.jpg" height="550" width="640"/>&#160;
+</p>
+<p>Inkscape comes with every Linux distribution, and can be installed on Windows as well. You can learn more about it here: <a href="http://www.inkscape.org/">http://www.inkscape.org/</a>.
+</p>
+<div class="objavi-forcebreak">
+</div>
+<h2>Git
+</h2>
+<p>Git is a version control system. It stores versions of your program code in a way that makes them easy to get back. Whenever you make changes to your code you ask Git to store your code in its repository. If you need to look at an old version of that code later you can. Even better, if some problem shows up in your code you can compare your latest code to an old, working version and see exactly what lines you changed.
+</p>
+<p><img alt="git11_1.jpg" src="static/ActivitiesGuideSugar-git11_1-en.jpg" height="530" width="581"/>&#160;&#8286;
+</p>
+<p>If there are two people working on the same program independently a version control system will merge their changes together automatically.
+</p>
+<p>Suppose you're working on a major new version of your Activity when someone finds a really embarrassing bug in the version you just released. If you use Git you don't need to tell people to live with it until the next release, which could be months away. Instead you can create a branch of the previous version and work on it alongside the version you're enhancing. In effect Git treats the old version you're fixing and the version you're improving as two separate projects.
+</p>
+<p>You can learn more about Git at the Git website: <a href="http://git-scm.com/">http://git-scm.com/</a>.
+</p>
+<p>When you're ready for a Git repository for your project you can set one up here: <a href="http://git.sugarlabs.org/">http://git.sugarlabs.org/</a>.&#160; I will have more to say about setting up and using a Git repository later in this book.
+ <br/></p>
+<p>There is a Git repository containing all the code examples from this book.&#160; Once you have Git installed you can copy the repository to your computer with this command:
+</p>
+<pre><code>git clone git://git.sugarlabs.org/\
+myo-sugar-activities-examples/</code><code>mainline.git</code></pre>
+<p>This command should be typed all on one line.&#160; The backslash (\) character at the end of the first line is used in Linux to continue a long command to a second line.&#160; It is used here to make the command fit on the page of the printed version of this book.&#160; When you type in the command you can leave it out and type <strong>myo-sugar-activities-examples/mainline.git</strong> immediately following <strong>git.sugarlabs.org/</strong>.
+</p>
+<p>This convention of splitting long commands over multiple lines will be used throughout this book.&#160;&#160; In addition to that, the code in Git will generally have longer lines than you'll see in the code listings in the book.&#160; For that reason I'd recommend that you not try to type in the code from these listings, but use the code in Git instead.
+ <br/></p>
+<h2>The GIMP
+</h2>
+<p>The GIMP is one of the most useful and badly named programs ever developed. You can think of it as a free version of Adobe Photoshop. If you need to work with image files (other than SVG's) you need this program.
+</p>
+<p><img alt="Using The GIMP to make a screen capture" src="static/ActivitiesGuideSugar-thegimp-en.jpg" height="390" width="640"/>&#160;
+</p>
+<p>You may never need this program to develop the Activity itself, but when it's time to distribute the Activity you'll use it to create screen shots of your Activity in action. Nothing sells an Activity to a potential user like good screen shots.
+</p>
+<h2>Sugar Emulation
+</h2>
+<p>Most Linux distributions should have Sugar included. In Fedora you can run Sugar as an alternative desktop environment. When you log in to GDM Sugar appears as a desktop selection alongside GNOME, KDE, Window Maker, and any other window managers you have installed.
+</p>
+<p>This is not the normal way to use Sugar for testing. The normal way uses a tool called Xephyr to run a Sugar environment in a window on your desktop. In effect, Xephyr runs an X session inside a window and Sugar runs in that. You can easily take screen shots of Sugar in action, stop and restart Sugar sessions without restarting the computer, and run multiple copies of Sugar to test collaboration.
+</p>
+<p><img alt="sugar-emulator in action" src="static/ActivitiesGuideSugar-ReadEtexts_02-en.jpg" height="480" width="640"/>&#160;
+</p>
+<p>I'll have more to say about this when it's time to test your first Activity.
+</p> &#160;
+<p>
+</p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch007_creating-your-first-activity.xhtml b/app/static/doc/myosa/ch007_creating-your-first-activity.xhtml
new file mode 100644
index 0000000..84c414c
--- /dev/null
+++ b/app/static/doc/myosa/ch007_creating-your-first-activity.xhtml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>Creating your First Sugar Activity
+</h1>
+<h2 class="western">Make A Standalone Python Program First
+</h2>
+<p class="western">The best advice I could give a beginning Activity developer is to make a version of your Activity that can run on its own, outside of the Sugar environment. Testing and debugging a Python program that stands alone is faster, easier and less tedious than doing the same thing with a similar Activity. You'll understand why when you start testing your first Activity.
+</p>
+<p class="western">The more bugs you find before you turn your code into an Activity the better. In fact, it's a good idea to keep a standalone version of your program around even after you have the Activity version well underway. I used my standalone version of <strong>Read Etexts</strong> to develop the text to speech with highlighting feature. This saved me a <em>lot</em> of time, which was especially important because I was figuring things out as I went.
+</p>
+<p>Our first project will be a version of the Read Etexts Activity I wrote.
+ <br/></p>
+<h2 class="western">Inherit From The sugar.activity.Activity Class
+</h2>
+<p>Next we're going to take our standalone Python program and make an Activity out of it.&#160; To do this we need to understand the concept of <em>inheritance</em>.&#160; In everyday speech inheritance means getting something from your parents that you didn't work for.&#160; A king will take his son to a castle window and say, "Someday, lad, this will all be yours!"&#160; That's inheritance.
+</p> In the world of computers programs can have parents and inherit things from them.&#160; Instead of inheriting property, they inherit code. There is a piece of Python code called sugar.activity.Activity that's the best parent an Activity could hope to have, and we're going to convince it to adopt our program.&#160; This doesn't mean that our program will never have to work again,&#160; but it won't have to work as much.
+<br/><h2>Package The Activity
+</h2>
+<p>Now we have to package up our code to make it something that can be run under Sugar and distributed as an .xo file.&#160; This involves setting up a MANIFEST, activity.info, setup.py, and creating a suitable icon with Inkscape.
+</p>
+<div class="objavi-forcebreak">
+</div>
+<h2>Add Refinements
+</h2>
+<p>Every Activity will have the basic Activity toolbar. For most Activities this will not be enough, so we'll need to create some custom toolbars as well. Then we need to hook them up to the rest of the Activity code so that what happens to the toolbar triggers actions in the Activity and what happens outside the toolbar is reflected in the state of the toolbar.
+</p>
+<p>In addition to toolbars we'll look at some other ways to spiff up your Activity.
+ <br/></p>
+<h2> Put The Project Code In Version Control
+ <br/></h2>
+<p>By this time we'll have enough code written that it's worth protecting and sharing with the world.&#160; To do that we need to create a Git repository and add our code to it.&#160; We'll also go over the basics of using Git.
+ <br/></p>
+<h2> Going International With Pootle
+</h2>
+<p>Now that our code is in Git we can request help from our first collaborator: the Pootle translation system.&#160; With a little setup work we can get volunteers to make translated versions of our Activity available.
+</p>
+<h2>Distributing The Activity
+</h2>
+<p>In this task we'll take our Activity and set it up on <a href="http://activities.sugarlabs.org">http://activities.sugarlabs.org</a>&#160; plus we'll package up the source code so it can be included in Linux distributions.
+ <br/></p>
+<h2> Add Collaboration
+</h2>
+<p>Next we'll add code to share e-books with Friends and the Neighborhood.
+ <br/></p>
+<h2>Add Text To Speech
+</h2>
+<p>Text to Speech with word highlighting is next.&#160; Our simple project will become a Kindle-killer!
+ <br/></p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch008_a-standalone-python-program-for-reading-etexts.xhtml b/app/static/doc/myosa/ch008_a-standalone-python-program-for-reading-etexts.xhtml
new file mode 100644
index 0000000..4afdb21
--- /dev/null
+++ b/app/static/doc/myosa/ch008_a-standalone-python-program-for-reading-etexts.xhtml
@@ -0,0 +1,244 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1> A Standalone Python Program For Reading Etexts
+</h1>
+<h2>The Program
+ <br/></h2>
+<p>Our example program is based on the first Activity I wrote, <strong>Read Etexts</strong>.&#160; This is a program for reading free e-books.
+ <br/></p>
+<p>The oldest and best source of free e-books is a website called <em>Project Gutenberg</em> <a href="http://www.gutenberg.org/wiki/Main_Page).">(</a><a href="http://www.gutenberg.org/wiki/Main_Page" target="_top">http://www.gutenberg.org/wiki/Main_Page</a>).&#160; They create books in plain text format, in other words the kind of file you could make if you typed a book into Notepad and hit the Enter key at the end of each line.&#160; They have thousands of books that are out of copyright, including some of the best ever written.&#160; Before you read further go to that website and pick out a book that interests you.&#160; Check out the "Top 100" list to see the most popular books and authors.
+</p>
+<p>The program we're going to create will read books in plain text format only.
+</p>
+<p>There is a Git repository containing all the code examples in this book.&#160; Once you have Git installed you can copy the repository to your computer with this command:
+</p>
+<pre>git clone git://git.sugarlabs.org/\
+myo-sugar-activities-examples/mainline.git</pre>
+<p>The code for our standalone Python program will be found in the directory <strong>Make_Standalone_Python</strong> in a file named <strong>ReadEtexts.py</strong>.&#160; It looks like this:
+ <br/></p>
+<pre>#! /usr/bin/env python
+import sys
+import os
+import zipfile
+import pygtk
+import gtk
+import getopt
+import pango
+
+page=0
+PAGE_SIZE = 45
+
+class ReadEtexts():
+
+ def keypress_cb(self, widget, event):
+ "Respond when the user presses one of the arrow keys"
+ keyname = gtk.gdk.keyval_name(event.keyval)
+ if keyname == 'plus':
+ self.font_increase()
+ return True
+ if keyname == 'minus':
+ self.font_decrease()
+ return True
+ if keyname == 'Page_Up' :
+ self.page_previous()
+ return True
+ if keyname == 'Page_Down':
+ self.page_next()
+ return True
+ if keyname == 'Up' or keyname == 'KP_Up' \
+ or keyname == 'KP_Left':
+ self.scroll_up()
+ return True
+ if keyname == 'Down' or keyname == 'KP_Down' \
+ or keyname == 'KP_Right':
+ self.scroll_down()
+ return True
+ return False
+
+ def page_previous(self):
+ global page
+ page=page-1
+ if page &lt; 0: page=0
+ self.show_page(page)
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ v_adjustment.value = v_adjustment.upper - \
+ v_adjustment.page_size
+
+ def page_next(self):
+ global page
+ page=page+1
+ if page &gt;&#8286;= len(self.page_index): page=0
+ self.show_page(page)
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ v_adjustment.value = v_adjustment.lower
+
+ def font_decrease(self):
+ font_size = self.font_desc.get_size() / 1024
+ font_size = font_size - 1
+ if font_size &lt; 1:
+ font_size = 1
+ self.font_desc.set_size(font_size * 1024)
+ self.textview.modify_font(self.font_desc)
+
+ def font_increase(self):
+ font_size = self.font_desc.get_size() / 1024
+ font_size = font_size + 1
+ self.font_desc.set_size(font_size * 1024)
+ self.textview.modify_font(self.font_desc)
+
+ def scroll_down(self):
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ if v_adjustment.value == v_adjustment.upper - \
+ v_adjustment.page_size:
+ self.page_next()
+ return
+ if v_adjustment.value &lt; v_adjustment.upper -\
+ v_adjustment.page_size:
+ new_value = v_adjustment.value + \
+ v_adjustment.step_increment
+ if new_value &gt; v_adjustment.upper -\
+ v_adjustment.page_size:
+ new_value = v_adjustment.upper -\
+ v_adjustment.page_size
+ v_adjustment.value = new_value
+
+ def scroll_up(self):
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ if v_adjustment.value == v_adjustment.lower:
+ self.page_previous()
+ return
+ if v_adjustment.value &gt; v_adjustment.lower:
+ new_value = v_adjustment.value - \
+ v_adjustment.step_increment
+ if new_value &lt; v_adjustment.lower:
+ new_value = v_adjustment.lower
+ v_adjustment.value = new_value
+
+ def show_page(self, page_number):
+ global PAGE_SIZE, current_word
+ position = self.page_index[page_number]
+ self.etext_file.seek(position)
+ linecount = 0
+ label_text = '\n\n\n'
+ textbuffer = self.textview.get_buffer()
+ while linecount &lt; PAGE_SIZE:
+ line = self.etext_file.readline()
+ label_text = label_text + unicode(line,
+ 'iso-8859-1')
+ linecount = linecount + 1
+ label_text = label_text + '\n\n\n'
+ textbuffer.set_text(label_text)
+ self.textview.set_buffer(textbuffer)
+
+ def save_extracted_file(self, zipfile, filename):
+ "Extract the file to a temp directory for viewing"
+ filebytes = zipfile.read(filename)
+ f = open("/tmp/" + filename, 'w')
+ try:
+ f.write(filebytes)
+ finally:
+ f.close
+
+ def read_file(self, filename):
+ "Read the Etext file"
+ global PAGE_SIZE
+
+ if zipfile.is_zipfile(filename):
+ self.zf = zipfile.ZipFile(filename, 'r')
+ self.book_files = self.zf.namelist()
+ self.save_extracted_file(self.zf,
+ self.book_files[0])
+ currentFileName = "/tmp/" + self.book_files[0]
+ else:
+ currentFileName = filename
+
+ self.etext_file = open(currentFileName,"r")
+ self.page_index = [ 0 ]
+ linecount = 0
+ while self.etext_file:
+ line = self.etext_file.readline()
+ if not line:
+ break
+ linecount = linecount + 1
+ if linecount &gt;= PAGE_SIZE:
+ position = self.etext_file.tell()
+ self.page_index.append(position)
+ linecount = 0
+ if filename.endswith(".zip"):
+ os.remove(currentFileName)
+
+ def destroy_cb(self, widget, data=None):
+ gtk.main_quit()
+
+ def main(self, file_path):
+ self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ self.window.connect("destroy", self.destroy_cb)
+ self.window.set_title("Read Etexts")
+ self.window.set_size_request(640, 480)
+ self.window.set_border_width(0)
+ self.read_file(file_path)
+ self.scrolled_window = gtk.ScrolledWindow(
+ hadjustment=None, vadjustment=None)
+ self.textview = gtk.TextView()
+ self.textview.set_editable(False)
+ self.textview.set_left_margin(50)
+ self.textview.set_cursor_visible(False)
+ self.textview.connect("key_press_event",
+ self.keypress_cb)
+ buffer = self.textview.get_buffer()
+ self.font_desc = pango.FontDescription("sans 12")
+ font_size = self.font_desc.get_size()
+ self.textview.modify_font(self.font_desc)
+ self.show_page(0)
+ self.scrolled_window.add(self.textview)
+ self.window.add(self.scrolled_window)
+ self.textview.show()
+ self.scrolled_window.show()
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ self.window.show()
+ gtk.main()
+
+if __name__ == "__main__":
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], "")
+ ReadEtexts().main(args[0])
+ except getopt.error, msg:
+ print msg
+ print "This program has no options"
+ sys.exit(2)
+</pre>
+<h2> Running The Program
+</h2>
+<p>To run the program you should first make it executable.&#160; You only need to do this once:
+</p>
+<pre>chmod 755 ReadEtexts.py</pre>
+<p>For this example I downloaded the file for <em>Pride and Prejudice</em>.&#160; The program will work with either of the Plain text formats, which are either uncompressed text or a Zip file.&#160; The zip file is named <strong>1342.zip</strong>, and we can read the book by running this from a terminal:
+</p>
+<pre>./ReadEtexts.py 1342.zip</pre>
+<p>This is what the program looks like in action:
+</p>
+<p><img alt="The standalone Read Etexts program in action." src="static/ActivitiesGuideSugar-ReadEtexts_01_1-en.jpg" width="646" height="503"/></p>
+<p>You can use the <em>Page Up, Page Down, Up, Down, Left</em>, and <em>Right</em> keys to navigate through the book and the '+' and '-' keys to adjust the font size.
+</p>
+<div class="objavi-forcebreak">
+</div>
+<h2>How The Program Works
+</h2>
+<p>This program reads through the text file containing the book and divides it into pages of 45 lines each.&#160; We need to do this because the <strong>gtk.TextView</strong> component we use for viewing the text would need a lot of memory to scroll through the whole book and that would hurt performance.&#160; A second reason is that we want to make reading the e-book as much as possible like reading a regular book, and regular books have pages.&#160; If a teacher assigns reading from a book she might say "read pages 35-50 for tommorow".&#160; Finally, we want this program to remember what page you stopped reading on and bring you back to that page again when you read the book next time.&#160; (The program we have so far doesn't do that yet).
+</p>
+<p>To page through the book we use <strong>random access</strong> to read the file.&#160; To understand what random access means to a file, consider a VHS tape and a DVD.&#160; To get to a certain scene in a VHS tape you need to go through all the scenes that came before it, in order.&#160; Even though you do it at high speed you still have to look at all of them to find the place you want to start watching.&#160; This is <strong>sequential access</strong>.&#160; On the other hand a DVD has chapter stops and possibly a chapter menu.&#160; Using a chapter menu you can look at any scene in the movie right away, and you can skip around as you like.&#160; This is random access, and the chapter menu is like an <strong>index</strong>.&#160; Of course you can access the material in a DVD sequentially too.
+</p>
+<p>We need random access to skip to whatever page we like, and we need an index so that we know where each page begins.&#160; We make the index by reading the entire file one line at a time.&#160; Every 45 lines we make a note of how many characters into the file we've gotten and store this information in a Python list.&#160; Then we go back to the beginning of the file and display the first page.&#160; When the program user goes to the next or previous page we figure out what the new page number will be and look in the list entry for that page.&#160; This tells us that page starts 4,200 characters into the file.&#160; We use seek() on the file to go to that character and then we read 45 lines starting at that point and load them into the TextView.
+</p>
+<p>When you run this program notice how fast it is.&#160; Python programs take longer to run a line of code than a compiled language would, but in this program it doesn't matter because the heavy lifting in the program is done by the TextView, which was created in a compiled language.&#160; The Python parts don't do that much so the program doesn't spend much time running them.
+</p>
+<p>Sugar uses Python a lot, not just for Activities but for the Sugar environment itself.&#160; You may read somewhere that using so much Python is "a disaster" for performance.&#160; Don't believe it.
+</p>
+<p>There are no slow programming languages, only slow programmers.
+ <br/></p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch009_inherit-from-sugaractivityactivity.xhtml b/app/static/doc/myosa/ch009_inherit-from-sugaractivityactivity.xhtml
new file mode 100644
index 0000000..94c0a02
--- /dev/null
+++ b/app/static/doc/myosa/ch009_inherit-from-sugaractivityactivity.xhtml
@@ -0,0 +1,279 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>Inherit From sugar.activity.Activity
+</h1>
+<h2>Object Oriented Python
+ <br/></h2>
+<p>Python supports two styles of programming:<strong> procedural</strong> and <strong>object oriented</strong>. Procedural programming is when you have some input data, do some processing on it, and produce an output. If you want to calculate all the prime numbers under a hundred or convert a Word document into a plain text file you'll probably use the procedural style to do that.
+</p>
+<p class="western">Object oriented programs are built up from units called <strong>objects</strong>. An object is described as a collection of fields or attributes containing data along with methods for doing things with that data. In addition to doing work and storing data objects can send messages to one another.
+</p>
+<p>Consider a word processing program. It doesn't have just one input, some process, and one output. It can receive input from the keyboard, from the mouse buttons, from the mouse traveling over something, from the clipboard, etc. It can send output to the screen, to a file, to a printer, to the clipboard, etc. A word processor can edit several documents at the same time too. Any program with a GUI is a natural fit for the object oriented style of programming.
+</p>
+<p>Objects are described by <em>classes</em>. When you create an object you are creating an <em>instance</em> of a class.
+</p>
+<p>There's one other thing that a class can do, which is to<strong> inherit</strong> methods and attributes from another class. When you define a class you can say it <strong>extends</strong> some class, and by doing that in effect your class has the functionality of the other class plus its own functionality. The extended class becomes its parent.
+ <br/></p>
+<p>All Sugar Activities extend a Python class called <strong>sugar.activity.Activity</strong>. This class provides methods that all Activities need. In addition to that, there are methods that you can override in your own class that the parent class will call when it needs to. For the beginning Activity writer three methods are important:
+</p>
+<p><em>__init__()</em>
+</p>
+<p>This is called when your Activity is started up. This is where you will set up the user interface for your Activity, including toolbars.
+</p>
+<p><em>read_file(self, file_path)</em>
+</p>
+<p>This is called when you resume an Activity from a Journal entry. It is called after the <em>__init__()</em> method is called. The file_path parameter contains the name of a temporary file that is a copy of the file in the Journal entry. The file is deleted as soon as this method finishes, but because Sugar runs on Linux if you open the file for reading your program can continue to read it even after it is deleted and it the file will not actually go away until you close it.
+</p>
+<p><em>write_file(self, file_path)</em>
+</p>
+<p>This is called when the Activity updates the Journal entry. Just like with <em>read_file()</em> your Activity does not work with the Journal directly. Instead it opens the file named in file_path for output and writes to it. That file in turn is copied to the Journal entry.
+</p>
+<p>There are three things that can cause <em>write_file()</em> to be executed:
+</p>
+<ul><li>Your Activity closes.</li>
+ <li>Someone presses the <strong>Keep</strong> button in the Activity toolbar.</li>
+ <li>Your Activity ceases to be the active Activity, or someone moves from the Activity View to some other View.</li>
+</ul><p>In addition to updating the file in the Journal entry the <em>read_file()</em> and <em>write_file()</em> methods are used to read and update the metadata in the Journal entry.
+</p>
+<p> When we convert our standalone Python program to an Activity we'll take out much of the code we wrote and replace it with code inherited from the sugar.activity.Activity&#160; class.
+</p>
+<h2>Extending The Activity Class
+</h2>
+<p>Here's a version of our program that extends Activity.&#160; You'll find it in the Git repository in the directory <strong>Inherit_From_sugar.activity.Activity</strong> under the name <strong>ReadEtextsActivity.py</strong>:
+</p>
+<pre>import sys
+import os
+import zipfile
+import pygtk
+import gtk
+import pango
+from sugar.activity import activity
+from sugar.graphics import style
+
+page=0
+PAGE_SIZE = 45
+
+class ReadEtextsActivity(activity.Activity):
+ def __init__(self, handle):
+ "The entry point to the Activity"
+ global page
+ activity.Activity.__init__(self, handle)
+
+ toolbox = activity.ActivityToolbox(self)
+ activity_toolbar = toolbox.get_activity_toolbar()
+ activity_toolbar.keep.props.visible = False
+ activity_toolbar.share.props.visible = False
+ self.set_toolbox(toolbox)
+
+ toolbox.show()
+ self.scrolled_window = gtk.ScrolledWindow()
+ self.scrolled_window.set_policy(gtk.POLICY_NEVER,
+ gtk.POLICY_AUTOMATIC)
+ self.scrolled_window.props.shadow_type = \
+ gtk.SHADOW_NONE
+
+ self.textview = gtk.TextView()
+ self.textview.set_editable(False)
+ self.textview.set_cursor_visible(False)
+ self.textview.set_left_margin(50)
+ self.textview.connect("key_press_event",
+ self.keypress_cb)
+
+ self.scrolled_window.add(self.textview)
+ self.set_canvas(self.scrolled_window)
+ self.textview.show()
+ self.scrolled_window.show()
+ page = 0
+ self.textview.grab_focus()
+ self.font_desc = pango.FontDescription("sans %d" %
+ style.zoom(10))
+ self.textview.modify_font(self.font_desc)
+
+ def keypress_cb(self, widget, event):
+ "Respond when the user presses one of the arrow keys"
+ keyname = gtk.gdk.keyval_name(event.keyval)
+ print keyname
+ if keyname == 'plus':
+ self.font_increase()
+ return True
+ if keyname == 'minus':
+ self.font_decrease()
+ return True
+ if keyname == 'Page_Up' :
+ self.page_previous()
+ return True
+ if keyname == 'Page_Down':
+ self.page_next()
+ return True
+ if keyname == 'Up' or keyname == 'KP_Up' \
+ or keyname == 'KP_Left':
+ self.scroll_up()
+ return True
+ if keyname == 'Down' or keyname == 'KP_Down' \
+ or keyname == 'KP_Right':
+ self.scroll_down()
+ return True
+ return False
+
+ def page_previous(self):
+ global page
+ page=page-1
+ if page &lt; 0: page=0
+ self.show_page(page)
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ v_adjustment.value = v_adjustment.upper -\
+ v_adjustment.page_size
+
+ def page_next(self):
+ global page
+ page=page+1
+ if page &gt;= len(self.page_index): page=0
+ self.show_page(page)
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ v_adjustment.value = v_adjustment.lower
+
+ def font_decrease(self):
+ font_size = self.font_desc.get_size() / 1024
+ font_size = font_size - 1
+ if font_size &lt; 1:
+ font_size = 1
+ self.font_desc.set_size(font_size * 1024)
+ self.textview.modify_font(self.font_desc)
+
+ def font_increase(self):
+ font_size = self.font_desc.get_size() / 1024
+ font_size = font_size + 1
+ self.font_desc.set_size(font_size * 1024)
+ self.textview.modify_font(self.font_desc)
+
+ def scroll_down(self):
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ if v_adjustment.value == v_adjustment.upper - \
+ v_adjustment.page_size:
+ self.page_next()
+ return
+ if v_adjustment.value &lt; v_adjustment.upper -\
+ v_adjustment.page_size:
+ new_value = v_adjustment.value +\
+ v_adjustment.step_increment
+ if new_value &gt; v_adjustment.upper -\
+ v_adjustment.page_size:
+ new_value = v_adjustment.upper -\
+ v_adjustment.page_size
+ v_adjustment.value = new_value
+
+ def scroll_up(self):
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ if v_adjustment.value == v_adjustment.lower:
+ self.page_previous()
+ return
+ if v_adjustment.value &gt; v_adjustment.lower:
+ new_value = v_adjustment.value - \
+ v_adjustment.step_increment
+ if new_value &lt; v_adjustment.lower:
+ new_value = v_adjustment.lower
+ v_adjustment.value = new_value
+
+ def show_page(self, page_number):
+ global PAGE_SIZE, current_word
+ position = self.page_index[page_number]
+ self.etext_file.seek(position)
+ linecount = 0
+ label_text = '\n\n\n'
+ textbuffer = self.textview.get_buffer()
+ while linecount &lt; PAGE_SIZE:
+ line = self.etext_file.readline()
+ label_text = label_text + unicode(line,
+ 'iso-8859-1')
+ linecount = linecount + 1
+ label_text = label_text + '\n\n\n'
+ textbuffer.set_text(label_text)
+ self.textview.set_buffer(textbuffer)
+
+ def save_extracted_file(self, zipfile, filename):
+ "Extract the file to a temp directory for viewing"
+ filebytes = zipfile.read(filename)
+ outfn = self.make_new_filename(filename)
+ if (outfn == ''):
+ return False
+ f = open(os.path.join(self.get_activity_root(),
+ 'instance', outfn), 'w')
+ try:
+ f.write(filebytes)
+ finally:
+ f.close
+
+ def read_file(self, filename):
+ "Read the Etext file"
+ global PAGE_SIZE
+
+ if zipfile.is_zipfile(filename):
+ self.zf = zipfile.ZipFile(filename, 'r')
+ self.book_files = self.zf.namelist()
+ self.save_extracted_file(self.zf,
+ self.book_files[0])
+ currentFileName = os.path.join(
+ self.get_activity_root(),
+ 'instance', self.book_files[0])
+ else:
+ currentFileName = filename
+
+ self.etext_file = open(currentFileName,"r")
+ self.page_index = [ 0 ]
+ linecount = 0
+ while self.etext_file:
+ line = self.etext_file.readline()
+ if not line:
+ break
+ linecount = linecount + 1
+ if linecount &gt;= PAGE_SIZE:
+ position = self.etext_file.tell()
+ self.page_index.append(position)
+ linecount = 0
+ if filename.endswith(".zip"):
+ os.remove(currentFileName)
+ self.show_page(0)
+
+ def make_new_filename(self, filename):
+ partition_tuple = filename.rpartition('/')
+ return partition_tuple[2]
+
+
+</pre>
+<p>This program has some significant differences from the standalone version.&#160; First, note that this line:
+</p>
+<pre>#! /usr/bin/env python
+</pre>
+<p>has been removed.&#160; We are no longer running the program directly from the Python interpreter.&#160; Now Sugar is running it as an Activity.&#160; Notice that much (but not all) of what was in the main() method has been moved to the <em>__init__()</em> method and the <em>main()</em> method has been removed.
+</p>
+<p>Notice too that the <em>class</em> statement has changed:
+</p>
+<p>
+</p>
+<pre>class ReadEtextsActivity(activity.Activity)
+</pre>
+<p>This statement now tells us that class ReadEtextsActivity extends the class <strong>sugar.activity.Activity</strong>.&#160;&#160; As a result it inherits the code that is in that class.&#160; Therefore we no longer need a GTK main loop, or to define a window.&#160; The code in this class we extend will do that for us.
+</p>
+<p> While we gain much from this inheritance, we lose something too: a title bar for the main window.&#160; In a graphical operating environment a piece of software called a <em>window manager</em> is responsible for putting borders on windows, making them resizeable, reducing them to icons, maximizing them, etc.&#160; Sugar uses a window manager named Matchbox which makes each window fill the whole screen and puts no border, title bar, or any other window decorations on the windows.&#160;&#160; As a result of that we can't close our application by clicking on the "X" in the title bar as before.&#160; To make up for this we need to have a toolbar that contains a Close button.&#160; Thus every Activity has an Activity toolbar that contains some standard controls and buttons.&#160; If you look at the code you'll see I'm hiding a couple of controls which we have no use for yet.
+</p>
+<p>The <em>read_file()</em> method is no longer called from the main() method and doesn't seem to be called from anywhere in the program.&#160; Of course it does get called, by some of the Activity code we inherited from our new parent class.&#160; Similarly the <em>__init__() </em>and <em>write_file() </em>methods (if we had a <em>write_file()</em> method) get called by the parent Activity class.
+</p>
+<p>If you're especially observant you might have noticed another change.&#160; Our original standalone program created a temporary file when it needed to extract something from a Zip file.&#160; It put that file in a directory called /tmp.&#160; Our new Activity still creates the file but puts it in a different directory, one specific to the Activity.
+</p>
+<p>
+</p>All writing to the file system is restricted to subdirectories of the path given by <em>self.get_activity_root()</em>.&#160; This method will give you a directory that belongs to your Activity alone.&#160; It will contain three subdirectories with different policies:
+<dl><dt><strong>data</strong></dt>
+ <dd> This directory is used for data such as configuration files.&#160; Files stored here will survive reboots and OS upgrades.</dd>
+</dl><dl><dt><strong>tmp</strong></dt>
+ <dd> This directory is used similar to the /tmp directory, being backed by RAM. It may be as small as 1 MB. This directory is deleted when the activity exits.</dd>
+</dl><dl><dt><strong>instance</strong></dt>
+ <dd> This directory is similar to the <strong>tmp</strong> directory, being backed by the computer's drive rather than by RAM. It is unique per instance. It is used for transfer to and from the Journal. This directory is deleted when the activity exits.</dd>
+</dl><p>
+</p>
+<p>Making these changes to the code is not enough to make our program an Activity.&#160; We have to do some packaging work and get it set up to run from the Sugar emulator.&#160; We also need to learn how to run the Sugar emulator.&#160; That comes next!
+ <br/></p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch010_package-the-activity.xhtml b/app/static/doc/myosa/ch010_package-the-activity.xhtml
new file mode 100644
index 0000000..527b06b
--- /dev/null
+++ b/app/static/doc/myosa/ch010_package-the-activity.xhtml
@@ -0,0 +1,208 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>Package The Activity
+</h1>
+<h2>Add setup.py
+</h2>
+<p> You'll need to add a Python program called <strong>setup.py</strong> to the same directory that you Activity program is in.&#160; Every setup.py is exactly the same as every other setup.py.&#160; The copies in our Git repository look like this:
+ <br/></p>
+<pre>#!/usr/bin/env python
+
+# Copyright (C) 2006, Red Hat, Inc.
+#
+# 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 2 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.
+#
+# You should have received a copy of the GNU General
+# Public License along with this program; if not,
+# write to the Free Software Foundation, Inc.,
+# 51 Franklin St, Fifth Floor, Boston, MA
+# 02110-1301 USA
+
+from sugar.activity import bundlebuilder
+
+bundlebuilder.start()</pre>
+<p>Be sure and copy the entire text above, including the comments.
+ <br/></p>
+<p>The setup.py program is used by sugar for a number of purposes.&#160; If you run setup.py from the command line you'll see the options that are used with it and what they do.
+</p>
+<pre>[jim@simmons bookexamples]$ ./setup.py
+/usr/lib/python2.6/site-packages/sugar/util.py:25:
+DeprecationWarning: the sha module is deprecated;
+use the hashlib module instead
+ import sha
+Available commands:
+
+build Build generated files
+dev Setup for development
+dist_xo Create a xo bundle package
+dist_source Create a tar source package
+fix_manifest Add missing files to the manifest
+genpot Generate the gettext pot file
+install Install the activity in the system
+
+(Type "./setup.py &lt;command&gt; --help" for help about a
+particular command's options.</pre>
+<p>We'll be running some of these commands later on.&#160; Don't be concerned about the <strong>DeprecationWarning</strong> message.&#160; That is just Python's way of telling us that it has a new way of doing something that is better but the old way we are using still works.&#160; The error is coming from code in Sugar itself and should be fixed in some future Sugar release.
+ <br/></p>
+<h2>Create activity.info
+</h2>
+<p> Next create a directory within the one your progam is in and name it <strong>activity</strong>.&#160; Create a file named <strong>activity.info</strong> within that directory and enter the lines below into it.&#160; Here is the one for our first Activity:
+ <br/></p>
+<pre>[Activity]
+name = Read ETexts II
+service_name = net.flossmanuals.ReadEtextsActivity
+icon = read-etexts
+exec = sugar-activity ReadEtextsActivity.ReadEtextsActivity
+show_launcher = no
+activity_version = 1
+mime_types = text/plain;application/zip
+license = GPLv2+</pre>
+<p>This file tells Sugar how to run your Activity.&#160; The properties needed in this file are:
+</p>
+<p>
+ </p><table border="0" cellpadding="4" cellspacing="0"><tbody><tr><td>&#160;<strong>name</strong></td>
+ <td>The name of your Activity as it will appear to the user.
+ <br/></td>
+ </tr><tr><td>&#160;<strong>service_name</strong></td>
+ <td>A unique name that Sugar will use to refer to your Activity.&#160; Any Journal entry created by your Activity will have this name stored in its metadata, so that when someone resumes the Journal entry Sugar knows to use the program that created it to read it.
+ <br/></td>
+ </tr><tr><td valign="top"><strong>icon </strong>
+ <br/></td>
+ <td>The name of the icon file you have created for the Activity.&#160; Since icons are always .svg files the icon file in the example is named read-etexts.svg.
+ <br/></td>
+ </tr><tr><td valign="top"><strong>exec</strong></td>
+ <td>This tells Sugar how to launch your Activity.&#160; What it says is to create an instance of the class <strong>ReadEtextsActivity </strong>which it will find in file <strong>ReadEtextsActivity.py</strong>.
+ <br/></td>
+ </tr><tr><td valign="top"><strong>show_launcher</strong></td>
+ <td>There are two ways to launch an Activity.&#160; The first is to click on the icon in the Activity view.&#160; The second is to resume an entry in the Journal. Activities that don't create Journal entries can only be resumed from the Journal, so there is no point in putting an icon in the Activity ring for them.&#160; Read Etexts is an Activity like that.
+ <br/></td>
+ </tr><tr><td valign="top"><strong>activity_version</strong></td>
+ <td>An integer that represents the version number of your program.&#160; The first version is 1, the next is 2, and so on.
+ <br/></td>
+ </tr><tr><td valign="top"><strong>mime_types</strong></td>
+ <td>Generally when you resume a Journal entry it launches the Activity that created it.&#160; In the case of an e-book it wasn't created by any Activity, so we need another way to tell the Journal which Activity it can use.&#160; A MIME type is the name of a common file format.&#160; Some examples are text/plain, text/html, application/zip and application/pdf.&#160; In this entry we're telling the Journal that our program can handle either plain text files or Zip archive files.
+ <br/></td>
+ </tr><tr><td valign="top"><strong>license</strong></td>
+ <td>Owning a computer program is not like buying a car.&#160; With a car, you're the owner and you can do what you like with it.&#160; You can sell it, rent it out, make it into a hot rod, whatever.&#160; With a computer program there is always a license that tells the person receiving the program what he is allowed to do with it.&#160; GPLv2+ is a popular standard license that can be used for Activities, and since this is <em>my</em> program that is what goes here.&#160; When you're ready to distribute one of <em>your</em> Activities I'll have more to say about licenses.
+ <br/></td>
+ </tr></tbody></table><h2>Create An Icon
+</h2>
+<p> Next we need to create an icon named <strong>read-etexts.svg</strong> and put it in the <strong>activity</strong> subdirectory.&#160;&#8286; We're going to use Inkscape to create the icon.&#160; From the <strong>New</strong> menu in Inkscape select <strong>icon_48x48</strong>.&#160; This will create a drawing area that is a good size.
+</p>
+<p>You don't need to be an expert in Inkscape to create an icon.&#160; In fact the less fancy your icon is the better.&#160; When drawing your icon remember the following points:
+</p>
+<ul><li>Your icon needs to look good in sizes ranging from really, really small to large.</li>
+ <li>It needs to be recognizeable when its really, really small.</li>
+ <li>You only get to use two colors: a stroke color and a fill color.&#160; It doesn't matter which ones you choose because Sugar will need to override your choices anyway, so just use black strokes on a white background.</li>
+ <li>A fill color is only applied to an area that is contained within an unbroken stroke.&#160; If you draw a box and one of the corners doesn't quite connect the area inside that box will not be filled.&#160; Free hand drawing is only for the talented.&#160; Circles, boxes, and arcs are easy to draw with Inkscape so use them when you can.</li>
+ <li>Inkscape will also draw 3D boxes using two point perspective.&#160; Don't use them.&#160; Icons should be flat images.&#160; 3D just doesn't look good in an icon.</li>
+ <li>Coming up with good ideas for icons is tough.&#160; I once came up with a rather nice picture of a library card catalog drawer for <strong>Get Internet Archive Books</strong>.&#160; The problem is, no child under the age of forty has ever seen a card catalog and fewer still understand its purpose.</li>
+</ul><p>When you're done making your icon you need to modify it so it can work with Sugar.&#160; Specifically, you need to make it show Sugar can use its own choice of stroke color and fill color.&#160; The SVG file format is based on XML, which means it is a text file with some special tags in it.&#160; This means that once we have finished editing it in Inkscape we can load the file into Eric and edit it as a text file.
+</p>
+<p>I'm not going to put the entire file in this chapter because most of it you'll just leave alone.&#160; The first part you need to modify is at the very beginning.
+</p>
+<p>Before:
+</p>
+<pre>&lt;?xml version="1.0" encoding="UTF-8" standalone="no"?&gt;
+&lt;!-- Created with Inkscape (http://www.inkscape.org/) --&gt;
+&lt;svg
+</pre>
+<p> After:
+</p>
+<pre>&lt;?xml version="1.0" ?&gt;
+&lt;!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN'
+ 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ &lt;!ENTITY stroke_color "#000000"&gt;
+ &lt;!ENTITY fill_color "#FFFFFF"&gt;
+]&gt;&lt;svg
+</pre>
+<p>Now in the body of the document you'll find references to <em>fill</em> and <em>stroke</em> as part of an attribute called <em>style</em>.&#160; Every line or shape you draw will have these, like this:
+</p>
+<pre>&lt;rect
+ style="fill:#ffffff;stroke:#000000;stroke-opacity:1"
+ id="rect904"
+ width="36.142857"
+ height="32.142857"
+ x="4.1428571"
+ y="7.1428571" /&gt;</pre>
+<p>You need to change each one to look like this:
+</p>
+<pre>&lt;rect
+ style="fill:&amp;fill_color;;stroke:&amp;stroke_color;
+;stroke-opacity:1"
+ id="rect904"
+ width="36.142857"
+ height="32.142857"
+ x="4.1428571"
+ y="7.1428571" /&gt;</pre>
+<p>Note that <em>&amp;stroke_color;</em> and <em>&amp;fill_color;</em> both end with semicolons (;), and semicolons are also used to separate the properties for style.&#160; Because of this it is an extremely common beginner's mistake to leave off the trailing semicolon because two semicolons in a row don't look right.&#160; Be assured that the two semicolons in a row are intentional and absolutely necessary!&#160; Second, the value for style should all go <em>on one line</em>.&#160; We split it here to make it fit on the printed page; do not split it in your own icon!
+ <br/></p>
+<h2>Make a MANIFEST File
+</h2>
+<p> You should remember that setup.py has an option to update a manifest.&#160; Let's try it:
+</p>
+<pre>./setup.py fix_manifest
+/usr/lib/python2.6/site-packages/sugar/util.py:25:
+DeprecationWarning: the sha module is deprecated;
+use the hashlib module instead
+ import sha
+WARNING:root:Missing po/ dir, cannot build_locale
+WARNING:root:Activity directory lacks a MANIFEST file.
+</pre>
+<p>This actually will build a MANIFEST file containing everything in the directory and its subdirectories.&#160; The /po directory it is complaining about is used to translate Activities into different languages.&#160; We can ignore that for now.
+</p>
+<p> The MANIFEST file it creates will contain some extra stuff, so we need to get rid of the extra lines using Eric.&#160; The corrected MANIFEST should look like this:
+</p>
+<pre>setup.py
+ReadEtextsActivity.py
+activity/read-etexts.svg
+activity/activity.info</pre>
+<h2>Install The Activity
+</h2>
+<p> There's just one more thing to do before we can test our Activity under the Sugar emulator.&#160; We need to install it, which in this case means making a symbolic link between the directory we're using for our code in the ~/Activities/ directory.&#160; The symbol ~ refers to the "home" directory of the user we're running Sugar under, and a symbolic link is a way to make a file or directory appear to be located in more than one place without copying it.&#160; We make this symbolic link by running setup.py again:
+</p>
+<pre>./setup.py dev
+</pre>
+<h2>Running Our Activity
+</h2>
+<p>Now at last we can run our Activity under Sugar.&#160; To do that we need to learn how to run <strong>sugar-emulator</strong>.
+</p>
+<p>Fedora doesn't make a menu option for Sugar Emulator, but it's easy to add one yourself.&#160; The command to run is simply
+</p>
+<pre>sugar-emulator</pre>
+<p>If your screen resolution is smaller than the default size sugar-emulator runs at it will run full screen.&#160; This is not convenient for testing, so you may want to specify your own size:
+</p>
+<pre>sugar-emulator -i 800x600</pre>
+<p>Note that this option only exists in Fedora 11 and later.
+</p>
+<p>When you run sugar-emulator a window opens up and the Sugar environment starts up and runs inside it.&#160; It looks like this:
+</p>
+<p><img alt="ReadEtexts_02.jpg" src="static/ActivitiesGuideSugar-ReadEtexts_02-en.jpg" height="480" width="640"/></p>
+<p>When running sugar-emulator you may find that some keys don't seem to work in the Sugar environment.&#160; This is caused by bugs in the <strong>Xephyr</strong> software that creates the window that Sugar runs in.&#160; Sometimes it has difficulty identifying your keyboard and as a result some keys get misinterpreted.&#160; On Fedora 11 I noticed that my function keys did not work, and my regular arrow keys didn't work either although my keypad arrow keys did.&#160; I was able to get my function keys working again by putting this line in <em>~/.sugar/debug</em>:
+</p>
+<pre>run setxkbmap <em>&lt;keymap name&gt;</em></pre>
+<p>This needs more explanation.&#160; First, the symbol "~" refers to your home directory.&#160; Second, any file named starting with a period is considered hidden in Linux, so you'll need to use the option to show hidden files and directories in the GNOME directory browser to navigate to it.&#160; Finally, the keymap name is a two character country code: us for the United States, fr for France, de for Germany, etc.
+ <br/></p>
+<p>To test our Activity we're going to need to have a book in the Journal, so use the <strong>Browse</strong> Activity to visit Project Gutenberg again and download the book of your choice.&#160; This time it's important to download the book in Zip format, because Browse cannot download a plain text file to the Journal.&#160; Instead, it opens the file for viewing as if it was a web page.&#160; If you try the same thing with the Zip file it will create an entry in the Journal.
+</p>
+<p>We can't just open the file with one click in the Journal because our program did not create the Journal entry and there are several Activities that support the MIME type of the Journal entry.&#160; We need to use the Start With menu option like this:
+</p>
+<p><img alt="ReadEtexts_03.jpg" src="static/ActivitiesGuideSugar-ReadEtexts_03-en.jpg" height="480" width="640"/></p>
+<p>When we do open the Journal entry this is what we see:
+</p>
+<p><img alt="ReadEtexts_04.jpg" src="static/ActivitiesGuideSugar-ReadEtexts_04-en.jpg" height="480" width="640"/></p>
+<p>Technically, this is the first <strong>iteration</strong> of our Activity.&#160; (Iteration is a vastly useful word meaning something you do more than once.&#160; In this book we're building our Activity a bit at a time so I can demonstrate Activity writing principles, but actually building a program in pieces, testing it, getting feedback, and building a bit more can be a highly productive way of creating software.&#160; Using the word <em>iteration</em> to describe each step in the process makes the process sound more formal than it really is).
+</p>
+<p>While this Activity might be good enough to show your own mother, we really should improve it a bit before we do that.&#160; That part comes next.
+ <br/></p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch011_add-refinements.xhtml b/app/static/doc/myosa/ch011_add-refinements.xhtml
new file mode 100644
index 0000000..0cd76dd
--- /dev/null
+++ b/app/static/doc/myosa/ch011_add-refinements.xhtml
@@ -0,0 +1,625 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>Add Refinements
+</h1>
+<h2>Toolbars
+</h2>
+<p>It is a truth universally acknowledged that a first rate Activity needs good Toolbars.&#160; In this chapter we'll learn how to make them.&#160; We're going to put the toolbar classes in a separate file from the rest, because there are two styles of toolbar (old and new) and we may want to support both in our Activity.&#160; If we have two different files containing toolbar classes our code can decide at runtime which one it wants to use.&#160; For now, this code supports the old style, which works with every version of Sugar.&#160; The new style is currently only supported by <em>Sugar on a Stick. </em>
+</p>
+<p>There is a file called <strong>toolbar.py</strong> in the <strong>Add_Refinements</strong> directory of the Git repository that looks like this:
+ <br/></p>
+<pre>from gettext import gettext as _
+import re
+
+import pango
+import gobject
+import gtk
+
+from sugar.graphics.toolbutton import ToolButton
+from sugar.activity import activity
+
+class ReadToolbar(gtk.Toolbar):
+ __gtype_name__ = 'ReadToolbar'
+
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+ self.back = ToolButton('go-previous')
+ self.back.set_tooltip(_('Back'))
+ self.back.props.sensitive = False
+ self.insert(self.back, -1)
+ self.back.show()
+
+ self.forward = ToolButton('go-next')
+ self.forward.set_tooltip(_('Forward'))
+ self.forward.props.sensitive = False
+ self.insert(self.forward, -1)
+ self.forward.show()
+
+ num_page_item = gtk.ToolItem()
+
+ self.num_page_entry = gtk.Entry()
+ self.num_page_entry.set_text('0')
+ self.num_page_entry.set_alignment(1)
+ self.num_page_entry.connect('insert-text',
+ self.num_page_entry_insert_text_cb)
+
+ self.num_page_entry.set_width_chars(4)
+
+ num_page_item.add(self.num_page_entry)
+ self.num_page_entry.show()
+
+ self.insert(num_page_item, -1)
+ num_page_item.show()
+
+ total_page_item = gtk.ToolItem()
+
+ self.total_page_label = gtk.Label()
+
+ label_attributes = pango.AttrList()
+ label_attributes.insert(pango.AttrSize(
+ 14000, 0, -1))
+ label_attributes.insert(pango.AttrForeground(
+ 65535, 65535, 65535, 0, -1))
+ self.total_page_label.set_attributes(
+ label_attributes)
+
+ self.total_page_label.set_text(' / 0')
+ total_page_item.add(self.total_page_label)
+ self.total_page_label.show()
+
+ self.insert(total_page_item, -1)
+ total_page_item.show()
+
+ def num_page_entry_insert_text_cb(self, entry, text,
+ length, position):
+ if not re.match('[0-9]', text):
+ entry.emit_stop_by_name('insert-text')
+ return True
+ return False
+
+ def update_nav_buttons(self):
+ current_page = self.current_page
+ self.back.props.sensitive = current_page &gt; 0
+ self.forward.props.sensitive = \
+ current_page &lt; self.total_pages - 1
+
+ self.num_page_entry.props.text = str(
+ current_page + 1)
+ self.total_page_label.props.label = \
+ ' / ' + str(self.total_pages)
+
+ def set_total_pages(self, pages):
+ self.total_pages = pages
+
+ def set_current_page(self, page):
+ self.current_page = page
+ self.update_nav_buttons()
+
+class ViewToolbar(gtk.Toolbar):
+ __gtype_name__ = 'ViewToolbar'
+
+ __gsignals__ = {
+ 'needs-update-size': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([])),
+ 'go-fullscreen': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([]))
+ }
+
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+ self.zoom_out = ToolButton('zoom-out')
+ self.zoom_out.set_tooltip(_('Zoom out'))
+ self.insert(self.zoom_out, -1)
+ self.zoom_out.show()
+
+ self.zoom_in = ToolButton('zoom-in')
+ self.zoom_in.set_tooltip(_('Zoom in'))
+ self.insert(self.zoom_in, -1)
+ self.zoom_in.show()
+
+ spacer = gtk.SeparatorToolItem()
+ spacer.props.draw = False
+ self.insert(spacer, -1)
+ spacer.show()
+
+ self.fullscreen = ToolButton('view-fullscreen')
+ self.fullscreen.set_tooltip(_('Fullscreen'))
+ self.fullscreen.connect('clicked',
+ self.fullscreen_cb)
+ self.insert(self.fullscreen, -1)
+ self.fullscreen.show()
+
+ def fullscreen_cb(self, button):
+ self.emit('go-fullscreen')
+</pre>
+<p>Another file in the same directory of the Git repository is named <strong>ReadEtextsActivity2.py.</strong>&#160; It looks like this:
+</p>
+<pre>import os
+import zipfile
+import gtk
+import pango
+from sugar.activity import activity
+from sugar.graphics import style
+from toolbar import ReadToolbar, ViewToolbar
+from gettext import gettext as _
+
+page=0
+PAGE_SIZE = 45
+TOOLBAR_READ = 2
+
+class ReadEtextsActivity(activity.Activity):
+ def __init__(self, handle):
+ "The entry point to the Activity"
+ global page
+ activity.Activity.__init__(self, handle)
+
+ toolbox = activity.ActivityToolbox(self)
+ activity_toolbar = toolbox.get_activity_toolbar()
+ activity_toolbar.keep.props.visible = False
+ activity_toolbar.share.props.visible = False
+
+ self.edit_toolbar = activity.EditToolbar()
+ self.edit_toolbar.undo.props.visible = False
+ self.edit_toolbar.redo.props.visible = False
+ self.edit_toolbar.separator.props.visible = False
+ self.edit_toolbar.copy.set_sensitive(False)
+ self.edit_toolbar.copy.connect('clicked',
+ self.edit_toolbar_copy_cb)
+ self.edit_toolbar.paste.props.visible = False
+ toolbox.add_toolbar(_('Edit'), self.edit_toolbar)
+ self.edit_toolbar.show()
+
+ self.read_toolbar = ReadToolbar()
+ toolbox.add_toolbar(_('Read'), self.read_toolbar)
+ self.read_toolbar.back.connect('clicked',
+ self.go_back_cb)
+ self.read_toolbar.forward.connect('clicked',
+ self.go_forward_cb)
+ self.read_toolbar.num_page_entry.connect('activate',
+ self.num_page_entry_activate_cb)
+ self.read_toolbar.show()
+
+ self.view_toolbar = ViewToolbar()
+ toolbox.add_toolbar(_('View'), self.view_toolbar)
+ self.view_toolbar.connect('go-fullscreen',
+ self.view_toolbar_go_fullscreen_cb)
+ self.view_toolbar.zoom_in.connect('clicked',
+ self.zoom_in_cb)
+ self.view_toolbar.zoom_out.connect('clicked',
+ self.zoom_out_cb)
+ self.view_toolbar.show()
+
+ self.set_toolbox(toolbox)
+ toolbox.show()
+ self.scrolled_window = gtk.ScrolledWindow()
+ self.scrolled_window.set_policy(gtk.POLICY_NEVER,
+ gtk.POLICY_AUTOMATIC)
+ self.scrolled_window.props.shadow_type = \
+ gtk.SHADOW_NONE
+
+ self.textview = gtk.TextView()
+ self.textview.set_editable(False)
+ self.textview.set_cursor_visible(False)
+ self.textview.set_left_margin(50)
+ self.textview.connect("key_press_event",
+ self.keypress_cb)
+
+ self.scrolled_window.add(self.textview)
+ self.set_canvas(self.scrolled_window)
+ self.textview.show()
+ self.scrolled_window.show()
+ page = 0
+ self.clipboard = gtk.Clipboard(
+ display=gtk.gdk.display_get_default(),
+ selection="CLIPBOARD")
+ self.textview.grab_focus()
+ self.font_desc = pango.FontDescription("sans %d" %
+ style.zoom(10))
+ self.textview.modify_font(self.font_desc)
+
+ buffer = self.textview.get_buffer()
+ self.markset_id = buffer.connect("mark-set",
+ self.mark_set_cb)
+ self.toolbox.set_current_toolbar(TOOLBAR_READ)
+
+ def keypress_cb(self, widget, event):
+ "Respond when the user presses one of the arrow keys"
+ keyname = gtk.gdk.keyval_name(event.keyval)
+ print keyname
+ if keyname == 'plus':
+ self.font_increase()
+ return True
+ if keyname == 'minus':
+ self.font_decrease()
+ return True
+ if keyname == 'Page_Up' :
+ self.page_previous()
+ return True
+ if keyname == 'Page_Down':
+ self.page_next()
+ return True
+ if keyname == 'Up' or keyname == 'KP_Up' \
+ or keyname == 'KP_Left':
+ self.scroll_up()
+ return True
+ if keyname == 'Down' or keyname == 'KP_Down' \
+ or keyname == 'KP_Right':
+ self.scroll_down()
+ return True
+ return False
+
+ def num_page_entry_activate_cb(self, entry):
+ global page
+ if entry.props.text:
+ new_page = int(entry.props.text) - 1
+ else:
+ new_page = 0
+
+ if new_page &gt;= self.read_toolbar.total_pages:
+ new_page = self.read_toolbar.total_pages - 1
+ elif new_page &lt; 0:
+ new_page = 0
+
+ self.read_toolbar.current_page = new_page
+ self.read_toolbar.set_current_page(new_page)
+ self.show_page(new_page)
+ entry.props.text = str(new_page + 1)
+ self.read_toolbar.update_nav_buttons()
+ page = new_page
+
+ def go_back_cb(self, button):
+ self.page_previous()
+
+ def go_forward_cb(self, button):
+ self.page_next()
+
+ def page_previous(self):
+ global page
+ page=page-1
+ if page &lt; 0: page=0
+ self.read_toolbar.set_current_page(page)
+ self.show_page(page)
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ v_adjustment.value = v_adjustment.upper -\
+ v_adjustment.page_size
+
+ def page_next(self):
+ global page
+ page=page+1
+ if page &gt;= len(self.page_index): page=0
+ self.read_toolbar.set_current_page(page)
+ self.show_page(page)
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ v_adjustment.value = v_adjustment.lower
+
+ def zoom_in_cb(self, button):
+ self.font_increase()
+
+ def zoom_out_cb(self, button):
+ self.font_decrease()
+
+ def font_decrease(self):
+ font_size = self.font_desc.get_size() / 1024
+ font_size = font_size - 1
+ if font_size &lt; 1:
+ font_size = 1
+ self.font_desc.set_size(font_size * 1024)
+ self.textview.modify_font(self.font_desc)
+
+ def font_increase(self):
+ font_size = self.font_desc.get_size() / 1024
+ font_size = font_size + 1
+ self.font_desc.set_size(font_size * 1024)
+ self.textview.modify_font(self.font_desc)
+
+ def mark_set_cb(self, textbuffer, iter, textmark):
+
+ if textbuffer.get_has_selection():
+ begin, end = textbuffer.get_selection_bounds()
+ self.edit_toolbar.copy.set_sensitive(True)
+ else:
+ self.edit_toolbar.copy.set_sensitive(False)
+
+ def edit_toolbar_copy_cb(self, button):
+ textbuffer = self.textview.get_buffer()
+ begin, end = textbuffer.get_selection_bounds()
+ copy_text = textbuffer.get_text(begin, end)
+ self.clipboard.set_text(copy_text)
+
+ def view_toolbar_go_fullscreen_cb(self, view_toolbar):
+ self.fullscreen()
+
+ def scroll_down(self):
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ if v_adjustment.value == v_adjustment.upper - \
+ v_adjustment.page_size:
+ self.page_next()
+ return
+ if v_adjustment.value &lt; v_adjustment.upper - \
+ v_adjustment.page_size:
+ new_value = v_adjustment.value + \
+ v_adjustment.step_increment
+ if new_value &gt; v_adjustment.upper - \
+ v_adjustment.page_size:
+ new_value = v_adjustment.upper - \
+ v_adjustment.page_size
+ v_adjustment.value = new_value
+
+ def scroll_up(self):
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ if v_adjustment.value == v_adjustment.lower:
+ self.page_previous()
+ return
+ if v_adjustment.value &gt; v_adjustment.lower:
+ new_value = v_adjustment.value - \
+ v_adjustment.step_increment
+ if new_value &lt; v_adjustment.lower:
+ new_value = v_adjustment.lower
+ v_adjustment.value = new_value
+
+ def show_page(self, page_number):
+ global PAGE_SIZE, current_word
+ position = self.page_index[page_number]
+ self.etext_file.seek(position)
+ linecount = 0
+ label_text = '\n\n\n'
+ textbuffer = self.textview.get_buffer()
+ while linecount &lt; PAGE_SIZE:
+ line = self.etext_file.readline()
+ label_text = label_text + unicode(line,
+ 'iso-8859-1')
+ linecount = linecount + 1
+ label_text = label_text + '\n\n\n'
+ textbuffer.set_text(label_text)
+ self.textview.set_buffer(textbuffer)
+
+ def save_extracted_file(self, zipfile, filename):
+ "Extract the file to a temp directory for viewing"
+ filebytes = zipfile.read(filename)
+ outfn = self.make_new_filename(filename)
+ if (outfn == ''):
+ return False
+ f = open(os.path.join(self.get_activity_root(),
+ 'tmp', outfn), 'w')
+ try:
+ f.write(filebytes)
+ finally:
+ f.close()
+
+ def get_saved_page_number(self):
+ global page
+ title = self.metadata.get('title', '')
+ if title == '' or not title[len(title)- 1].isdigit():
+ page = 0
+ else:
+ i = len(title) - 1
+ newPage = ''
+ while (title[i].isdigit() and i &gt; 0):
+ newPage = title[i] + newPage
+ i = i - 1
+ if title[i] == 'P':
+ page = int(newPage) - 1
+ else:
+ # not a page number; maybe a volume number.
+ page = 0
+
+ def save_page_number(self):
+ global page
+ title = self.metadata.get('title', '')
+ if title == '' or not title[len(title)-1].isdigit():
+ title = title + ' P' + str(page + 1)
+ else:
+ i = len(title) - 1
+ while (title[i].isdigit() and i &gt; 0):
+ i = i - 1
+ if title[i] == 'P':
+ title = title[0:i] + 'P' + str(page + 1)
+ else:
+ title = title + ' P' + str(page + 1)
+ self.metadata['title'] = title
+
+ def read_file(self, filename):
+ "Read the Etext file"
+ global PAGE_SIZE, page
+
+ if zipfile.is_zipfile(filename):
+ self.zf = zipfile.ZipFile(filename, 'r')
+ self.book_files = self.zf.namelist()
+ self.save_extracted_file(self.zf,
+ self.book_files[0])
+ currentFileName = os.path.join(
+ self.get_activity_root(),
+ 'tmp', self.book_files[0])
+ else:
+ currentFileName = filename
+
+ self.etext_file = open(currentFileName,"r")
+ self.page_index = [ 0 ]
+ pagecount = 0
+ linecount = 0
+ while self.etext_file:
+ line = self.etext_file.readline()
+ if not line:
+ break
+ linecount = linecount + 1
+ if linecount &gt;= PAGE_SIZE:
+ position = self.etext_file.tell()
+ self.page_index.append(position)
+ linecount = 0
+ pagecount = pagecount + 1
+ if filename.endswith(".zip"):
+ os.remove(currentFileName)
+ self.get_saved_page_number()
+ self.show_page(page)
+ self.read_toolbar.set_total_pages(pagecount + 1)
+ self.read_toolbar.set_current_page(page)
+
+ def make_new_filename(self, filename):
+ partition_tuple = filename.rpartition('/')
+ return partition_tuple[2]
+
+ def write_file(self, filename):
+ "Save meta data for the file."
+ self.metadata['activity'] = self.get_bundle_id()
+ self.save_page_number()
+</pre>
+<p>This is the <strong>activity.info</strong> for this example:
+</p>
+<pre>[Activity]
+name = Read ETexts II
+service_name = net.flossmanuals.ReadEtextsActivity
+icon = read-etexts
+<strong>exec = sugar-activity ReadEtextsActivity2.ReadEtextsActivity</strong>
+show_launcher = no
+activity_version = 1
+mime_types = text/plain;application/zip
+license = GPLv2+
+
+</pre>
+<p>The line in <strong>bold</strong> is the only one that needs changing.&#160; When we run this new version this is what we'll see:
+</p>
+<p><img alt="ReadEtexts_05.jpg" src="static/ActivitiesGuideSugar-ReadEtexts_05-en.jpg" height="480" width="640"/></p>
+<p>There are a few things worth pointing out in this code.&#160; First, have a look at this import:
+</p>
+<pre>from gettext import gettext as _
+</pre>
+<p> We'll be using the <em>gettext</em> module of Python to support translating our Activity into other languages. We'll be using it in statements like this one:
+</p>
+<pre> self.back.set_tooltip(_('Back'))</pre>
+<p>The underscore acts the same way as the gettext function because of the way we imported gettext.&#160; The effect of this statement will be to look in a special translation file for a word or phrase that matches the key "Back" and replace it with its translation.&#160; If there is no translation file for the language we want then it will simply use the word "Back".&#160; We'll explore setting up these translation files later, but for now using gettext for all of the words and phrases we will show to our Activity users lays some important groundwork.
+</p>
+<p>The second thing worth pointing out is that while our revised Activity has four toolbars we only had to create two of them.&#160; The other two, <strong>Activity</strong> and <strong>Edit</strong>, are part of the Sugar Python library.&#160; We can use those toolbars as is, hide the controls we don't need, or even extend them by adding new controls.&#160; In the example we're hiding the <strong>Keep</strong> and <strong>Share</strong> controls of the Activity toolbar and the <strong>Undo</strong>, <strong>Redo</strong>, and <strong>Paste</strong> buttons of the Edit toolbar.&#160; We currently do not support sharing books or modifying the text in books so these controls are not needed.&#160; Note too that the Activity toolbar is part of the <strong>ActivityToolbox</strong>.&#160; There is no way to give your Activity a toolbox that does not contain the Activity toolbar as its first entry.
+</p>
+<p>Another thing to notice is that the Activity class doesn't just provide us with a window.&#160; The window has a VBox to hold our toolbars and the body of our Activity.&#160; We install the toolbox using <em>set_toolbox()</em> and the body of the Activity using <em>set_canvas()</em>.
+</p>
+<p>The <strong>Read</strong> and <strong>View</strong> toolbars are regular PyGtk programming, but notice that there is a special button for Sugar toolbars that can have a tooltip attached to it, plus the <strong>View</strong> toolbar has code to hide the toolbox and <strong>ReadEtextsActivity2</strong> has code to unhide it.&#160; This is an easy function to add to your own Activities and many games and other kinds of Activities can benefit from the increased screen area you get when you hide the toolbox.
+</p>
+<h2>Metadata And Journal Entries
+ <br/></h2>
+<p>Every Journal entry represents a single file plus <strong>metadata</strong>, or information describing the file.&#160; There are standard metadata entries that all Journal entries have and you can also create your own custom metadata.
+</p>
+<p> Unlike ReadEtextsActivity, this version has a <em>write_file()</em> method.
+</p>
+<pre> def write_file(self, filename):
+ "Save meta data for the file."
+ self.metadata['activity'] = self.get_bundle_id()
+ self.save_page_number()
+</pre>
+<p> We didn't have a <em>write_file()</em> method before because we weren't going to update the file the book is in, and we still aren't.&#160; We will, however, be updating the metadata for the Journal entry.&#160; Specifically, we'll be doing two things:
+</p>
+<ul><li>Save the page number our Activity user stopped reading on so when he launches the Activity again we can return to that page.</li>
+ <li>Tell the Journal entry that it belongs to our Activity, so that in the future it will use our Activity's icon and can launch our Activity with one click.</li>
+</ul><p>The way the <strong>Read</strong> Activity saves page number is to use a custom metadata property.&#160;
+</p>
+<pre> self.metadata['Read_current_page'] = \
+ str(self._document.get_page_cache().get_current_page())
+</pre>
+<p><strong>Read</strong> creates a custom metadata property named <em>Read_current_page </em>to store the current page number.&#160; You can create any number of custom metadata properties just this easily, so you may wonder why we aren't doing that with <strong>Read Etexts</strong>.&#160; Actually, the first version of <strong>Read Etexts</strong> did use a custom property, but in Sugar .82 or lower there was a bug in the Journal such that custom metadata did not survive after the computer was turned off.&#160; As a result my Activity would remember pages numbers while the computer was running, but would forget them as soon as it was shut down.&#160; XO laptops currently cannot upgrade to anything newer than .82, and when it is possible to upgrade it will be a big job for the schools.
+</p>
+<p>To get around this problem I created the following two methods:
+ <br/></p>
+<pre> def get_saved_page_number(self):
+ global page
+ title = self.metadata.get('title', '')
+ if title == '' or not title[len(title)-1].isdigit():
+ page = 0
+ else:
+ i = len(title) - 1
+ newPage = ''
+ while (title[i].isdigit() and i &gt; 0):
+ newPage = title[i] + newPage
+ i = i - 1
+ if title[i] == 'P':
+ page = int(newPage) - 1
+ else:
+ # not a page number; maybe a volume number.
+ page = 0
+
+ def save_page_number(self):
+ global page
+ title = self.metadata.get('title', '')
+ if title == '' or not title[len(title)-1].isdigit():
+ title = title + ' P' + str(page + 1)
+ else:
+ i = len(title) - 1
+ while (title[i].isdigit() and i &gt; 0):
+ i = i - 1
+ if title[i] == 'P':
+ title = title[0:i] + 'P' + str(page + 1)
+ else:
+ title = title + ' P' + str(page + 1)
+ self.metadata['title'] = title
+</pre>
+<p> <em>save_page_number()</em> looks at the current title metadata and either adds a page number to the end of it or updates the page number already there.&#160; Since title is standard metadata for all Journal entries the Journal bug does not affect it.
+</p>
+<p>These examples show how to read metadata too. &#160;
+ <br/></p>
+<pre> title = self.metadata.get('title', '')
+</pre>
+<p> This line of code says "Get the metadata property named <em>title</em> and put it in the variable named <em>title</em>, If there is no title property put an empty string in <em>title</em>.
+</p>
+<p>Generally&#160; you will save metadata in the <em>write_file()</em> method and read it in the <em>read_file()</em> method.
+</p>
+<p>In a normal Activity that writes out a file in write_file() this next line would be unnecessary:
+</p>
+<p>
+</p>
+<pre> self.metadata['activity'] = self.get_bundle_id()
+</pre>
+<p> Any Journal entry created by an Activity will automatically have this property set. In the case of <em>Pride and Prejudice</em>, our Activity did not create it.&#160; We are able to read it because our Activity supports its <em>MIME type</em>.&#160; Unfortunately, that MIME type, <em>application/zip</em>, is used by other Activities.&#160; I found it very frustrating to want to open a book in <strong>Read Etexts</strong> and accidentally have it opened in <strong>EToys</strong> instead.&#160; This line of code solves that problem.&#160; You only need to use <em>Start Using...</em> the first time you read a book.&#160; After that the book will use the <strong>Read Etexts</strong> icon and can be resumed with a single click.
+</p>
+<p>This does not at all affect the MIME type of the Journal entry, so if you wanted to deliberately open <em>Pride and Prejudice</em> with <strong>Etoys</strong> it is still possible.
+</p>
+<p>Before we leave the subject of Journal metadata let's look at all the standard metadata that every Activity has.&#160; Here is some code that creates a new Journal entry and updates a bunch of standard properties:
+</p>
+<pre> def create_journal_entry(self, tempfile):
+ journal_entry = datastore.create()
+ journal_title = self.selected_title
+ if self.selected_volume != '':
+ journal_title += ' ' + _('Volume') + ' ' + \
+ self.selected_volume
+ if self.selected_author != '':
+ journal_title = journal_title + ', by ' + \
+ self.selected_author
+ journal_entry.metadata['title'] = journal_title
+ journal_entry.metadata['title_set_by_user'] = '1'
+ journal_entry.metadata['keep'] = '0'
+ format = \
+ self._books_toolbar.format_combo.props.value
+ if format == '.djvu':
+ journal_entry.metadata['mime_type'] = \
+ 'image/vnd.djvu'
+ if format == '.pdf' or format == '_bw.pdf':
+ journal_entry.metadata['mime_type'] = \
+ 'application/pdf'
+ journal_entry.metadata['buddies'] = ''
+ journal_entry.metadata['preview'] = ''
+ journal_entry.metadata['icon-color'] = \
+ profile.get_color().to_string()
+ textbuffer = self.textview.get_buffer()
+ journal_entry.metadata['description'] = \
+ textbuffer.get_text(textbuffer.get_start_iter(),
+ textbuffer.get_end_iter())
+ journal_entry.file_path = tempfile
+ datastore.write(journal_entry)
+ os.remove(tempfile)
+ self._alert(_('Success'), self.selected_title + \
+ _(' added to Journal.'))
+</pre>
+<p>This code is taken from an Activity I wrote that downloads books from a website and creates Journal entries for them.&#160; The Journal entries contain a friendly title and a full description of the book.
+</p>
+<p>Most Activities will only deal with one Journal entry by using the <em>read_file()</em> and <em>write_file()</em> methods but you are not limited to that.&#160; In a later chapter I'll show you how to create and delete Journal entries, how to list the contents of the Journal, and more.
+ <br/></p>We've covered a lot of technical information in this chapter and there's more to come, but before we get to that we need to look at some other important topics:
+<ul><li>Putting your Activity in version control.&#160; This will enable you to share your code with the world and get other people to help work on it.</li>
+ <li>Getting your Activity translated into other languages.</li>
+ <li>Distributing your finished Activity.&#160; (Or your not quite finished but still useful Activity).
+ <br/></li>
+</ul></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch012_add-your-activity-code-to-version-control.xhtml b/app/static/doc/myosa/ch012_add-your-activity-code-to-version-control.xhtml
new file mode 100644
index 0000000..0f7af14
--- /dev/null
+++ b/app/static/doc/myosa/ch012_add-your-activity-code-to-version-control.xhtml
@@ -0,0 +1,336 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>Add Your Activity Code To Version Control
+</h1>
+<h2>What Is Version Control?
+</h2>
+<p> <em>"If I have seen further it is only by standing on the shoulders of giants."</em>
+</p>
+<p>Isaac Newton, in a letter to Robert Hooke.
+</p>
+<p>Writing an Activity is usually not something you do by yourself.&#160; You will usually have collaborators in one form or another.&#160; When I started writing <strong>Read Etexts</strong> I copied much of the code from the <strong>Read</strong> Activity.&#160; When I implemented text to speech I adapted a toolbar from the <strong>Speak</strong> Activity.&#160; When I finally got my copied file sharing code working the author of <strong>Image Viewer</strong> thought it was good enough to copy into that Activity.&#160; Another programmer saw the work I did for text to speech and thought he could do it better.&#160; He was right, and his improvements got merged into my own code.&#160; When I wrote <strong>Get Internet Archive Books</strong> someone else took the user interface I came up with and made a more powerful and versatile Activity called <strong>Get Books</strong>.&#160; Like Newton, everyone benefits from the work others have done before.
+ <br/></p>
+<p>Even if I wanted to write Activities without help I would still need collaborators to translate them into other languages.
+</p>
+<p>To make collaboration possible you need to have a place where everyone can post their code and share it.&#160; This is called a code repository.&#160; It isn't enough to just share the latest version of your code.&#160; What you really want to do is share <em>every</em> version of your code.&#160; Every time you make a significant change to your code you want to have the new version and the previous version available.&#160; Not only do you want to have every version of your code available, you want to be able to compare any two versions your code to see what changed between them.&#160; This is what version control software does.
+</p>
+<p>The three most popular version control tools are <strong>CVS</strong>, <strong>Subversion</strong>, and <strong>Git</strong>.&#160; Git is the newest and is the one used by Sugar Labs.&#160; While not every Activity has its code in the Sugar Labs Git repository (other free code repositories exist) there is no good reason not to do it and significant benefits if you do.&#160; If you want to get your Activity translated into other languages using the Sugar Labs Git repository is a must. &#160;
+</p>
+<div class="objavi-forcebreak">
+</div>
+<h2>Git Along Little Dogies
+</h2>
+<p>Git is a <strong>distributed</strong> version control system.&#160; This means that not only are there copies of every version of your code in a central repository, the same copies exist on every user's computer.&#160; This means you can update your local repository while you are not connected to the Internet, then connect and share everything at one time.
+</p>
+<p> There are two ways you will interact with your Git repository: through Git commands and through the website at <a href="http://git.sugarlabs.org/.">http://git.sugarlabs.org/.</a>&#160;&#160; We'll look at this website first.
+</p>
+<p>Go to <a href="http://git.sugarlabs.org">http://git.sugarlabs.org/</a>&#160; and click on the <strong>Projects</strong> link in the upper right corner:
+</p>
+<p><img alt="git1.jpg" src="static/ActivitiesGuideSugar-git1-en.jpg" width="473" height="194"/></p>
+<p>You will see a list of projects in the repository.&#160; They will be listed from newest to oldest.&#160; You'll also see a <strong>New Project</strong> link but you'll need to create an account to use that and we aren't ready to do that yet.
+ <br/></p>
+<p><img alt="git2.jpg" src="static/ActivitiesGuideSugar-git2-en.jpg" width="535" height="453"/></p>
+<p>If you use the <strong>Search</strong> link in the upper right corner of the page you'll get a search form.&#160; Use it to search for "read etexts".&#160; Click on the link for that project when you find it.&#160; You should see something like this:
+ <br/></p>
+<p><img alt="git3.jpg" src="static/ActivitiesGuideSugar-git3-en.jpg" width="571" height="571"/></p>
+<p>This page lists <em>some</em> of the activity for the project but I don't find it particularly useful.&#160; To get a much better look at your project start by clicking on the repository name on the right side of the page.&#160; In this case the repository is named <strong>mainline</strong>.
+</p>
+<p><img alt="git4.jpg" src="static/ActivitiesGuideSugar-git4-en.jpg" width="399" height="236"/></p>
+<p>You'll see something like this at the top of the page:
+</p>
+<p><img alt="git5.jpg" src="static/ActivitiesGuideSugar-git5-en.jpg" width="574" height="509"/></p>
+<p>This page has some useful information on it.&#160; First, have a look at the <strong>Public clone url</strong> and the <strong>HTTP clone url</strong>.&#160; You need to click on <strong>More info...</strong> to see either one.&#160; If you run either of these commands from the console you will get a copy of the git repository for the project copied to your computer.&#160; This copy will include every version of every piece of code in the project.&#160; You would need to modify it a bit before you could share your changes back to the main repository, but everything would be there.
+</p>
+<p>The list under <strong>Activities</strong> is not that useful, but if you click on the <strong>Source Tree</strong> link you'll see something really good:
+</p>
+<p><img alt="git6.jpg" src="static/ActivitiesGuideSugar-git6-en.jpg" width="571" height="468"/></p>
+<p>Here is a list of every file in the project, the date it was last updated, and a comment on what was modified.&#160; Click on the link for <strong>ReadEtextsActivity.py</strong> and you'll see this:
+</p>
+<p><img alt="git7.jpg" src="static/ActivitiesGuideSugar-git7-en.jpg" width="597" height="642"/></p>
+<p>This is the latest code in that file in pretty print format.&#160; Python keywords are shown in a different color, there are line numbers, etc.&#160; This is a good page for looking at code on the screen, but it doesn't print well and it's not much good for copying snippets of code into Eric windows either.&#160; For either of those things you'll want to click on <strong>raw blob data</strong> at the top of the listing:
+</p>
+<p><img alt="git8.jpg" src="static/ActivitiesGuideSugar-git8-en.jpg" width="549" height="484"/></p>
+<p>We're not done yet.&#160; Use the <strong>Back</strong> button to get back to the pretty print listing and click on the <strong>Commits</strong> link.&#160; This will give us a list of everything that changed each time we committed code into Git:
+</p>
+<p><img alt="git9.jpg" src="static/ActivitiesGuideSugar-git9-en.jpg" width="636" height="540"/></p>
+<p>You may have noticed the odd combination of letters and numbers after the words <strong>James Simmons committed</strong>.&#160; This is a kind of version number.&#160; The usual practice with version control systems is to give each version of code you check in a version number, usually a simple sequence number.&#160; Git is distributed, with many separate copies of the repository being modified independently and then merged.&#160; That makes using just a sequential number to identify versions unworkable.&#160; Instead, Git gives each version a really, really large random number.&#160; The number is expressed in base 16, which uses the symbols 0-9 and a-f.&#160; What you see in green is only a small part of the complete number.&#160; The number is a link, and if you click on it you'll see this:
+</p>
+<p><img alt="git10.jpg" src="static/ActivitiesGuideSugar-git10-en.jpg" width="561" height="607"/></p>
+<p>At the top of the page we see the complete version number used for this commit.&#160; Below the gray box we see the full comment that was used to commit the changes.&#160; Below that is a listing of what files were changed.&#160;&#160; If we look further down the page we see this:
+</p>
+<p><img alt="git11_1.jpg" src="static/ActivitiesGuideSugar-git11_1-en.jpg" width="581" height="530"/></p>
+<p>This is a <em>diff</em> report which shows the lines that have changed between this version and the previous version.&#160; For each change it shows a few lines before and after the change to give you a better idea of what the change does.&#160; Every change shows line numbers too.
+</p>
+<p>A report like this is a wonderful aid to programming.&#160; Sometimes when you're working on an enhancement to your program something that had been working mysteriously stops working.&#160; When that happens you will wonder just what you changed that could have caused the problem.&#160; A diff report can help you find the source of the problem.
+</p>
+<p>By now you must be convinced that you want your project code in Git.&#160; Before we can do that we need to create an account on this website.&#160; That is no more difficult than creating an account on any other website, but it will need an important piece of information from us that we don't have yet.&#160; Getting that information is our next task.
+</p>
+<div class="objavi-forcebreak">
+</div>
+<h2>Setting Up SSH Keys
+</h2>
+<p>To send your code to the <strong>Gitorious</strong> code repository you need an SSH public/private key pair.&#160;&#8286; SSH is a way of sending data over the network in encrypted format.&#160; (In other words, it uses a secret code so nobody but the person getting the data can read it).&#160; Public/private key encryption is a way of encrypting data that provides a way to guarantee that the person who is sending you the data is who he claims to be.
+</p>
+<p>In simple terms it works like this: the SSH software generates two very large numbers that are used to encode and decode the data going over the network.&#160; The first number, called the<strong> private key</strong>, is kept secret and is only used by you to encode the data.&#160; The second number, called the <strong>public key</strong>, is given to anyone who needs to decode your data.&#160; He can decode it using the public key; there is no need for him to know the private key.&#160; He can also use the public key to encode a message to send back to you and you can decode it using your private key.
+</p>
+<p>Git uses SSH like an electronic signature to verify that code changes that are supposed to be coming from you actually are coming from you.&#160; The Git repository is given your public key.&#160; It knows that anything it decodes with that key must have been sent by you because only you have the private key needed to encode it.
+</p>
+<p> We will be using a tool called <strong>OpenSSH</strong> to generate the public and private keys.&#160; This is included with every version of Linux so you just need to verify that it has been installed.&#160; Then use the <strong>ssh-keygen</strong> utility that comes with OpenSSH to generate the keys:
+ <br/></p>
+<pre>[jim@olpc2 ~]$ ssh-keygen
+Generating public/private rsa key pair.
+Enter file in which to save the key (/home/jim/.ssh/id_rsa): </pre>
+<p>By default ssh-keygen generates an <strong>RSA</strong> key, which is the kind we want.&#160; By default it puts the keyfiles in a directory called <strong>/<em>yourhome</em>/.ssh</strong> and we want that too, so DO NOT enter a filename when it asks you to.&#160; Just hit the <strong>Enter</strong> key to continue.
+</p>
+<pre>[jim@olpc2 ~]$ ssh-keygen
+Generating public/private rsa key pair.
+Enter file in which to save the key (/home/jim/.ssh/id_rsa):
+Created directory '/home/jim/.ssh'.
+Enter passphrase (empty for no passphrase):
+</pre>
+<p>Now we DO want a <strong>passphrase</strong> here.&#160; A passphrase is like a password that is used with the public and private keys to do the encrypting.&#160; When you type it in you will not be able to see what you typed.&#160; Because of that it will ask you to type the same thing again, and it will check to see that you typed them in the same way both times.
+</p>
+<pre>[jim@olpc2 ~]$ ssh-keygen
+Generating public/private rsa key pair.
+Enter file in which to save the key (/home/jim/.ssh/id_rsa):
+Created directory '/home/jim/.ssh'.
+Enter passphrase (empty for no passphrase):
+Enter same passphrase again:
+Your identification has been saved in /home/jim/.ssh/id_rsa.
+Your public key has been saved in /home/jim/.ssh/id_rsa.pub.
+The key fingerprint is:
+d0:fe:c0:0c:1e:72:56:7a:19:cd:f3:85:c7:4c:9e:18
+jim@olpc2.simmons
+The key's randomart image is:
++--[ RSA 2048]----+
+| oo E=. |
+| + o+ .+=. |
+| . B + o.oo |
+| = O . |
+| . S |
+| o |
+| . |
+| |
+| |
++-----------------+
+</pre>
+<p> When choosing a passphrase remember that it needs to be something you can type reliably without seeing it and it would be better if it was <em>not</em> a word you can find in the dictionary, because those are easily broken. When I need to make a password I use the tool at <a href="http://www.multicians.org/thvv/gpw.html.">http://www.multicians.org/thvv/gpw.html.</a>&#160; This tool generates a bunch of nonsense words that are pronounceable.&#160; Pick one that appeals to you and use that.
+</p>
+<p>Now have a look inside the .ssh directory.&#160; By convention every file or directory name that begins with a period is considered hidden by Linux, so it won't show up in a GNOME file browser window unless you use the option on the View menu to Show Hidden Files.&#160; When you display the contents of that directory you'll see two files: <strong>id_rsa</strong> and <strong>id_rsa.pub</strong>.&#160; The public key is in id_rsa.pub.&#160; Try opening that file with gedit (Open With Text Editor) and you'll see something like this:
+</p>
+<p><img alt="git12.jpg" src="static/ActivitiesGuideSugar-git12-en.jpg" width="640" height="278"/></p>
+<p>When you create your account on <a href="http://git.sugarlabs.org">git.sugarlabs.org</a> there will be a place where you can add your public SSH key.&#160; To do that use <strong>Select All</strong> from the <strong>Edit</strong> menu in gedit, then <strong>Copy</strong> and <strong>Paste</strong> into the field provided on the web form.
+</p>
+<h2>Create A New Project
+</h2>
+<p>I'm going to create a new Project in Git for the examples for this book.&#160; I need to log in with my new account and click the <strong>New Project</strong> link we saw earlier.&#160; I get this form, which I have started filling in:
+</p>
+<p><img alt="git13.jpg" src="static/ActivitiesGuideSugar-git13-en.jpg" width="398" height="343"/></p>
+<p>The <strong>Title</strong> is used on the website, the <strong>Slug</strong> is a shortened version of the title without spaces used to name the Git repository.&#160; <strong>Categories</strong> are optional.&#160; <strong>License</strong> is GPL v2 for my projects.&#160; You can choose from any of the licenses in the list for your own Projects, and you can change the license entry later if you want to.&#160; You will also need to enter a <strong>Description</strong> for your project.
+</p>
+<p>Once you have this set up you'll be able to click on the mainline entry for the Project (like we did with Read Etexts before) and see something like this:
+</p>
+<p><img alt="git14.jpg" src="static/ActivitiesGuideSugar-git14-en.jpg" width="640" height="454"/></p>
+<p>The next step is to convert our project files into a local Git repository, add the files to it, then push it to the repository on <a href="http://git.sugarlabs.org">git.sugarlabs.org.</a>&#160; We need to do this because you cannot <strong>clone</strong> an empty repository, and our remote repository is currently empty.&#160; To get around that problem we'll push the local repository out to the new remote repository we just created, then clone the remote one and delete our existing project and its Git repository.&#160; From then on we'll do all our work in the cloned repository.
+ <br/></p>
+<p>This process may remind you of the Edward Albee quote, "<span class="body">Sometimes a person has to go a very long distance out of his way to come back a short distance correctly".</span> Fortunately we only need to do it once per project.&#160; Enter the commands shown below in <strong>bold</strong> after making you project directory the current one:
+ <br/></p>
+<pre><strong>git init</strong>
+Initialized empty Git repository in
+/home/jim/olpc/bookexamples/.git/
+<strong>git add *.py</strong>
+<strong>git add activity
+git add MANIFEST</strong>
+<strong>git add .gitignore
+git commit -a -m "Create repository and load"</strong>
+[master (root-commit) 727bfe8] Create repository and load
+ 9 files changed, 922 insertions(+), 0 deletions(-)
+ create mode 100644 .gitignore
+ create mode 100644 MANIFEST
+ create mode 100755 ReadEtexts.py
+ create mode 100644 ReadEtextsActivity.py
+ create mode 100644 ReadEtextsActivity2.py
+ create mode 100644 activity/activity.info
+ create mode 100644 activity/read-etexts.svg
+ create mode 100755 setup.py
+ create mode 100644 toolbar.py
+</pre>
+<p> I have made an empty local Git repository with <strong>git init</strong>, then I've used <strong>git add</strong> to add the important files to it.&#160; (In fact <strong>git add</strong> doesn't actually add anything itself; it just tells Git to add the file on the next <strong>git commit</strong>).&#160; Finally <strong>git commit</strong> with the options shown will actually put the latest version of these files in my new local repository.
+</p>
+<p>To push this local repository to <a href="http://git.sugarlabs.org">git.sugarlabs.org</a>&#160; we use the commands from the web page:
+</p>
+<pre><strong>git remote add origin \
+gitorious@git.sugarlabs.org:\
+myo-sugar-activities-examples/mainline.git
+git push origin master</strong>
+Counting objects: 17, done.
+Compressing objects: 100% (14/14), done.
+Writing objects: 100% (15/15), 7.51 KiB, done.
+Total 15 (delta 3), reused 0 (delta 0)
+To gitorious@git.sugarlabs.org:myo-sugar-activities-examples/
+mainline.git
+ 2cb3a1e..700789d master -&gt; master
+=&gt; Syncing Gitorious...
+Heads up: head of changed to
+700789d3333a7257999d0a69bdcafb840e6adc09 on master
+Notify cia.vc of 727bfe819d5b7b70f4f2b31d02f5562709284ac4 on
+myo-sugar-activities-examples
+Notify cia.vc of 700789d3333a7257999d0a69bdcafb840e6adc09 on
+myo-sugar-activities-examples
+[OK]
+<strong>rm *</strong>
+<strong>rm activity -rf
+rm .git -rf
+cd ~
+rm Activity/ReadEtextsII</strong>
+<strong>mkdir olpc</strong>
+<strong>cd olpc</strong>
+<strong>mkdir bookexamples</strong>
+<strong>cd bookexamples
+git clone \
+git://git.sugarlabs.org/\
+myo-sugar-activities-examples/mainline.git</strong>
+Initialized empty Git repository in
+/home/jim/olpc/bookexamples/mainline/.git/
+remote: Counting objects: 18, done.
+remote: Compressing objects: 100% (16/16), done.
+remote: Total 18 (delta 3), reused 0 (delta 0)
+Receiving objects: 100% (18/18), 8.53 KiB, done.
+Resolving deltas: 100% (3/3), done.
+</pre>
+<p> The lines in <strong>bold</strong> are the commands to enter, and everything else is messages that Git sends to the console.&#160; I've split some of the longer Git commands with the backslash (\) to make them fit better on the printed page, and wrapped some output lines that would normally print on one line for the same reason.&#160; It probably isn't clear what we're doing here and why, so let's take it step by step:
+</p>
+<ul><li>The first command <strong>git remote add origin</strong> tells the remote Git repository that we are going to send it stuff from our local repository.
+ <br/></li>
+ <li>The second command <strong>git push origin master</strong> actually sends your local Git repository to the remote one and its contents will be copied in.&#160; When you enter this command you will be asked to enter the SSH pass phrase you created in the last section.&#160; GNOME will remember this phrase for you and enter it for every Git command afterwards so you don't need to.&#160; It will keep doing this until you log out or turn off the computer.
+ <br/></li>
+ <li>The next step is to delete our existing files and our local Git repository (which is contained in the hidden directory .git).&#160; The <strong>rm .git -rf</strong> means "Delete the directory .git and everything in it".&#160; <strong>rm</strong> is a Unix command, not part of Git.&#160; If you like you can delete your existing files <em>after</em> you create the cloned repository in the next step.&#160; Note the command <strong>rm Activity/ReadEtextsII</strong>, which deletes the symbolic link to our old project that we created by running <strong>./setup.py dev</strong>.&#160; We'll need to go to our new cloned project directory and run that again before we can test our Activity again.
+ <br/></li>
+ <li>Now we do the <strong>git clone</strong> command from the web page.&#160; This takes the remote Git repository we just added our MANIFEST file to and makes a new local repository in directory <strong>/<em>yourhome</em>/olpc/bookexamples/mainline.</strong></li>
+</ul><p>Finally we have a local repository we can use.&#160; Well, not quite.&#160; We can commit our code to it but we cannot push anything back to the remote repository because our local repository isn't configured correctly yet.
+</p>
+<p>What we need to do is edit the file <strong>config</strong> in directory <strong>.git</strong> in <strong>/<em>yourhome</em>/olpc/bookexamples/mainline.&#160; </strong>We can use gedit to do that.&#160; We need to change the <strong>url=</strong> entry to point to the <strong>Push url</strong> shown on the mainline web page.&#160; When we're done our <strong>config</strong> file should look like this:
+</p>
+<pre>[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = false
+ logallrefupdates = true
+[remote "origin"]
+<strong> url = gitorious@git.sugarlabs.org:
+myo-sugar-activities-examples/mainline.git</strong>
+ fetch = +refs/heads/*:refs/remotes/origin/*
+[branch "master"]
+ remote = origin
+ merge = refs/heads/master
+</pre>
+<p> The line in <strong>bold</strong> is the only one that gets changed.&#160; It is split here to make it fit on the printed page.&#160; In your own files it should all be one line with no spaces between the colon(:) that ends the first line and the beginning of the second line.
+ <br/></p>
+<p>From now on anyone who wants to work on our project can get a local copy of the Git repository by doing this from within the directory where he wants the repository to go:
+</p>
+<p>
+</p>
+<pre><strong>git clone git://git.sugarlabs.org/\
+myo-sugar-activities-examples/mainline.git</strong></pre>
+<p> He'll have to change his <strong>.git/config</strong> file just like we did, then he'll be ready to go.
+</p>
+<h2>Everyday Use Of Git
+</h2>
+<p> While getting the repositories set up to begin with is a chore, daily use is not.&#160; There are only a few commands you'll need to work with.&#160; When we left off we had a repository in <strong>/<em>yourhome</em>/olpc/bookexamples/mainline</strong> with our files in it.&#160; We will need to add any new files we create too.
+ <br/></p>
+<p>We use the <strong>git add</strong> command to tell Git that we want to use Git to store a particular file.&#160; This doesn't actually store anything, it just tells Git our intentions.&#160; The format of the command is simply:
+</p>
+<pre>git add <em>file_or_directory_name</em></pre>
+<p>There are files we <em>don't</em> want to add to Git, to begin with those files that end in <strong>.pyc</strong>.&#160; If we never do a <strong>git add</strong> on them they'll never get added, but Git will constantly ask us why we aren't adding them.&#160; Fortunately there is a way to tell Git that we really, really don't want to add those files.&#160; We need to create a file named <strong>.gitignore</strong> using gedit and put in entries like this:
+</p>
+<pre>*.pyc
+*.e4p
+*.zip
+.eric4project/
+.ropeproject/</pre>
+<p>These entries will also ignore project files used by Eric and zip files containing ebooks,&#160; Once we have this file created in the mainline directory we can add it to the repository:
+</p>
+<pre>git add .gitignore
+git commit -a -m "Add .gitignore file"
+</pre>
+<p>From now on Git will no longer ask us to add .pyc or other unwanted&#160; files that match our patterns. If there are other files we don't want in the repository we can add them to .gitignore either as full file names or directory names or as patterns like *.pyc.
+</p>
+<p>&#160;In addition to adding files to Git we can remove them too:
+</p>
+<pre>git rm <em>filename</em></pre>
+<p>Note that this just tells Git that from now on it will not be keeping track of a given filename, and that will take effect at the next commit.&#160; Old versions of the file are still in the repository.
+</p>
+<p>If you want to see what&#160;changes will be applied at the next commit run this:
+</p>
+<pre><strong>git status</strong>
+# On branch master
+# Changed but not updated:
+# (use "git add &lt;file&gt;..." to update what will
+# be committed)
+#
+# modified: ReadEtextsActivity.py
+#
+no changes added to commit (use "git add" and/or
+"git commit -a")
+</pre>
+<p>Finally, to put your latest changes in the repository use this:
+</p>
+<pre><strong>git commit -a -m "Change use of instance directory to tmp"</strong>
+Created commit a687b27: Change use of instance
+directory to tmp
+ 1 files changed, 2 insertions(+), 2 deletions(-)
+</pre>
+<p>If you leave off the -m an editor will open up and you can type in a comment, then save and exit. Unfortunately by default the editor is <strong>vi</strong>, an old text mode editor that is not friendly like gedit.
+</p>
+<p>When we have all our changes done we can send them to the central repository using <strong>git push</strong>:
+</p>
+<pre><strong>git push</strong>
+Counting objects: 5, done.
+Compressing objects: 100% (3/3), done.
+Writing objects: 100% (3/3), 322 bytes, done.
+Total 3 (delta 2), reused 0 (delta 0)
+To gitorious@git.sugarlabs.org:
+myo-sugar-activities-examples/mainline.git
+ 700789d..a687b27 master -&gt; master
+=&gt; Syncing Gitorious...
+Heads up: head of changed to
+a687b27e2f034e5a17d2ca2fe9f2787c7f633e64 on master
+Notify cia.vc of a687b27e2f034e5a17d2ca2fe9f2787c7f633e64
+on myo-sugar-activities-examples
+[OK]
+
+</pre>
+<p>We can get the latest changes from other developers by doing <strong>git pull</strong>:
+</p>
+<pre><strong>git pull</strong>
+remote: Counting objects: 17, done.
+remote: Compressing objects: 100% (14/14), done.
+remote: Total 15 (delta 3), reused 0 (delta 0)
+Unpacking objects: 100% (15/15), done.
+From gitorious@git.sugarlabs.org:
+myo-sugar-activities-examples/mainline
+ 2cb3a1e..700789d master -&gt; origin/master
+Updating 2cb3a1e..700789d
+Fast forward
+ .gitignore | 6 +
+ MANIFEST | 244 +--------------------------
+---------
+ ReadEtexts.py | 182 +++++++++++++++++++++++++++
+ ReadEtextsActivity.py | 182 +++++++++++++++++++++++++++
+ ReadEtextsActivity2.py | 311 +++++++++++++++++++++++++++
++++++++++++++++++++
+ activity/activity.info | 9 ++
+ activity/read-etexts.svg | 71 +++++++++++
+ setup.py | 21 +++
+ toolbar.py | 136 ++++++++++++++++++++
+ 9 files changed, 921 insertions(+), 241 deletions(-)
+ create mode 100644 .gitignore
+ create mode 100755 ReadEtexts.py
+ create mode 100644 ReadEtextsActivity.py
+ create mode 100644 ReadEtextsActivity2.py
+ create mode 100644 activity/activity.info
+ create mode 100644 activity/read-etexts.svg
+ create mode 100755 setup.py
+ create mode 100644 toolbar.py
+&#160;</pre></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch013_going-international-with-pootle.xhtml b/app/static/doc/myosa/ch013_going-international-with-pootle.xhtml
new file mode 100644
index 0000000..46c8b47
--- /dev/null
+++ b/app/static/doc/myosa/ch013_going-international-with-pootle.xhtml
@@ -0,0 +1,137 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>Going International With Pootle
+</h1>
+<h2>Introduction
+ <br/></h2>
+<p>The goal of Sugar Labs and One Laptop Per Child is to educate all the children of the world, and we can't do that with Activities that are only available in one language.&#160; It is equally true that making separate versions of each Activity for every language is not going to work, and expecting Activity developers to be fluent in many languages is not realistic either.&#160; We need a way for Activity developers to be able to concentrate on creating Activities and for those who can translate to just do that.&#160; Fortunately, this is possible and the way it's done is by using <em>gettext</em>.
+</p>
+<h2>Getting Text With gettext
+</h2>
+<p>You should remember that our latest code example made use of an odd import:
+</p>
+<pre>from gettext import gettext as _
+</pre>
+<p> The "_()" function was used in statements like this:
+</p>
+<pre> self.back.set_tooltip(_('Back'))
+</pre>
+<p> At the time I explained that this odd looking function was used to translate the word "Back" into other languages, so that when someone looks at the Back button's tool tip he'll see the text in his own language.&#160; I also said that if it was not possible to translate this text the user would see the word "Back" untranslated.&#160; In this chapter we'll learn more about how this works and what we have to do to support the volunteers who translate these text strings into other languages.
+</p>
+<p>The first thing you need to learn is how to properly format the text strings to be translated.&#160; This is an issue when the text strings are actual sentences containing information.&#160; For example, you might write such a message this way:
+</p>
+<pre> message = _("User ") + username + \
+ _(" has joined the chat room.")
+</pre>
+<p>This would work, but you've made things difficult for the translator. &#160; He has two separate strings to translate and no clue that they belong together.&#160; It is much better to do this:
+</p>
+<pre> message = _("User %s has joined the chat room.") % \
+ username
+</pre>
+<p>If you know both statements give the same resulting string then you can easily see why a translator would prefer the second one.&#160; Use this technique whenever you need a message that has some information inserted into it.&#160; When you use it, try and limit yourself to only one format code (the %s) per string.&#160; If you use more than one it can cause problems for the translator.
+ <br/></p>
+<h2>Going To Pot
+</h2>
+<p>Assuming that every string of text a user might be shown by our Activity is passed through "_()" the next step is to generate a pot file.&#160; You can do this by running setup.py with a special option:
+</p>
+<pre>./setup.py genpot</pre>
+<p>This creates a directory called <strong>po</strong> and puts a file <strong><em>ActivityName</em>.pot</strong> in that directory.&#160; In the case of our example project <em>ActivityName</em> is <strong>ReadEtextsII</strong>.&#160; This is the contents of that file:
+</p>
+<pre># SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the
+# PACKAGE package.
+# FIRST AUTHOR &lt;EMAIL@ADDRESS&gt;, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-01-06 18:31-0600\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME &lt;EMAIL@ADDRESS&gt;\n"
+"Language-Team: LANGUAGE &lt;LL@li.org&gt;\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: activity/activity.info:2
+msgid "Read ETexts II"
+msgstr ""
+
+#: toolbar.py:34
+msgid "Back"
+msgstr ""
+
+#: toolbar.py:40
+msgid "Forward"
+msgstr ""
+
+#: toolbar.py:115
+msgid "Zoom out"
+msgstr ""
+
+#: toolbar.py:120
+msgid "Zoom in"
+msgstr ""
+
+#: toolbar.py:130
+msgid "Fullscreen"
+msgstr ""
+
+#: ReadEtextsActivity2.py:34
+msgid "Edit"
+msgstr ""
+
+#: ReadEtextsActivity2.py:38
+msgid "Read"
+msgstr ""
+
+#: ReadEtextsActivity2.py:46
+msgid "View"
+msgstr ""
+</pre>
+<p> This file contains an entry for every text string in our Activity (as msgid) and a place to put a translation of that string (msgstr).&#160;&#160; Copies of this file will be made by the Pootle server for every language desired, and the msgstr entries will be filled in by volunteer translators.
+</p>
+<h2>Going To Pootle
+ <br/></h2>
+<p>Before any of that can happen we need to get our POT file into Pootle.&#160; The first thing we need to do is get the new directory into our Git repository and push it out to Gitorious.&#160; You should be familiar with the needed commands by now:
+</p>
+<pre>git add po
+git commit -a -m "Add POT file"
+git push
+</pre>
+<p>Next we need to give the user "pootle" commit authority to our Git project.&#160; Go to <a href="http://git.sugarlabs.org">git.sugarlabs.org,</a>&#160; sign in, and find your Project page and click on the mainline link.&#160; You should see this on the page that takes you to:
+</p>
+<p>&#160;<img alt="Add pootle as a committer" src="static/ActivitiesGuideSugar-pootle1-en.jpg" height="456" width="376"/></p>
+<p>Click on the <strong>Add committer</strong> link and type in the name <strong>pootle </strong>in the form that takes you to.&#160; When you come back to this page <strong>pootle</strong> will be listed under Committers.
+ <br/></p>
+<p> Your next step is to go to web site <a href="http://bugs.sugarlabs.org">http://bugs.sugarlabs.org</a>&#160; and register for a user id.&#160; When you get that open up a ticket something like this:
+</p>
+<p><img alt="pootle2.jpg" src="static/ActivitiesGuideSugar-pootle2-en.jpg" height="527" width="640"/></p>
+<p>The <strong>Component</strong> entry <em>localization</em> should be used, along with <strong>Type</strong> <em>task</em>.
+</p>
+<p>Believe it or not, this is all you need to do to get your Activity set up to be translated.
+</p>
+<h2>Pay No Attention To That Man Behind The Curtain
+</h2>
+<p>After this you'll need to do a few things to get translations from Pootle into your Activity.
+</p>
+<ul><li>When you add text strings (labels, error messages, etc.) to your Activity always use the <strong>_()</strong> function with them so they can be translated.</li>
+ <li>After adding new strings always run <strong>./setup.py genpot</strong> to recreate the POT file.</li>
+ <li>After that commit and push your changes to Gitorious.</li>
+ <li>Every so often, and especially before releasing a new version, do a <strong>git pull</strong>.&#160; If there are any localization files added to Gitorious this will bring them to you.</li>
+ <li>After getting a bunch of new files run <strong>./setup.py fix_manifest</strong> to get the new files included in your <strong>MANIFEST</strong> file.&#160; Afterwards edit the <strong>MANIFEST</strong> with gedit to remove any unwanted entries (which will be Eric project files, etc.).
+ <br/></li>
+</ul><p>Localization with Pootle will create a large number of files in your project, some in the <strong>po</strong> directory and others in a new directory called <strong>locale</strong>.&#160; As long as these are listed in the MANIFEST they will be included in the .xo file that you will use to distribute your Activity.
+</p>
+<h2>C'est Magnifique!
+</h2>
+<p>Here is a screen shot of the French language version of <strong>Read Etexts</strong> reading Jules Verne's novel <em>Le tour du monde en quatre-vingts jours</em>:
+</p>
+<p><img alt="Jules Verne in French" src="static/ActivitiesGuideSugar-pootle3-en.jpg" height="480" width="640"/>&#160;
+</p>
+<p>There is reason to believe that the book is in French too.
+ <br/></p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch014_distribute-your-activity.xhtml b/app/static/doc/myosa/ch014_distribute-your-activity.xhtml
new file mode 100644
index 0000000..01ad352
--- /dev/null
+++ b/app/static/doc/myosa/ch014_distribute-your-activity.xhtml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>Distribute Your Activity
+</h1>
+<h2>Choose A License
+</h2>
+<p>Before you give your Activity to anyone you need to choose a license that it will be distributed under.&#160; Buying software is like buying a book.&#160; There are certain rights you have with a book and others you don't have.&#160; If you buy a copy of <em>The DaVinci Code</em> you have the right to read it, to loan it out, to sell it to a used bookstore, or to burn it.&#160; You do <em>not</em> have the right to make copies of it or to make a movie out of it.&#160; Software is the same way, but often worse.&#160; Those long license agreements we routinely accept by clicking a button might not allow you to sell the software when you're done with it, or even give it away.&#160; If you sell your computer you may find that the software you bought is only good for that computer, and only while you are the owner of the computer.&#160; (You can get good deals on reconditioned computers with no operating system installed for that very reason).
+</p>
+<p>If you are in the business of selling software you might have to hire a lawyer to draw up a license agreement, but if you're giving away software there are several standard licenses you can choose from for free.&#160; The most popular by far is called the <em>General Public License</em>, or GPL.&#160; Like the licenses Microsoft uses it allows the people who get your program to do some things with it but not others.&#160; What makes it interesting is not what it allows them to do (which is pretty much anything they like) but what it forbids them to do.
+</p>
+<p>If someone distributes a program licensed under the GPL they are also required to make the source code of the program available to anyone who wants it.&#160; That person may do as he likes with the code, with one important restriction: if he distributes a program based on that code he must <em>also</em> license that code using the GPL.&#160; This makes it impossible for someone to take a GPL licensed work, improve it, and sell it to someone without giving him the source code to the new version.
+</p>
+<p>While the GPL is not the only license available for Activities to be distributed on <a href="http://activities.sugarlabs.org">http://activities.sugarlabs.org</a>&#160; all the licenses require that anyone getting the Activity also gets the complete source code for it.&#160; You've already taken care of that requirement by putting your source code in Gitorious.&#160; If you used any code from an existing Activity licensed with the GPL you <em>must</em> license your own code the same way.&#160; If you used a significant amount of code from this book (which is also GPL licensed) you may be required to use the GPL too.
+</p>
+<p>Is licensing something you should worry about?&#160; Not really.&#160; The only reason you'd want to use a license other than the GPL is if you wanted to sell your Activity instead of give it away.&#160; Consider what you'd have to do to make that possible:
+</p>
+<ul><li>You'd have to use some language other than Python so you could give someone the program without giving them the source code.</li>
+ <li>You would have to have your own source code repository not available to the general public and make arrangements to have the data backed up regularly.</li>
+ <li>You would have to have your own website to distribute the Activity.&#160; The website would have to be set up to accept payments somehow.
+ <br/></li>
+ <li>You would have to advertise this website somehow or nobody would know your Activity existed.</li>
+ <li>You would have to have a lawyer draw up a license for your Activity.</li>
+ <li>You would have to come up with some mechanism to keep your customers from giving away copies of your Activity.</li>
+ <li>You would have to create an Activity so astoundingly clever that nobody else could make something similar and give it away.</li>
+ <li>You would have to deal with the fact that your "customers" would be children with no money or credit cards.</li>
+</ul><p>In summary, <a href="http://activities.sugarlabs.org">activities.sugarlabs.org</a>&#160; is not the <em>iPhone App Store</em>.&#160; It is a place where programmers share and build upon each other's work and give the results to children for free.&#160; The GPL encourages that to happen, and I recommend that you choose that for your license.
+ <br/></p>
+<h2>Add License Comments To Your Python Code
+</h2>
+<p>At the top of each Python source file in your project (except <strong>setup.py</strong>, which is already commented) put comments like this:
+</p>
+<p>&#160;
+</p>
+<pre># <em>filename</em> <em>Program description</em>
+#
+# Copyright (C) 2010 <em>Your Name Here</em>
+#
+# 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 2 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.
+#
+# You should have received a copy of the GNU General
+# Public License along with this program; if not, write
+# to the Free Software Foundation, Inc., 51 Franklin
+# St, Fifth Floor, Boston, MA 02110-1301 USA
+</pre>
+<p> If the code is based on someone else's code you should mention that as a courtesy.
+</p>
+<h2>Create An .xo File
+</h2>
+<p>Make certain that activity.info has the version number you want to give your Activity (currently it must be a positive integer) and run this command:
+</p>
+<pre>./setup.py dist_xo</pre>
+<p> This will create a <strong>dist</strong> directory if one does not exist and put a file named something like <strong>ReadETextsII-1.xo</strong> in it.&#160; The "1" indicates version 1 of the Activity.
+ <br/></p>
+<p>If you did everything right this <strong>.xo</strong> file should be ready to distribute.&#160; You can copy it to a thumb drive and install it on an XO laptop or onto another thumb drive running <em>Sugar on a Stick</em>.&#160; You probably should do that before distributing it any further.&#160; I like to live with new versions of my Activities for a week or so before putting them on <a href="http://activities.sugarlabs.org">activities.sugarlabs.org.</a>
+ <br/></p>
+<p>Now would be a good time to add <strong>dist</strong> to your <strong>.gitignore</strong> file, then commit it and push it to Gitorious.&#160; You don't want to have copies of your .xo files in Git.&#160; Another good thing to do at this point would be to tag your Git repository with the version number so you can identify which code goes with which version.
+</p>
+<p>
+</p>
+<pre>git tag -m "Release 1" v1 HEAD
+git push --tags
+</pre>
+<h2>Add Your Activity To ASLO
+ <br/></h2>
+<p>When you're ready to post the .xo file on ASLO you'll create an account as you did with the other websites.&#160; When you've logged in there you'll see a <strong>Tools</strong> link in the upper right corner of the page. Click on that and you'll see a popup menu with an option for <strong>Developer Hub</strong>, which you should click on.&#160; That will take you to the pages where you can add new Activities.&#160; The first thing it asks for when setting up a new Activity is what license you will use.&#160; After that you should have no problem getting your Activity set up.
+</p>
+<p>You will need to create an Activity icon as a .gif file and create screen shots of your Activity in action.&#160; You can do both of these things with <em>The GIMP</em> (GNU Image Manipulation Program).&#160; For the icon all you need to do is open the .svg file with The GIMP and <strong>Save As</strong> a .gif file.
+</p>
+<p>For the screen shots use sugar-emulator to display your Activity in action, then use the <strong>Screenshot</strong> option from the <strong>Create</strong> submenu of the <strong>File</strong> menu with these options:
+</p>
+<p><img alt="gimp1.jpg" src="static/ActivitiesGuideSugar-gimp1-en.jpg" height="346" width="358"/></p>
+<p>This tells GIMP to wait 10 seconds, then take a screenshot of the window you click on with the mouse.&#160; You'll know that the 10 seconds are up because the mouse pointer will change shape to a plus (+) sign.&#160; You also tell it <em>not</em> to include the window decoration (which means the window title bar and border).&#160; Since windows in Sugar do not have decorations eliminating the decorations used by sugar-emulator will give you a screenshot that looks exactly like a Sugar Activity in action.
+</p>
+<p>Every Activity needs one screenshot, but you can have more if you like.&#160; Screenshots help sell the Activity and instruct those who will use it on what the Activity can do.&#160; Unfortunately, ASLO cannot display pictures in a predictable sequence, so it is not suited to displaying steps to perform.
+</p>
+<p> Another thing you'll need to provide is a home page for your Activity.&#160; The one for <strong>Read Etexts</strong> is here:
+</p>
+<p><a href="http://wiki.sugarlabs.org/go/Activities/Read_Etexts">http://wiki.sugarlabs.org/go/Activities/Read_Etexts</a>
+ <br/></p>
+<p>Yes, one more website to get an account for.&#160; Once you do you can specify a link with <em>/go/Activities/some_name</em> and when you click on that link the Wiki will create a page for you.&#160; The software used for the Wiki is <em>MediaWiki</em>, the same as used for <em>Wikipedia</em>.&#160; Your page does not need to be as elaborate as mine is, but you definitely should provide a link to your source code in Gitorious.
+ <br/></p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch015_debugging-sugar-activities.xhtml b/app/static/doc/myosa/ch015_debugging-sugar-activities.xhtml
new file mode 100644
index 0000000..7c8dbfb
--- /dev/null
+++ b/app/static/doc/myosa/ch015_debugging-sugar-activities.xhtml
@@ -0,0 +1,259 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>Debugging Sugar Activities
+</h1>
+<h2>Introduction
+</h2>
+<p>No matter how careful you are it is reasonably likely that your Activity will not work perfectly the first time you try it out.&#160; Debugging a Sugar Activity is a bit different than debugging a standalone program.&#160; When you test a standalone program you just run the program itself.&#160; If there are syntax errors in the code you'll see the error messages on the console right away, and if you're running under the <strong>Eric</strong> IDE the offending line of code will be selected in the editor so you can correct it and keep going.
+</p>
+<p>With Sugar it's a bit different.&#160; It's the Sugar environment, not Eric, that runs your program.&#160; If there are syntax errors in your code you won't see them right away.&#160; Instead, the blinking Activity icon you see when your Activity starts up will just keep on blinking for several minutes and then will just go away, and your Activity won't start up.&#160; The only way you'll see the error that caused the problem will be to use the <strong>Log Activity</strong>.&#160; If your program has no syntax errors but does have logic errors you won't be able to step through your code with a debugger to find them.&#160; Instead, you'll need to use some kind of logging to trace through what's happening in your code, and again use the Log Activity to view the trace messages.&#160; Now would be a good time to repeat some advice I gave before:
+</p>
+<h2>Make A Standalone Version Of Your Program First
+</h2>
+<p>Whatever your Activity does, it's a good bet that 80% of it could be done by a standalone program which would be much less tedious to debug.&#160; If you can think of a way to make your Activity runnable as either an Activity or a standalone Python program then by all means do it.
+</p>
+<h2>Use PyLint, PyChecker, or PyFlakes
+</h2>
+<p>One of the advantages of a compiled language like <strong>C</strong> over an interpreted language like Python is that the compiler does a complete syntax check of the code before converting it to machine language.&#160; If there are syntax errors the compiler gives you informative error messages and stops the compile.&#160; There is a utility call <strong>lint</strong> which C programmers can use to do even more thorough checks than the compiler would do and find questionable things going on in the code.
+</p>
+<p>Python does not have a compiler but it does have several lint-like utilities you can run on your code before you test it.&#160; These utilities are <strong>pyflakes</strong>, <strong>pychecker</strong>, and <strong>pylint</strong>.&#160; Any Linux distribution should have all three available.
+</p>
+<h3>PyFlakes
+</h3>
+<p>Here is an example of using PyFlakes:
+</p>
+<pre><strong>pyflakes minichat.py</strong>
+minichat.py:25: 'COLOR_BUTTON_GREY' imported but unused
+minichat.py:28: 'XoColor' imported but unused
+minichat.py:29: 'Palette' imported but unused
+minichat.py:29: 'CanvasInvoker' imported but unused
+</pre>
+<p>PyFlakes seems to do the least checking of the three, but it does find errors like these above that a human eye would miss.
+</p>
+<h3>PyChecker
+</h3>
+<p>Here is PyChecker in action:
+ <br/></p>
+<pre><strong>pychecker ReadEtextsActivity.py</strong>
+Processing ReadEtextsActivity...
+/usr/lib/python2.5/site-packages/dbus/_dbus.py:251:
+DeprecationWarning: The dbus_bindings module is not public
+API and will go away soon.
+
+Most uses of dbus_bindings are applications catching
+the exception dbus.dbus_bindings.DBusException.
+You should use dbus.DBusException instead (this is
+compatible with all dbus-python versions since 0.40.2).
+
+If you need additional public API, please contact
+the maintainers via &lt;dbus@lists.freedesktop.org&gt;.
+
+ import dbus.dbus_bindings as m
+
+Warnings...
+
+/usr/lib/python2.5/site-packages/sugar/activity/activity.py:847:
+Parameter (ps) not used
+/usr/lib/python2.5/site-packages/sugar/activity/activity.py:992:
+Parameter (event) not used
+/usr/lib/python2.5/site-packages/sugar/activity/activity.py:992:
+Parameter (widget) not used
+/usr/lib/python2.5/site-packages/sugar/activity/activity.py:996:
+Parameter (widget) not used
+
+/usr/lib/python2.5/site-packages/sugar/graphics/window.py:157:
+No class attribute (_alert) found
+/usr/lib/python2.5/site-packages/sugar/graphics/window.py:164:
+Parameter (window) not used
+/usr/lib/python2.5/site-packages/sugar/graphics/window.py:188:
+Parameter (widget) not used
+/usr/lib/python2.5/site-packages/sugar/graphics/window.py:200:
+Parameter (event) not used
+/usr/lib/python2.5/site-packages/sugar/graphics/window.py:200:
+Parameter (widget) not used
+
+ReadEtextsActivity.py:62: Parameter (widget) not used
+
+4 errors suppressed, use -#/--limit to increase the number
+of errors displayed
+</pre>
+<p>PyChecker not only checks your code, it checks the code you import, including Sugar code.
+ <br/></p>
+<h3>PyLint
+</h3>
+<p> Here is PyLint, the most thorough of the three:
+</p>
+<pre><strong>pylint ReadEtextsActivity.py</strong>
+No config file found, using default configuration
+************* Module ReadEtextsActivity
+C:177: Line too long (96/80)
+C: 1: Missing docstring
+C: 27: Operator not preceded by a space
+page=0
+ ^
+C: 27: Invalid name "page" (should match
+(([A-Z_][A-Z0-9_]*)|(__.*__))$)
+C: 30:ReadEtextsActivity: Missing docstring
+C:174:ReadEtextsActivity.read_file: Invalid name "zf" (should
+match [a-z_][a-z0-9_]{2,30}$)
+W: 30:ReadEtextsActivity: Method 'write_file' is abstract
+in class 'Activity' but is not overridden
+R: 30:ReadEtextsActivity: Too many ancestors (12/7)
+W: 33:ReadEtextsActivity.__init__: Using the global statement
+R: 62:ReadEtextsActivity.keypress_cb:
+Too many return statements (7/6)
+C: 88:ReadEtextsActivity.page_previous: Missing docstring
+W: 89:ReadEtextsActivity.page_previous:
+Using the global statement
+C: 90:ReadEtextsActivity.page_previous:
+Operator not preceded by a space
+ page=page-1
+ ^
+C: 91:ReadEtextsActivity.page_previous:
+Operator not preceded by a space
+ if page &lt; 0: page=0
+ ^
+C: 91:ReadEtextsActivity.page_previous: More than one
+statement on a single line
+C: 96:ReadEtextsActivity.page_next: Missing docstring
+W: 97:ReadEtextsActivity.page_next: Using the global
+statement
+C: 98:ReadEtextsActivity.page_next: Operator not preceded
+by a space
+ page=page+1
+ ^
+C: 99:ReadEtextsActivity.page_next: More than one
+statement on a single line
+C:104:ReadEtextsActivity.font_decrease: Missing docstring
+C:112:ReadEtextsActivity.font_increase: Missing docstring
+C:118:ReadEtextsActivity.scroll_down: Missing docstring
+C:130:ReadEtextsActivity.scroll_up: Missing docstring
+C:142:ReadEtextsActivity.show_page: Missing docstring
+W:143:ReadEtextsActivity.show_page: Using global for
+'PAGE_SIZE' but no assigment is done
+W:143:ReadEtextsActivity.show_page: Using global for
+'current_word' but no assigment is done
+W:157:ReadEtextsActivity.save_extracted_file: Redefining
+name 'zipfile' from outer scope (line 21)
+C:163:ReadEtextsActivity.save_extracted_file: Invalid
+name "f" (should match [a-z_][a-z0-9_]{2,30}$)
+W:171:ReadEtextsActivity.read_file: Using global
+for 'PAGE_SIZE' but no assigment is done
+C:177:ReadEtextsActivity.read_file: Invalid name
+"currentFileName" (should match [a-z_][a-z0-9_]{2,30}$)
+C:179:ReadEtextsActivity.read_file: Invalid name
+"currentFileName" (should match [a-z_][a-z0-9_]{2,30}$)
+C:197:ReadEtextsActivity.make_new_filename: Missing
+docstring
+R:197:ReadEtextsActivity.make_new_filename: Method could be
+a function
+R: 30:ReadEtextsActivity: Too many public methods (350/20)
+W:174:ReadEtextsActivity.read_file: Attribute
+'zf' defined outside __init__
+W:181:ReadEtextsActivity.read_file: Attribute
+'etext_file' defined outside __init__
+W:175:ReadEtextsActivity.read_file: Attribute
+'book_files' defined outside __init__
+W:182:ReadEtextsActivity.read_file: Attribute
+'page_index' defined outside __init__
+
+<em>... A bunch of tables appear here ...</em>
+
+Global evaluation
+-----------------
+Your code has been rated at 7.52/10 (previous run: 7.52/10)
+</pre>
+<p>PyLint is the toughest on your code and your ego.&#160; It not only tells you about syntax errors, it tells you everything someone might find fault with in your code.&#160; This includes style issues that won't affect how your code runs but will affect how readable it is to other programmers.
+</p>
+<div class="objavi-forcebreak">
+</div>
+<h2>The Log Activity
+</h2>
+<p>When you start testing your Activities the Log Activity will be like your second home. It displays a list of log files in the left pane and when you select one it will display the contents of the file in the right pane.&#160; Every time you run your Activity a new log file is created for it, so you can compare the log you got this time with what you got on previous runs.&#160; The <strong>Edit</strong> toolbar is especially useful.&#160; It contains a button to show the log file with lines wrapped (which is not turned on by default but probably should be).&#160; It has another button to copy selections from the log to the clipboard, which will be handy if you want to show log messages to other developers.
+</p>
+<p>The <strong>Tools</strong> toolbar has a button to delete log files.&#160; I've never found a reason to use it.&#160; Log files go away on their own when you shut down sugar-emulator.
+ <br/></p>
+<p>&#160;<img alt="The Log Activity" src="static/ActivitiesGuideSugar-logactivity1-en.jpg" height="450" width="600"/></p>
+<p>
+</p>
+<p>Here is what the Log Activity looks like showing a syntax error in your code:&#160;
+</p>
+<p>
+</p>
+<p>&#160;<img alt="The Log Activity displaying a syntax error in Speak." src="static/ActivitiesGuideSugar-logactivity2-en.jpg" height="450" width="600"/></p>
+<h2>Logging
+</h2>
+<p>Without a doubt the oldest debugging technique there is would be the simple print statement.&#160; If you have a running program that misbehaves because of logic errors and you can't step through the code in a debugger to figure out what's happening you might print statements in your code.&#160; For instance, if you aren't sure that a method is ever getting executed you might put a statement like this as the first line of the method:
+</p>
+<pre> def my_method():
+ <strong>print 'my_method() begins'</strong>
+</pre>
+<p>You can include data in your print statements too.&#160; Suppose you need to know how many times a loop is run.&#160; You could do this:
+</p>
+<pre> while linecount &lt; PAGE_SIZE:
+ line = self.etext_file.readline()
+ label_text = label_text + unicode(line,
+ 'iso-8859-1')
+ linecount = linecount + 1
+ <strong>print 'linecount=', linecount </strong>
+</pre>
+<p>The output of these print statements can be seen in the Log Activity.&#160; When you're finished debugging your program you would remove these statements.
+</p>
+<p> An old programming book I read once made the case for leaving the statements in the finished program.&#160; The authors felt that using these statements for debugging and them removing them is a bit like wearing a parachute when the plane is on the ground and taking it off when it's airborne.&#160; If the program is out in the world and has problems you might well wish you had those statements in the code so you could help the user and yourself figure out what's going on.&#160; On the other hand, print statements aren't free.&#160; They do take time to run and they fill up the log files with junk.&#160; What we need are print statements that you can turn on an off.
+</p>
+<p>The way you can do this is with Python Standard Logging.&#160; In the form used by most Activities it looks like this:
+</p>
+<pre> self._logger = logging.getLogger(
+ 'read-etexts-activity')
+</pre>
+<p>These statements would go in the <em>__init__()</em> method of your Activity.&#160; Every time you want to do a print() statement you would do this instead:
+</p>
+<pre> def _shared_cb(self, activity):
+ <strong>self._logger.debug('My activity was shared')</strong>
+ self.initiating = True
+ self._sharing_setup()
+
+ <strong>self._logger.debug(
+ 'This is my activity: making a tube...')</strong>
+ id = self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].\
+ OfferDBusTube(SERVICE, {})
+
+ def _sharing_setup(self):
+ if self._shared_activity is None:
+ <strong>self._logger.error(
+ 'Failed to share or join activity')</strong>
+ return
+</pre>
+<p>Notice that there are two kinds of logging going on here: <strong>debug</strong> and <strong>error</strong>. &#160; These are error levels.&#160; Every statement has one, and they control which log statements are run and which are ignored.&#160; There are several levels of error logging, from lowest severity to highest:
+</p>
+<p>
+</p>
+<pre><code> self._logger.debug("debug message")
+ self._logger.info("info message")
+ self._logger.warn("warn message")
+ self._logger.error("error message")
+ self._logger.critical("critical message")
+</code></pre>
+<p>When you set the error level in your program to one of these values you get messages with that level and higher.&#160; You can set the level in your program code like this:
+</p>
+<p>
+</p>
+<pre><code> self._logger.setLevel(logging.DEBUG)
+</code></pre>
+<p>You can also set the logging level outside your program code using an <strong>environment variable</strong>.&#160; For instance, in Sugar .82 and lower you can start sugar-emulator like this:
+</p>
+<pre>SUGAR_LOGGER_LEVEL=debug sugar-emulator
+</pre>
+<p> You can do the same thing with .84 and later, but there is a more convenient way.&#160; Edit the file <strong>~/.sugar/debug</strong> and uncomment the line that sets the SUGAR_LOGGER_LEVEL.&#160; Whatever value you have for SUGAR_LOGGER_LEVEL in ~/.sugar/debug will override the one set by the environment variable, so either change the setting in the file or use the environment variable, but don't do both.
+ <br/></p>
+<h2>The Analyze Activity
+</h2>
+<p>Another Activity you may find yourself using at some point is <strong>Analyze</strong>.&#160; This is more likely to be used to debug Sugar itself than to debug your Activity.&#160; If, for instance, your collaboration test environment doesn't seem to be working this Activity might help you or someone else figure out why.
+</p>
+<p>I don't have a lot to say about this Activity here, but you should be aware that it exists.
+ <br/></p>
+<p><img alt="The Analyze Activity" src="static/ActivitiesGuideSugar-analyzeactivity_1-en.jpg" height="450" width="600"/><br/>&#160;
+</p>
+<p>
+ <br/></p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch017_making-shared-activities.xhtml b/app/static/doc/myosa/ch017_making-shared-activities.xhtml
new file mode 100644
index 0000000..a5a6f25
--- /dev/null
+++ b/app/static/doc/myosa/ch017_making-shared-activities.xhtml
@@ -0,0 +1,2185 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>Making Shared Activities
+</h1>
+<h2>Introduction
+</h2>
+<p>One of the distinctive features of Sugar is how many Activities support being used by more than one person at a time.&#160; More and more computers are being used as a communications medium.&#160; The latest computer games don't just pit the player against the computer; they create a world where players compete against each other.&#160; Websites like <em>Facebook</em> are increasingly popular because they allow people to interact with each other and even play games.&#160; It is only natural that educational software should support these kinds of interactions.
+</p>
+<p> I have a niece that is an enthusiastic member of the <em>Club Penguin</em> website created by Disney.&#160; When I gave her Sugar on a Stick Blueberry as an extra Christmas gift I demonstrated the Neighborhood view and told her that Sugar would make her whole computer like <em>Club Penguin</em>.&#160; She thought that was a pretty cool idea.&#160; I felt pretty cool saying it.
+ <br/></p>
+<h2>Running Sugar As More Than One User
+ <br/></h2>
+<p>Before you write any piece of software you need to give some thought to how you will test it.&#160; In the case of a shared Activity you might think you'd need more than one computer available to do testing, but those who designed Sugar did give some thought to testing shared Activities and gave us ways to test them using only one computer.&#160; These methods have been evolving so there are slight variations in how you test depending on the version of Sugar you're using.&#160; The first thing you have to know is how to run multiple copies of Sugar as different users.
+ <br/></p>
+<h3>Fedora 10 (Sugar .82)
+</h3>
+<p>In Sugar .82 there is a handy way to run multiple copies of sugar-emulator and have each copy be a different user, without having to be logged into your Linux box as more than one user.&#160; On the command line for each additional user you want add a SUGAR_PROFILE environment variable like this:
+</p>
+<pre>SUGAR_PROFILE=austen sugar-emulator</pre>
+<p>When you do this sugar-emulator will create a directory named austen under ~/.sugar to store profile information, etc.&#160; You will be prompted to enter a name and select colors for your icon.&#160; Every time you launch using the SUGAR_PROFILE of austen you will be this user.&#160; If you launch with no SUGAR_PROFILE you will be the regular user you set up before.
+ <br/></p>
+<h3>Fedora 11 (Sugar .84)
+</h3>
+<p>As handy as using SUGAR_PROFILE is the developers of Sugar decided it had limitations so with version .84 and later it no longer works.&#160; With .84 and later you need to create a second Linux user and run your sugar-emulators as two separate Linux users.&#160; In the GNOME environment there is an option <strong>Users and Groups</strong> in the <strong>Administration</strong> submenu of the <strong>System</strong> menu which will enable you to set up a second user.&#160; Before it comes up it will prompt you for the administrative password you created when you first set up Linux.
+</p>
+<p>Creating the second user is simple enough, but how do you go about being logged in as two different users at the same time?&#160; It's actually pretty simple.&#160; You need to open a terminal window and type this:
+</p>
+<pre>ssh -XY <em>jausten</em>@localhost</pre>
+<p>where "jausten" is the userid of the second user.&#160; You will be asked to verify that the computer at "localhost" should be trusted.&#160; Since "localhost" just means that you are using the network to connect to another account on the same computer it is safe to answer "yes".&#160; Then you will be prompted to enter her password, and from then on everything you do in that terminal window will be done as her.&#160; You can launch sugar-emulator from that terminal and the first time you do it will prompt you for a name and icon colors.
+ <br/></p>
+<h3>sugar-jhbuild
+</h3>
+<p>With sugar-jhbuild (the latest version of Sugar) things are a bit different again.&#160; You will use the method of logging in as multiple Linux users like you did in .84, but you won't get prompted for a name.&#160; Instead the name associated with the userid you're running under will be the name you'll use in Sugar.&#160; You won't be able to change it, but you will be able to choose your icon colors as before.
+</p>
+<p>You will need a separate install of sugar-jhbuild for each user.&#160; These additional installs will go quickly because you installed all the dependencies the first time.
+</p>
+<div class="objavi-forcebreak">
+</div>
+<h2>Connecting To Other Users
+</h2>
+<p>Sugar uses software called <strong>Telepathy </strong>that implements an instant messaging protocol called <strong>XMPP</strong> (<em>Extended Messaging and Presence Protocol</em>).&#160; This protocol used to be called <strong>Jabber</strong>.&#160; In essence Telepathy lets you put an instant messaging client in your Activity.&#160; You can use this to send messages from user to user, execute methods remotely, and do file transfers.
+ <br/></p>
+<p>There are actually two ways that Sugar users can join together in a network:
+</p>
+<h3>Salut
+</h3>
+<p>If two computer users are connected to the same segment of a network they should be able to find each other and share Activities.&#160; If you have a home network where everyone uses the same router you can share with others on that network.&#160; This is sometimes called <em>Link-Local XMPP</em>. &#160; The Telepathy software that makes this possible is called <strong>Salut</strong>.
+</p>
+<p>The XO laptop has special hardware and software to support <em>Mesh Networking</em>, where XO laptops that are near each other can automatically start networking with each other without needing a router.&#160; As far as Sugar is concerned, it doesn't matter what kind of network you have.&#160; Wired or wireless, Mesh or not, they all work.
+ <br/></p>
+<h3>Jabber Server
+</h3>
+<p>The other way to connect to other users is by going through a Jabber Server.&#160; The advantage of using a Jabber server is you can contact and share Activities with people outside your own network.&#160; These people might even be on the other side of the world.&#160; Jabber allows Activities in different networks to connect when both networks are protected by firewalls.&#160; The part of Telepathy that works with a Jabber server is called <strong>Gabble</strong>.
+ <br/></p>
+<p>Generally you should use Salut for testing if at all possible. This simplifies testing and doesn't use up resources on a Jabber server.
+ <br/></p>
+<p>It does not matter if your Activity connects to others using Gabble or Salut.&#160; In fact, the Activity has no idea which it is using.&#160; Those details are hidden from the Activity by Telepathy.&#160; Any Activity that works with Salut will work with Gabble and vice versa.
+</p>
+<p>To set up sugar-emulator to use Salut go to the Sugar control panel:
+ <br/></p>
+<p> <img alt="collab1_1.jpg" src="static/ActivitiesGuideSugar-collab1_1-en.jpg" width="584" height="569"/></p>
+<p>In Sugar .82 this menu option is <strong>Control Panel</strong>.&#160; In later versions it is <strong>My Settings</strong>.
+</p>
+<p><img alt="collab2_1.jpg" src="static/ActivitiesGuideSugar-collab2_1-en.jpg" width="600" height="505"/></p>
+<p>Click on the <strong>Network</strong> icon.
+</p>
+<p><img alt="collab3_1.jpg" src="static/ActivitiesGuideSugar-collab3_1-en.jpg" width="538" height="517"/></p>
+<p>The <strong>Server</strong> field in this screen should be empty to use Salut.&#160; You can use the backspace key to remove any entry there.
+</p>
+<p>You will need to follow these steps for every Sugar user that will take part in your test.
+</p>
+<p>If for some reason you wish to test your Activity using a Jabber server the OLPC Wiki maintains a list of publicly available servers at <a href="http://wiki.laptop.org/go/Community_Jabber_Servers">http://wiki.laptop.org/go/Community_Jabber_Servers</a>.
+</p>
+<p> Once you have either Salut or a Jabber server set up in both instances of Sugar that you are running you should look at the Neighborhood view of both to see if they can detect each other, and perhaps try out the <strong>Chat</strong> Activity between the two.&#160; If you have that working you're ready to try programming a shared Activity.
+</p>
+<h2>The MiniChat Activity
+</h2>
+<p>Just as we took the <strong>Read Etexts</strong> Activity and stripped it down to the basics we're going to do the same to the <strong>Chat</strong> Activity to create a new Activity called <strong>MiniChat</strong>.&#160; The real Chat Activity has a number of features that we don't need to demonstrate shared Activity messaging:
+</p>
+<ul><li>It has the ability to load its source code into <strong>Pippy</strong> for viewing.&#160; This was a feature that all Activities on the XO were supposed to have, but Chat is one of the few that implemented it.&#160; Personally, if I want to see an Activity's code I prefer to go to <a href="http://git.sugarlabs.org">git.sugarlabs.org</a>&#160; where I can see old versions of the code as well as the latest.</li>
+ <li>Chat can connect one to one with a conventional <strong>XMPP</strong> client.&#160; This may be useful for Chat but would not be needed or desirable for most shared Activities.</li>
+ <li>If you include a URL in a Chat message the user interface enables you to click on the URL make a Journal entry for that URL.&#160; You can then use the Journal to open it with the <strong>Browse</strong> Activity.&#160; (This is necessary because activities cannot launch each other).&#160; Pretty cool, but not needed to demonstrate how to make a shared Activity.</li>
+ <li>The chat session is stored in the Journal.&#160; When you resume a Chat entry from the Journal it restores the messages from your previous chat session into the user interface.&#160; We already know how to save things to the Journal and restore things from the Journal, so MiniChat won't do this.</li>
+</ul><p>The resulting code is about half as long as the original.&#160; I made a few other changes too:
+</p>
+<ul><li>The text entry field is above the chat messages, instead of below.&#160; This makes it easier to do partial screenshots of the Activity in action.</li>
+ <li>I removed the new style toolbar and added an old style toolbar, so I could test it in Fedora 10 and 11 which don't support the new toolbars.
+ <br/></li>
+ <li>I took the class <span class="TypeName"><strong>TextChannelWrapper</strong> and put it in its own file.&#160; I did this because the class looked like it might be useful for other projects.</span></li>
+</ul><p><span class="TypeName"/>The code and all supporting files for <strong>MiniChat</strong> are in the <strong>MiniChat</strong> directory of the Git repository.&#160; You'll need to run
+</p>
+<pre>./setup.py dev</pre>
+<p><span class="TypeName">on the project to make it ready to test.&#160; The <strong>activity.info</strong> looks like this:</span>
+</p>
+<pre>[Activity]
+name = Mini Chat
+service_name = net.flossmanuals.MiniChat
+icon = chat
+exec = sugar-activity minichat.MiniChat
+show_launcher = yes
+activity_version = 1
+license = GPLv2+
+</pre>
+<p>Here is the code for <strong>textchannel.py</strong>:
+</p>
+<pre>import logging
+
+from telepathy.client import Connection, Channel
+from telepathy.interfaces import (
+ CHANNEL_INTERFACE, CHANNEL_INTERFACE_GROUP,
+ CHANNEL_TYPE_TEXT, CONN_INTERFACE_ALIASING)
+from telepathy.constants import (
+ CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES,
+ CHANNEL_TEXT_MESSAGE_TYPE_NORMAL)
+
+class TextChannelWrapper(object):
+ """Wrap a telepathy Text Channel to make
+ usage simpler."""
+ def __init__(self, text_chan, conn):
+ """Connect to the text channel"""
+ self._activity_cb = None
+ self._activity_close_cb = None
+ self._text_chan = text_chan
+ self._conn = conn
+ self._logger = logging.getLogger(
+ 'minichat-activity.TextChannelWrapper')
+ self._signal_matches = []
+ m = self._text_chan[CHANNEL_INTERFACE].\
+ connect_to_signal(
+ 'Closed', self._closed_cb)
+ self._signal_matches.append(m)
+
+ def send(self, text):
+ """Send text over the Telepathy text channel."""
+ # XXX Implement CHANNEL_TEXT_MESSAGE_TYPE_ACTION
+ if self._text_chan is not None:
+ self._text_chan[CHANNEL_TYPE_TEXT].Send(
+ CHANNEL_TEXT_MESSAGE_TYPE_NORMAL, text)
+
+ def close(self):
+ """Close the text channel."""
+ self._logger.debug('Closing text channel')
+ try:
+ self._text_chan[CHANNEL_INTERFACE].Close()
+ except:
+ self._logger.debug('Channel disappeared!')
+ self._closed_cb()
+
+ def _closed_cb(self):
+ """Clean up text channel."""
+ self._logger.debug('Text channel closed.')
+ for match in self._signal_matches:
+ match.remove()
+ self._signal_matches = []
+ self._text_chan = None
+ if self._activity_close_cb is not None:
+ self._activity_close_cb()
+
+ def set_received_callback(self, callback):
+ """Connect the function callback to the signal.
+
+ callback -- callback function taking buddy
+ and text args
+ """
+ if self._text_chan is None:
+ return
+ self._activity_cb = callback
+ m = self._text_chan[CHANNEL_TYPE_TEXT].\
+ connect_to_signal(
+ 'Received', self._received_cb)
+ self._signal_matches.append(m)
+
+ def handle_pending_messages(self):
+ """Get pending messages and show them as
+ received."""
+ for id, timestamp, sender, type, flags, text \
+ in self._text_chan[
+ CHANNEL_TYPE_TEXT].ListPendingMessages(
+ False):
+ self._received_cb(id, timestamp, sender,
+ type, flags, text)
+
+ def _received_cb(self, id, timestamp, sender,
+ type, flags, text):
+ """Handle received text from the text channel.
+
+ Converts sender to a Buddy.
+ Calls self._activity_cb which is a callback
+ to the activity.
+ """
+ if self._activity_cb:
+ buddy = self._get_buddy(sender)
+ self._activity_cb(buddy, text)
+ self._text_chan[
+ CHANNEL_TYPE_TEXT].
+ AcknowledgePendingMessages([id])
+ else:
+ self._logger.debug(
+ 'Throwing received message on the floor'
+ ' since there is no callback connected. See '
+ 'set_received_callback')
+
+ def set_closed_callback(self, callback):
+ """Connect a callback for when the text channel
+ is closed.
+
+ callback -- callback function taking no args
+
+ """
+ self._activity_close_cb = callback
+
+ def _get_buddy(self, cs_handle):
+ """Get a Buddy from a (possibly channel-specific)
+ handle."""
+ # XXX This will be made redundant once Presence
+ # Service provides buddy resolution
+ from sugar.presence import presenceservice
+ # Get the Presence Service
+ pservice = presenceservice.get_instance()
+ # Get the Telepathy Connection
+ tp_name, tp_path = \
+ pservice.get_preferred_connection()
+ conn = Connection(tp_name, tp_path)
+ group = self._text_chan[CHANNEL_INTERFACE_GROUP]
+ my_csh = group.GetSelfHandle()
+ if my_csh == cs_handle:
+ handle = conn.GetSelfHandle()
+ elif group.GetGroupFlags() &amp; \
+ CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES:
+ handle = group.GetHandleOwners([cs_handle])[0]
+ else:
+ handle = cs_handle
+
+ # XXX: deal with failure to get the handle owner
+ assert handle != 0
+
+ return pservice.get_buddy_by_telepathy_handle(
+ tp_name, tp_path, handle)
+</pre>
+<p>Here is the code for <strong>minichat.py</strong>:
+</p>
+<pre>from gettext import gettext as _
+import hippo
+import gtk
+import pango
+import logging
+from sugar.activity.activity import (Activity,
+ ActivityToolbox, SCOPE_PRIVATE)
+from sugar.graphics.alert import NotifyAlert
+from sugar.graphics.style import (Color, COLOR_BLACK,
+ COLOR_WHITE, COLOR_BUTTON_GREY, FONT_BOLD,
+ FONT_NORMAL)
+from sugar.graphics.roundbox import CanvasRoundBox
+from sugar.graphics.xocolor import XoColor
+from sugar.graphics.palette import Palette, CanvasInvoker
+
+from textchannel import TextChannelWrapper
+
+logger = logging.getLogger('minichat-activity')
+
+class MiniChat(Activity):
+ def __init__(self, handle):
+ Activity.__init__(self, handle)
+
+ root = self.make_root()
+ self.set_canvas(root)
+ root.show_all()
+ self.entry.grab_focus()
+
+ toolbox = ActivityToolbox(self)
+ activity_toolbar = toolbox.get_activity_toolbar()
+ activity_toolbar.keep.props.visible = False
+ self.set_toolbox(toolbox)
+ toolbox.show()
+
+ self.owner = self._pservice.get_owner()
+ # Auto vs manual scrolling:
+ self._scroll_auto = True
+ self._scroll_value = 0.0
+ # Track last message, to combine several
+ # messages:
+ self._last_msg = None
+ self._last_msg_sender = None
+ self.text_channel = None
+
+ if self._shared_activity:
+ # we are joining the activity
+ self.connect('joined', self._joined_cb)
+ if self.get_shared():
+ # we have already joined
+ self._joined_cb()
+ else:
+ # we are creating the activity
+ if not self.metadata or self.metadata.get(
+ 'share-scope',
+ SCOPE_PRIVATE) == SCOPE_PRIVATE:
+ # if we are in private session
+ self._alert(_('Off-line'),
+ _('Share, or invite someone.'))
+ self.connect('shared', self._shared_cb)
+
+ def _shared_cb(self, activity):
+ logger.debug('Chat was shared')
+ self._setup()
+
+ def _setup(self):
+ self.text_channel = TextChannelWrapper(
+ self._shared_activity.telepathy_text_chan,
+ self._shared_activity.telepathy_conn)
+ self.text_channel.set_received_callback(
+ self._received_cb)
+ self._alert(_('On-line'), _('Connected'))
+ self._shared_activity.connect('buddy-joined',
+ self._buddy_joined_cb)
+ self._shared_activity.connect('buddy-left',
+ self._buddy_left_cb)
+ self.entry.set_sensitive(True)
+ self.entry.grab_focus()
+
+ def _joined_cb(self, activity):
+ """Joined a shared activity."""
+ if not self._shared_activity:
+ return
+ logger.debug('Joined a shared chat')
+ for buddy in \
+ self._shared_activity.get_joined_buddies():
+ self._buddy_already_exists(buddy)
+ self._setup()
+
+ def _received_cb(self, buddy, text):
+ """Show message that was received."""
+ if buddy:
+ nick = buddy.props.nick
+ else:
+ nick = '???'
+ logger.debug(
+ 'Received message from %s: %s', nick, text)
+ self.add_text(buddy, text)
+
+ def _alert(self, title, text=None):
+ alert = NotifyAlert(timeout=5)
+ alert.props.title = title
+ alert.props.msg = text
+ self.add_alert(alert)
+ alert.connect('response', self._alert_cancel_cb)
+ alert.show()
+
+ def _alert_cancel_cb(self, alert, response_id):
+ self.remove_alert(alert)
+
+ def _buddy_joined_cb (self, activity, buddy):
+ """Show a buddy who joined"""
+ if buddy == self.owner:
+ return
+ if buddy:
+ nick = buddy.props.nick
+ else:
+ nick = '???'
+ self.add_text(buddy, buddy.props.nick+'
+ '+_('joined the chat'),
+ status_message=True)
+
+ def _buddy_left_cb (self, activity, buddy):
+ """Show a buddy who joined"""
+ if buddy == self.owner:
+ return
+ if buddy:
+ nick = buddy.props.nick
+ else:
+ nick = '???'
+ self.add_text(buddy, buddy.props.nick+'
+ '+_('left the chat'),
+ status_message=True)
+
+ def _buddy_already_exists(self, buddy):
+ """Show a buddy already in the chat."""
+ if buddy == self.owner:
+ return
+ if buddy:
+ nick = buddy.props.nick
+ else:
+ nick = '???'
+ self.add_text(buddy, buddy.props.nick+
+ ' '+_('is here'),
+ status_message=True)
+
+ def make_root(self):
+ conversation = hippo.CanvasBox(
+ spacing=0,
+ background_color=COLOR_WHITE.get_int())
+ self.conversation = conversation
+
+ entry = gtk.Entry()
+ entry.modify_bg(gtk.STATE_INSENSITIVE,
+ COLOR_WHITE.get_gdk_color())
+ entry.modify_base(gtk.STATE_INSENSITIVE,
+ COLOR_WHITE.get_gdk_color())
+ entry.set_sensitive(False)
+ entry.connect('activate',
+ self.entry_activate_cb)
+ self.entry = entry
+
+ hbox = gtk.HBox()
+ hbox.add(entry)
+
+ sw = hippo.CanvasScrollbars()
+ sw.set_policy(hippo.ORIENTATION_HORIZONTAL,
+ hippo.SCROLLBAR_NEVER)
+ sw.set_root(conversation)
+ self.scrolled_window = sw
+
+ vadj = self.scrolled_window.props.widget.\
+ get_vadjustment()
+ vadj.connect('changed', self.rescroll)
+ vadj.connect('value-changed',
+ self.scroll_value_changed_cb)
+
+ canvas = hippo.Canvas()
+ canvas.set_root(sw)
+
+ box = gtk.VBox(homogeneous=False)
+ box.pack_start(hbox, expand=False)
+ box.pack_start(canvas)
+
+ return box
+
+ def rescroll(self, adj, scroll=None):
+ """Scroll the chat window to the bottom"""
+ if self._scroll_auto:
+ adj.set_value(adj.upper-adj.page_size)
+ self._scroll_value = adj.get_value()
+
+ def scroll_value_changed_cb(self, adj, scroll=None):
+ """Turn auto scrolling on or off.
+
+ If the user scrolled up, turn it off.
+ If the user scrolled to the bottom, turn it back on.
+ """
+ if adj.get_value() &lt; self._scroll_value:
+ self._scroll_auto = False
+ elif adj.get_value() == adj.upper-adj.page_size:
+ self._scroll_auto = True
+
+ def add_text(self, buddy, text, status_message=False):
+ """Display text on screen, with name and colors.
+
+ buddy -- buddy object
+ text -- string, what the buddy said
+ status_message -- boolean
+ False: show what buddy said
+ True: show what buddy did
+
+ hippo layout:
+ .------------- rb ---------------.
+ | +name_vbox+ +----msg_vbox----+ |
+ | | | | | |
+ | | nick: | | +--msg_hbox--+ | |
+ | | | | | text | | |
+ | +---------+ | +------------+ | |
+ | | | |
+ | | +--msg_hbox--+ | |
+ | | | text | | |
+ | | +------------+ | |
+ | +----------------+ |
+ `--------------------------------'
+ """
+ if buddy:
+ nick = buddy.props.nick
+ color = buddy.props.color
+ try:
+ color_stroke_html, color_fill_html = \
+ color.split(',')
+ except ValueError:
+ color_stroke_html, color_fill_html = (
+ '#000000', '#888888')
+ # Select text color based on fill color:
+ color_fill_rgba = Color(
+ color_fill_html).get_rgba()
+ color_fill_gray = (color_fill_rgba[0] +
+ color_fill_rgba[1] +
+ color_fill_rgba[2])/3
+ color_stroke = Color(
+ color_stroke_html).get_int()
+ color_fill = Color(color_fill_html).get_int()
+ if color_fill_gray &lt; 0.5:
+ text_color = COLOR_WHITE.get_int()
+ else:
+ text_color = COLOR_BLACK.get_int()
+ else:
+ nick = '???'
+ # XXX: should be '' but leave for debugging
+ color_stroke = COLOR_BLACK.get_int()
+ color_fill = COLOR_WHITE.get_int()
+ text_color = COLOR_BLACK.get_int()
+ color = '#000000,#FFFFFF'
+
+ # Check for Right-To-Left languages:
+ if pango.find_base_dir(nick, -1) == \
+ pango.DIRECTION_RTL:
+ lang_rtl = True
+ else:
+ lang_rtl = False
+
+ # Check if new message box or add text to previous:
+ new_msg = True
+ if self._last_msg_sender:
+ if not status_message:
+ if buddy == self._last_msg_sender:
+ # Add text to previous message
+ new_msg = False
+
+ if not new_msg:
+ rb = self._last_msg
+ msg_vbox = rb.get_children()[1]
+ msg_hbox = hippo.CanvasBox(
+ orientation=hippo.ORIENTATION_HORIZONTAL)
+ msg_vbox.append(msg_hbox)
+ else:
+ rb = CanvasRoundBox(
+ background_color=color_fill,
+ border_color=color_stroke,
+ padding=4)
+ rb.props.border_color = color_stroke
+ self._last_msg = rb
+ self._last_msg_sender = buddy
+ if not status_message:
+ name = hippo.CanvasText(text=nick+': ',
+ color=text_color,
+ font_desc=FONT_BOLD.get_pango_desc())
+ name_vbox = hippo.CanvasBox(
+ orientation=hippo.ORIENTATION_VERTICAL)
+ name_vbox.append(name)
+ rb.append(name_vbox)
+ msg_vbox = hippo.CanvasBox(
+ orientation=hippo.ORIENTATION_VERTICAL)
+ rb.append(msg_vbox)
+ msg_hbox = hippo.CanvasBox(
+ orientation=hippo.ORIENTATION_HORIZONTAL)
+ msg_vbox.append(msg_hbox)
+
+ if status_message:
+ self._last_msg_sender = None
+
+ if text:
+ message = hippo.CanvasText(
+ text=text,
+ size_mode=hippo.CANVAS_SIZE_WRAP_WORD,
+ color=text_color,
+ font_desc=FONT_NORMAL.get_pango_desc(),
+ xalign=hippo.ALIGNMENT_START)
+ msg_hbox.append(message)
+
+ # Order of boxes for RTL languages:
+ if lang_rtl:
+ msg_hbox.reverse()
+ if new_msg:
+ rb.reverse()
+
+ if new_msg:
+ box = hippo.CanvasBox(padding=2)
+ box.append(rb)
+ self.conversation.append(box)
+
+ def entry_activate_cb(self, entry):
+ text = entry.props.text
+ logger.debug('Entry: %s' % text)
+ if text:
+ self.add_text(self.owner, text)
+ entry.props.text = ''
+ if self.text_channel:
+ self.text_channel.send(text)
+ else:
+ logger.debug(
+ 'Tried to send message but text '
+ 'channel not connected.')
+</pre>
+<p>And this is what the Activity looks like in action:
+</p>
+<p> <img alt="MiniChat in action" src="static/ActivitiesGuideSugar-collab4-en.jpg" width="600" height="373"/></p>
+<p>Try launching more than one copy of sugar-emulator, with this Activity installed in each.&#160; If you're using Fedora 10 and SUGAR_PROFILE the Activity does not need to be installed more than once, but if you're using a later version of Sugar that requires separate Linux userids for each instance you'll need to maintain separate copies of the code for each user.&#160; In your own projects using a central Git repository at <a href="http://git.sugarlabs.org">git.sugarlabs.org</a>&#160; will make this easy.&#160; You just do a git push to copy your changes to the central repository and a git pull to copy them to your second userid.&#160; The second userid can use the public URL.&#160; There's no need to set up SSH for any user other than the primary one.
+</p>
+<p>You may have read somewhere that you can install an Activity on one machine and share that Activity with another that does not have the activity installed.&#160; In such a case the second machine would get a copy of the Activity from the first machine and install it automatically.&#160; You may have also read that if two users of a shared Activity have different versions of that Activity then the one who has the newer version will automatically update the older.&#160; Neither statement is true now or is likely to be true in the near future.&#160; These ideas are discussed on the mailing lists from time to time but there are practical difficulties to overcome before anything like that could work, mostly having to do with security.&#160; For now both users of a shared Activity must have the Activity installed.&#160; On the other hand, depending on how the Activity is written two different versions of an Activity may be able to communicate with one another.&#160; If the messages they exchange are in the same format there should be no problem.
+ <br/></p>
+<p>Once you have both instances of sugar-emulator going you can launch MiniChat on one and invite the second user to Join the Chat session.&#160; You can do both with the Neighborhood panes of each instance.&#160; Making the invitation looks like this:
+</p>
+<p><img alt="Making the invitation" src="static/ActivitiesGuideSugar-collab5-en.jpg" width="391" height="246"/></p>
+<p>Accepting it looks like this:
+</p>
+<p><img alt="collab6.jpg" src="static/ActivitiesGuideSugar-collab6-en.jpg" width="390" height="215"/></p>
+<p>After you've played with <strong>MiniChat</strong> for awhile come back and we'll discuss the secrets of using Telepathy to create a shared Activity.
+</p>
+<div class="objavi-forcebreak">
+</div>
+<h2>Know who Your Buddies Are
+ <br/></h2>
+<p> XMPP, as we said before, is the <strong>Extended Messaging and Presence Protocol</strong>.&#160;&#8286; <strong>Presence</strong> is just what it sounds like; it handles letting you know who is available to share your Activity, as well as what other Activities are available to share.&#160; There are two ways to share your Activity.&#160; The first one is when you change the <strong>Share with:</strong> pulldown on the standard toolbar so it reads <strong>My Neighborhood</strong> instead of <strong>Private</strong>.&#160; That means anyone on the network can share your Activity.&#160; The other way to share is to go to the Neighborhood view and invite someone specific to share.&#160; The person getting the invitation has no idea of the invitation was specifically for him or broadcast to the Neighborhood.&#160; The technical term for persons sharing your Activity is <strong>Buddies</strong>.&#160; The place where Buddies meet and collaborate is called an <strong>MUC</strong> or <strong>Multi User Chatroom</strong>.
+ <br/></p>
+<p>The code used by our Activity for inviting Buddies and joining the Activity as a Buddy is in the <em>__init__() </em>method:
+</p>
+<pre> <strong>if self._shared_activity:</strong>
+ # we are joining the activity
+ self.connect('joined', self._joined_cb)
+ if self.get_shared():
+ # we have already joined
+ self._joined_cb()
+ else:
+ # we are creating the activity
+ if not self.metadata or self.metadata.get(
+ 'share-scope',
+ SCOPE_PRIVATE) == SCOPE_PRIVATE:
+ # if we are in private session
+ self._alert(_('Off-line'),
+ _('Share, or invite someone.'))
+ self.connect('shared', self._shared_cb)
+
+ def _shared_cb(self, activity):
+ logger.debug('Chat was shared')
+ self._setup()
+
+ def _joined_cb(self, activity):
+ """Joined a shared activity."""
+ if not self._shared_activity:
+ return
+ logger.debug('Joined a shared chat')
+ for buddy in \
+ self._shared_activity.get_joined_buddies():
+ self._buddy_already_exists(buddy)
+ self._setup()
+
+ def _setup(self):
+ self.text_channel = TextChannelWrapper(
+ self._shared_activity.telepathy_text_chan,
+ self._shared_activity.telepathy_conn)
+ self.text_channel.set_received_callback(
+ self._received_cb)
+ self._alert(_('On-line'), _('Connected'))
+ self._shared_activity.connect('buddy-joined',
+ self._buddy_joined_cb)
+ self._shared_activity.connect('buddy-left',
+ self._buddy_left_cb)
+ self.entry.set_sensitive(True)
+ self.entry.grab_focus()
+</pre>
+<p>There are two ways to launch an Activity: as the first user of an Activity or by joining an existing Activity.&#160; The first line above in <strong>bold</strong> determines whether we are joining or are the first user of the Activity.&#160; If so we ask for the <em>_joined_cb()</em> method to be run when the 'joined' event occurs. This method gets a buddy list from the <em>_shared_activity</em> object and creates messages in the user interface informing the user that these buddies are already in the chat room.&#160; Then it runs the <em>_setup()</em> method.
+ <br/></p>
+<p>If we are not joining an existing Activity then we check to see if we are currently sharing the Activity with anyone.&#160; If we aren't we pop up a message telling the user to invite someone to chat.&#160; We also request that when the 'shared' even happens the <em>_shared_cb()</em> method should run.&#160; This method just runs the <em>_setup()</em> method.
+</p>
+<p>The <em>_setup()</em> method creates a <strong>TextChannelWrapper</strong> object using the code in <strong>textchannel.py</strong>.&#160; It also tells the _shared_activity object that it wants some callback methods run when new buddies join the Activity and when existing buddies leave the Activity.&#160; Everything you need to know about your buddies can be found in the code above, except how to send messages to them.&#160; For that we use the <strong>Text Channel</strong>.&#160; There is no need to learn about the Text Channel in great detail because the TextChannelWrapper class does everything you'll ever need to do with the TextChannel and hides the details from you.
+</p>
+<pre> def entry_activate_cb(self, entry):
+ text = entry.props.text
+ logger.debug('Entry: %s' % text)
+ if text:
+ self.add_text(self.owner, text)
+ entry.props.text = ''
+ if self.text_channel:
+ self.text_channel.send(text)
+ else:
+ logger.debug(
+ 'Tried to send message but text '
+ 'channel not connected.')
+</pre>
+<p>The <em>add_text()</em> method is of interest.&#160; It takes the owner of the message and figures out what colors belong to that owner and displays the message in those colors.&#160; In the case of messages sent by the Activity it gets the owner like this in the <em>__init__()</em> method:
+</p>
+<p>
+</p>
+<pre>&#160;&#160;&#160;&#160;&#160;&#160;&#160; self.owner = self._pservice.get_owner()</pre>
+<p>In the case of received messages it gets the buddy the message came from:
+</p>
+<pre> def _received_cb(self, buddy, text):
+ """Show message that was received."""
+ if buddy:
+ nick = buddy.props.nick
+ else:
+ nick = '???'
+ logger.debug('Received message from %s: %s',
+ nick, text)
+ self.add_text(buddy, text)
+</pre>
+<p>But what if we want to do more than just send text messages back and forth?&#160; What do we use for that?
+ <br/></p>
+<h2>It's A Series Of Tubes!
+</h2>
+<p> No, not the Internet.&#160; Telepathy has a concept called <strong>Tubes</strong> which describes the way instances of an Activity can communicate together.&#160; What Telepathy does is take the Text Channel and build Tubes on top of it.&#160; There are two kinds of Tubes:
+</p>
+<ul><li>D-Bus Tubes</li>
+ <li>Stream Tubes</li>
+</ul><p>A <strong>D-Bus Tube</strong> is used to enable one instance of an Activity to call methods in the Buddy instances of the Activity.&#160; A <strong>Stream Tube</strong> is used for sending data over <strong>Sockets</strong>, for instance for copying a file from one instance of an Activity to another.&#160; A Socket is a way of communicating over a network using Internet Protocols.&#160; For instance the HTTP protocol used by the World Wide Web is implemented with Sockets.&#160; In the next example we'll use HTTP to transfer books from one instance of <strong>Read Etexts III</strong> to another.
+</p>
+<h2>Read Etexts III, Now with Book Sharing!
+ <br/></h2>
+<p> The Git repository with the code samples for this book has a file named <strong>ReadEtextsActivity3.py</strong> in the <strong>Making_Shared_Activities</strong> directory which looks like this:
+</p>
+<pre>import sys
+import os
+import logging
+import tempfile
+import time
+import zipfile
+import pygtk
+import gtk
+import pango
+import dbus
+import gobject
+import telepathy
+from sugar.activity import activity
+from sugar.graphics import style
+from sugar import network
+from sugar.datastore import datastore
+from sugar.graphics.alert import NotifyAlert
+from toolbar import ReadToolbar, ViewToolbar
+from gettext import gettext as _
+
+page=0
+PAGE_SIZE = 45
+TOOLBAR_READ = 2
+
+logger = logging.getLogger('read-etexts2-activity')
+
+class ReadHTTPRequestHandler(
+ network.ChunkedGlibHTTPRequestHandler):
+ """HTTP Request Handler for transferring document
+ while collaborating.
+
+ RequestHandler class that integrates with Glib
+ mainloop. It writes the specified file to the
+ client in chunks, returning control to the
+ mainloop between chunks.
+
+ """
+ def translate_path(self, path):
+ """Return the filepath to the shared document."""
+ return self.server.filepath
+
+
+class ReadHTTPServer(network.GlibTCPServer):
+ """HTTP Server for transferring document while
+ collaborating."""
+ def __init__(self, server_address, filepath):
+ """Set up the GlibTCPServer with the
+ ReadHTTPRequestHandler.
+
+ filepath -- path to shared document to be served.
+ """
+ self.filepath = filepath
+ network.GlibTCPServer.__init__(self,
+ server_address, ReadHTTPRequestHandler)
+
+
+class ReadURLDownloader(network.GlibURLDownloader):
+ """URLDownloader that provides content-length and
+ content-type."""
+
+ def get_content_length(self):
+ """Return the content-length of the download."""
+ if self._info is not None:
+ return int(self._info.headers.get(
+ 'Content-Length'))
+
+ def get_content_type(self):
+ """Return the content-type of the download."""
+ if self._info is not None:
+ return self._info.headers.get('Content-type')
+ return None
+
+READ_STREAM_SERVICE = 'read-etexts-activity-http'
+
+class ReadEtextsActivity(activity.Activity):
+ def __init__(self, handle):
+ "The entry point to the Activity"
+ global page
+ activity.Activity.__init__(self, handle)
+
+ self.fileserver = None
+ self.object_id = handle.object_id
+
+ toolbox = activity.ActivityToolbox(self)
+ activity_toolbar = toolbox.get_activity_toolbar()
+ activity_toolbar.keep.props.visible = False
+
+ self.edit_toolbar = activity.EditToolbar()
+ self.edit_toolbar.undo.props.visible = False
+ self.edit_toolbar.redo.props.visible = False
+ self.edit_toolbar.separator.props.visible = False
+ self.edit_toolbar.copy.set_sensitive(False)
+ self.edit_toolbar.copy.connect('clicked',
+ self.edit_toolbar_copy_cb)
+ self.edit_toolbar.paste.props.visible = False
+ toolbox.add_toolbar(_('Edit'), self.edit_toolbar)
+ self.edit_toolbar.show()
+
+ self.read_toolbar = ReadToolbar()
+ toolbox.add_toolbar(_('Read'), self.read_toolbar)
+ self.read_toolbar.back.connect('clicked',
+ self.go_back_cb)
+ self.read_toolbar.forward.connect('clicked',
+ self.go_forward_cb)
+ self.read_toolbar.num_page_entry.connect('activate',
+ self.num_page_entry_activate_cb)
+ self.read_toolbar.show()
+
+ self.view_toolbar = ViewToolbar()
+ toolbox.add_toolbar(_('View'), self.view_toolbar)
+ self.view_toolbar.connect('go-fullscreen',
+ self.view_toolbar_go_fullscreen_cb)
+ self.view_toolbar.zoom_in.connect('clicked',
+ self.zoom_in_cb)
+ self.view_toolbar.zoom_out.connect('clicked',
+ self.zoom_out_cb)
+ self.view_toolbar.show()
+
+ self.set_toolbox(toolbox)
+ toolbox.show()
+ self.scrolled_window = gtk.ScrolledWindow()
+ self.scrolled_window.set_policy(gtk.POLICY_NEVER,
+ gtk.POLICY_AUTOMATIC)
+ self.scrolled_window.props.shadow_type = \
+ gtk.SHADOW_NONE
+
+ self.textview = gtk.TextView()
+ self.textview.set_editable(False)
+ self.textview.set_cursor_visible(False)
+ self.textview.set_left_margin(50)
+ self.textview.connect("key_press_event",
+ self.keypress_cb)
+
+ self.progressbar = gtk.ProgressBar()
+ self.progressbar.set_orientation(
+ gtk.PROGRESS_LEFT_TO_RIGHT)
+ self.progressbar.set_fraction(0.0)
+
+ self.scrolled_window.add(self.textview)
+ self.textview.show()
+ self.scrolled_window.show()
+
+ vbox = gtk.VBox()
+ vbox.pack_start(self.progressbar, False,
+ False, 10)
+ vbox.pack_start(self.scrolled_window)
+ self.set_canvas(vbox)
+ vbox.show()
+
+ page = 0
+ self.clipboard = gtk.Clipboard(
+ display=gtk.gdk.display_get_default(),
+ selection="CLIPBOARD")
+ self.textview.grab_focus()
+ self.font_desc = pango.FontDescription("sans %d" %
+ style.zoom(10))
+ self.textview.modify_font(self.font_desc)
+
+ buffer = self.textview.get_buffer()
+ self.markset_id = buffer.connect("mark-set",
+ self.mark_set_cb)
+
+ self.toolbox.set_current_toolbar(TOOLBAR_READ)
+ self.unused_download_tubes = set()
+ self.want_document = True
+ self.download_content_length = 0
+ self.download_content_type = None
+ # Status of temp file used for write_file:
+ self.tempfile = None
+ self.close_requested = False
+ self.connect("shared", self.shared_cb)
+
+ self.is_received_document = False
+
+ if self._shared_activity and \
+ handle.object_id == None:
+ # We're joining, and we don't already have
+ # the document.
+ if self.get_shared():
+ # Already joined for some reason, just get the
+ # document
+ self.joined_cb(self)
+ else:
+ # Wait for a successful join before trying to get
+ # the document
+ self.connect("joined", self.joined_cb)
+
+ def keypress_cb(self, widget, event):
+ "Respond when the user presses one of the arrow keys"
+ keyname = gtk.gdk.keyval_name(event.keyval)
+ print keyname
+ if keyname == 'plus':
+ self.font_increase()
+ return True
+ if keyname == 'minus':
+ self.font_decrease()
+ return True
+ if keyname == 'Page_Up' :
+ self.page_previous()
+ return True
+ if keyname == 'Page_Down':
+ self.page_next()
+ return True
+ if keyname == 'Up' or keyname == 'KP_Up' \
+ or keyname == 'KP_Left':
+ self.scroll_up()
+ return True
+ if keyname == 'Down' or keyname == 'KP_Down' \
+ or keyname == 'KP_Right':
+ self.scroll_down()
+ return True
+ return False
+
+ def num_page_entry_activate_cb(self, entry):
+ global page
+ if entry.props.text:
+ new_page = int(entry.props.text) - 1
+ else:
+ new_page = 0
+
+ if new_page &gt;= self.read_toolbar.total_pages:
+ new_page = self.read_toolbar.total_pages - 1
+ elif new_page &lt; 0:
+ new_page = 0
+
+ self.read_toolbar.current_page = new_page
+ self.read_toolbar.set_current_page(new_page)
+ self.show_page(new_page)
+ entry.props.text = str(new_page + 1)
+ self.read_toolbar.update_nav_buttons()
+ page = new_page
+
+ def go_back_cb(self, button):
+ self.page_previous()
+
+ def go_forward_cb(self, button):
+ self.page_next()
+
+ def page_previous(self):
+ global page
+ page=page-1
+ if page &lt; 0: page=0
+ self.read_toolbar.set_current_page(page)
+ self.show_page(page)
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ v_adjustment.value = v_adjustment.upper - \
+ v_adjustment.page_size
+
+ def page_next(self):
+ global page
+ page=page+1
+ if page &gt;= len(self.page_index): page=0
+ self.read_toolbar.set_current_page(page)
+ self.show_page(page)
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ v_adjustment.value = v_adjustment.lower
+
+ def zoom_in_cb(self, button):
+ self.font_increase()
+
+ def zoom_out_cb(self, button):
+ self.font_decrease()
+
+ def font_decrease(self):
+ font_size = self.font_desc.get_size() / 1024
+ font_size = font_size - 1
+ if font_size &lt; 1:
+ font_size = 1
+ self.font_desc.set_size(font_size * 1024)
+ self.textview.modify_font(self.font_desc)
+
+ def font_increase(self):
+ font_size = self.font_desc.get_size() / 1024
+ font_size = font_size + 1
+ self.font_desc.set_size(font_size * 1024)
+ self.textview.modify_font(self.font_desc)
+
+ def mark_set_cb(self, textbuffer, iter, textmark):
+
+ if textbuffer.get_has_selection():
+ begin, end = textbuffer.get_selection_bounds()
+ self.edit_toolbar.copy.set_sensitive(True)
+ else:
+ self.edit_toolbar.copy.set_sensitive(False)
+
+ def edit_toolbar_copy_cb(self, button):
+ textbuffer = self.textview.get_buffer()
+ begin, end = textbuffer.get_selection_bounds()
+ copy_text = textbuffer.get_text(begin, end)
+ self.clipboard.set_text(copy_text)
+
+ def view_toolbar_go_fullscreen_cb(self, view_toolbar):
+ self.fullscreen()
+
+ def scroll_down(self):
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ if v_adjustment.value == v_adjustment.upper - \
+ v_adjustment.page_size:
+ self.page_next()
+ return
+ if v_adjustment.value &lt; v_adjustment.upper - \
+ v_adjustment.page_size:
+ new_value = v_adjustment.value + \
+ v_adjustment.step_increment
+ if new_value &gt; v_adjustment.upper - \
+ v_adjustment.page_size:
+ new_value = v_adjustment.upper - \
+ v_adjustment.page_size
+ v_adjustment.value = new_value
+
+ def scroll_up(self):
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ if v_adjustment.value == v_adjustment.lower:
+ self.page_previous()
+ return
+ if v_adjustment.value &gt; v_adjustment.lower:
+ new_value = v_adjustment.value - \
+ v_adjustment.step_increment
+ if new_value &lt; v_adjustment.lower:
+ new_value = v_adjustment.lower
+ v_adjustment.value = new_value
+
+ def show_page(self, page_number):
+ global PAGE_SIZE, current_word
+ position = self.page_index[page_number]
+ self.etext_file.seek(position)
+ linecount = 0
+ label_text = '\n\n\n'
+ textbuffer = self.textview.get_buffer()
+ while linecount &lt; PAGE_SIZE:
+ line = self.etext_file.readline()
+ label_text = label_text + unicode(line,
+ 'iso-8859-1')
+ linecount = linecount + 1
+ label_text = label_text + '\n\n\n'
+ textbuffer.set_text(label_text)
+ self.textview.set_buffer(textbuffer)
+
+ def save_extracted_file(self, zipfile, filename):
+ "Extract the file to a temp directory for viewing"
+ filebytes = zipfile.read(filename)
+ outfn = self.make_new_filename(filename)
+ if (outfn == ''):
+ return False
+ f = open(os.path.join(self.get_activity_root(),
+ 'tmp', outfn), 'w')
+ try:
+ f.write(filebytes)
+ finally:
+ f.close()
+
+ def get_saved_page_number(self):
+ global page
+ title = self.metadata.get('title', '')
+ if title == '' or not \
+ title[len(title)-1].isdigit():
+ page = 0
+ else:
+ i = len(title) - 1
+ newPage = ''
+ while (title[i].isdigit() and i &gt; 0):
+ newPage = title[i] + newPage
+ i = i - 1
+ if title[i] == 'P':
+ page = int(newPage) - 1
+ else:
+ # not a page number; maybe a
+ # volume number.
+ page = 0
+
+ def save_page_number(self):
+ global page
+ title = self.metadata.get('title', '')
+ if title == '' or not \
+ title[len(title)- 1].isdigit():
+ title = title + ' P' + str(page + 1)
+ else:
+ i = len(title) - 1
+ while (title[i].isdigit() and i &gt; 0):
+ i = i - 1
+ if title[i] == 'P':
+ title = title[0:i] + 'P' + str(page + 1)
+ else:
+ title = title + ' P' + str(page + 1)
+ self.metadata['title'] = title
+
+ def read_file(self, filename):
+ "Read the Etext file"
+ global PAGE_SIZE, page
+
+ tempfile = os.path.join(self.get_activity_root(),
+ 'instance', 'tmp%i' % time.time())
+ os.link(filename, tempfile)
+ self.tempfile = tempfile
+
+ if zipfile.is_zipfile(filename):
+ self.zf = zipfile.ZipFile(filename, 'r')
+ self.book_files = self.zf.namelist()
+ self.save_extracted_file(self.zf,
+ self.book_files[0])
+ currentFileName = os.path.join(
+ self.get_activity_root(),
+ 'tmp', self.book_files[0])
+ else:
+ currentFileName = filename
+
+ self.etext_file = open(currentFileName,"r")
+ self.page_index = [ 0 ]
+ pagecount = 0
+ linecount = 0
+ while self.etext_file:
+ line = self.etext_file.readline()
+ if not line:
+ break
+ linecount = linecount + 1
+ if linecount &gt;= PAGE_SIZE:
+ position = self.etext_file.tell()
+ self.page_index.append(position)
+ linecount = 0
+ pagecount = pagecount + 1
+ if filename.endswith(".zip"):
+ os.remove(currentFileName)
+ self.get_saved_page_number()
+ self.show_page(page)
+ self.read_toolbar.set_total_pages(
+ pagecount + 1)
+ self.read_toolbar.set_current_page(page)
+
+ # We've got the document, so if we're a shared
+ # activity, offer it
+ if self.get_shared():
+ self.watch_for_tubes()
+ self.share_document()
+
+ def make_new_filename(self, filename):
+ partition_tuple = filename.rpartition('/')
+ return partition_tuple[2]
+
+ def write_file(self, filename):
+ "Save meta data for the file."
+ if self.is_received_document:
+ # This document was given to us by someone, so
+ # we have to save it to the Journal.
+ self.etext_file.seek(0)
+ filebytes = self.etext_file.read()
+ f = open(filename, 'wb')
+ try:
+ f.write(filebytes)
+ finally:
+ f.close()
+ elif self.tempfile:
+ if self.close_requested:
+ os.link(self.tempfile, filename)
+ logger.debug(
+ "Removing temp file %s because we "
+ "will close",
+ self.tempfile)
+ os.unlink(self.tempfile)
+ self.tempfile = None
+ else:
+ # skip saving empty file
+ raise NotImplementedError
+
+ self.metadata['activity'] = self.get_bundle_id()
+ self.save_page_number()
+
+ def can_close(self):
+ self.close_requested = True
+ return True
+
+ def joined_cb(self, also_self):
+ """Callback for when a shared activity is joined.
+
+ Get the shared document from another participant.
+ """
+ self.watch_for_tubes()
+ gobject.idle_add(self.get_document)
+
+ def get_document(self):
+ if not self.want_document:
+ return False
+
+ # Assign a file path to download if one
+ # doesn't exist yet
+ if not self._jobject.file_path:
+ path = os.path.join(self.get_activity_root(),
+ 'instance',
+ 'tmp%i' % time.time())
+ else:
+ path = self._jobject.file_path
+
+ # Pick an arbitrary tube we can try to
+ # download the document from
+ try:
+ tube_id = self.unused_download_tubes.pop()
+ except (ValueError, KeyError), e:
+ logger.debug(
+ 'No tubes to get the document '
+ 'from right now: %s',
+ e)
+ return False
+
+ # Avoid trying to download the document multiple
+ # timesat once
+ self.want_document = False
+ gobject.idle_add(self.download_document, tube_id, path)
+ return False
+
+ def download_document(self, tube_id, path):
+ chan = self._shared_activity.telepathy_tubes_chan
+ iface = chan[telepathy.CHANNEL_TYPE_TUBES]
+ addr = iface.AcceptStreamTube(tube_id,
+ telepathy.SOCKET_ADDRESS_TYPE_IPV4,
+ telepathy.SOCKET_ACCESS_CONTROL_LOCALHOST, 0,
+ utf8_strings=True)
+ logger.debug('Accepted stream tube: '
+ 'listening address is %r',
+ addr)
+ assert isinstance(addr, dbus.Struct)
+ assert len(addr) == 2
+ assert isinstance(addr[0], str)
+ assert isinstance(addr[1], (int, long))
+ assert addr[1] &gt; 0 and addr[1] &lt; 65536
+ port = int(addr[1])
+
+ self.progressbar.show()
+ getter = ReadURLDownloader(
+ "http://%s:%d/document"
+ % (addr[0], port))
+ getter.connect("finished",
+ self.download_result_cb, tube_id)
+ getter.connect("progress",
+ self.download_progress_cb, tube_id)
+ getter.connect("error",
+ self.download_error_cb, tube_id)
+ logger.debug("Starting download to %s...", path)
+ getter.start(path)
+ self.download_content_length = \
+ getter.get_content_length()
+ self.download_content_type = \
+ getter.get_content_type()
+ return False
+
+ def download_progress_cb(self, getter,
+ bytes_downloaded, tube_id):
+ if self.download_content_length &gt; 0:
+ logger.debug(
+ "Downloaded %u of %u bytes from tube %u...",
+ bytes_downloaded,
+ self.download_content_length,
+ tube_id)
+ else:
+ logger.debug("Downloaded %u bytes from tube %u...",
+ bytes_downloaded, tube_id)
+ total = self.download_content_length
+ self.set_downloaded_bytes(bytes_downloaded, total)
+ gtk.gdk.threads_enter()
+ while gtk.events_pending():
+ gtk.main_iteration()
+ gtk.gdk.threads_leave()
+
+ def set_downloaded_bytes(self, bytes, total):
+ fraction = float(bytes) / float(total)
+ self.progressbar.set_fraction(fraction)
+ logger.debug("Downloaded percent", fraction)
+
+ def clear_downloaded_bytes(self):
+ self.progressbar.set_fraction(0.0)
+ logger.debug("Cleared download bytes")
+
+ def download_error_cb(self, getter, err, tube_id):
+ self.progressbar.hide()
+ logger.debug(
+ "Error getting document from tube %u: %s",
+ tube_id, err)
+ self.alert(_('Failure'),
+ _('Error getting document from tube'))
+ self.want_document = True
+ self.download_content_length = 0
+ self.download_content_type = None
+ gobject.idle_add(self.get_document)
+
+ def download_result_cb(self, getter, tempfile,
+ suggested_name, tube_id):
+ if self.download_content_type.startswith(
+ 'text/html'):
+ # got an error page instead
+ self.download_error_cb(getter,
+ 'HTTP Error', tube_id)
+ return
+
+ del self.unused_download_tubes
+
+ self.tempfile = tempfile
+ file_path = os.path.join(self.get_activity_root(),
+ 'instance', '%i' % time.time())
+ logger.debug(
+ "Saving file %s to datastore...", file_path)
+ os.link(tempfile, file_path)
+ self._jobject.file_path = file_path
+ datastore.write(self._jobject,
+ transfer_ownership=True)
+
+ logger.debug(
+ "Got document %s (%s) from tube %u",
+ tempfile, suggested_name, tube_id)
+ self.is_received_document = True
+ self.read_file(tempfile)
+ self.save()
+ self.progressbar.hide()
+
+ def shared_cb(self, activityid):
+ """Callback when activity shared.
+
+ Set up to share the document.
+
+ """
+ # We initiated this activity and have now shared it,
+ # so by definition we have the file.
+ logger.debug('Activity became shared')
+ self.watch_for_tubes()
+ self.share_document()
+
+ def share_document(self):
+ """Share the document."""
+ h = hash(self._activity_id)
+ port = 1024 + (h % 64511)
+ logger.debug(
+ 'Starting HTTP server on port %d', port)
+ self.fileserver = ReadHTTPServer(("", port),
+ self.tempfile)
+
+ # Make a tube for it
+ chan = self._shared_activity.telepathy_tubes_chan
+ iface = chan[telepathy.CHANNEL_TYPE_TUBES]
+ self.fileserver_tube_id = iface.OfferStreamTube(
+ READ_STREAM_SERVICE,
+ {},
+ telepathy.SOCKET_ADDRESS_TYPE_IPV4,
+ ('127.0.0.1', dbus.UInt16(port)),
+ telepathy.SOCKET_ACCESS_CONTROL_LOCALHOST,
+ 0)
+
+ def watch_for_tubes(self):
+ """Watch for new tubes."""
+ tubes_chan = self._shared_activity.telepathy_tubes_chan
+
+ tubes_chan[telepathy.CHANNEL_TYPE_TUBES].\
+ connect_to_signal(
+ 'NewTube', self.new_tube_cb)
+ tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes(
+ reply_handler=self.list_tubes_reply_cb,
+ error_handler=self.list_tubes_error_cb)
+
+ def new_tube_cb(self, tube_id, initiator, tube_type,
+ service, params, state):
+ """Callback when a new tube becomes available."""
+ logger.debug(
+ 'New tube: ID=%d initator=%d type=%d service=%s '
+ 'params=%r state=%d', tube_id, initiator,
+ tube_type, service, params, state)
+ if service == READ_STREAM_SERVICE:
+ logger.debug('I could download from that tube')
+ self.unused_download_tubes.add(tube_id)
+ # if no download is in progress, let's fetch
+ # the document
+ if self.want_document:
+ gobject.idle_add(self.get_document)
+
+ def list_tubes_reply_cb(self, tubes):
+ """Callback when new tubes are available."""
+ for tube_info in tubes:
+ self.new_tube_cb(*tube_info)
+
+ def list_tubes_error_cb(self, e):
+ """Handle ListTubes error by logging."""
+ logger.error('ListTubes() failed: %s', e)
+
+ def alert(self, title, text=None):
+ alert = NotifyAlert(timeout=20)
+ alert.props.title = title
+ alert.props.msg = text
+ self.add_alert(alert)
+ alert.connect('response', self.alert_cancel_cb)
+ alert.show()
+
+ def alert_cancel_cb(self, alert, response_id):
+ self.remove_alert(alert)
+ self.textview.grab_focus()
+</pre>
+<p>The contents of <strong>activity.info</strong> are these lines:
+</p>
+<pre>[Activity]
+name = Read Etexts III
+service_name = net.flossmanuals.ReadEtextsActivity
+icon = read-etexts
+exec = sugar-activity ReadEtextsActivity3.ReadEtextsActivity
+show_launcher = no
+activity_version = 1
+mime_types = text/plain;application/zip
+license = GPLv2+
+</pre>
+<p>To try it out, download a <em>Project Gutenberg</em> book to the Journal, open it with this latest <strong>Read Etexts III</strong>, then share it with a second user who has the program installed but not running.&#160; She should accept the invitation to join that appears in her Neighborhood view.&#160; When she does Read Etexts II will start up and copy the book from the first user over the network and load it.&#160; The Activity will first show a blank screen, but then a progress bar will appear just under the toolbar and indicate the progress of the copying.&#160; When it is finished the first page of the book will appear.
+</p>
+<p>So how does it work?&#160; Let's look at the code.&#160; The first points of interest are the class definitions that appear at the beginning: <strong>ReadHTTPRequestHandler</strong>, <strong>ReadHTTPServer</strong>, and <strong>ReadURLDownloader</strong>.&#160; These three classes extend (that is to say, inherit code from) classes provided by the <strong>sugar.network</strong> package.&#160; These classes provide an <strong>HTTP client</strong> for receiving the book and an <strong>HTTP Server</strong> for sending the book.
+</p>
+<p>This is the code used to send a book:
+</p>
+<pre> def shared_cb(self, activityid):
+ """Callback when activity shared.
+
+ Set up to share the document.
+
+ """
+ # We initiated this activity and have now shared it,
+ # so by definition we have the file.
+ logger.debug('Activity became shared')
+ self.watch_for_tubes()
+ self.share_document()
+
+ def share_document(self):
+ """Share the document."""
+ h = hash(self._activity_id)
+ port = 1024 + (h % 64511)
+ logger.debug(
+ 'Starting HTTP server on port %d', port)
+ self.fileserver = ReadHTTPServer(("", port),
+ self.tempfile)
+
+ # Make a tube for it
+ chan = self._shared_activity.telepathy_tubes_chan
+ iface = chan[telepathy.CHANNEL_TYPE_TUBES]
+ self.fileserver_tube_id = iface.OfferStreamTube(
+ READ_STREAM_SERVICE,
+ {},
+ telepathy.SOCKET_ADDRESS_TYPE_IPV4,
+ ('127.0.0.1', dbus.UInt16(port)),
+ telepathy.SOCKET_ACCESS_CONTROL_LOCALHOST,
+ 0)
+</pre>
+<p>You will notice that a hash of the <em>_activity_id</em> is used to get a port number.&#160; That port is used for the HTTP server and is passed to Telepathy, which offers it as a <strong>Stream Tube</strong>.&#160; On the receiving side we have this code:
+</p>
+<pre> def joined_cb(self, also_self):
+ """Callback for when a shared activity is joined.
+
+ Get the shared document from another participant.
+ """
+ self.watch_for_tubes()
+ gobject.idle_add(self.get_document)
+
+ def get_document(self):
+ if not self.want_document:
+ return False
+
+ # Assign a file path to download if one doesn't
+ # exist yet
+ if not self._jobject.file_path:
+ path = os.path.join(self.get_activity_root(),
+ 'instance',
+ 'tmp%i' % time.time())
+ else:
+ path = self._jobject.file_path
+
+ # Pick an arbitrary tube we can try to download the
+ # document from
+ try:
+ tube_id = self.unused_download_tubes.pop()
+ except (ValueError, KeyError), e:
+ logger.debug(
+ 'No tubes to get the document from '
+ 'right now: %s',
+ e)
+ return False
+
+ # Avoid trying to download the document multiple
+ # times at once
+ self.want_document = False
+ gobject.idle_add(self.download_document,
+ tube_id, path)
+ return False
+
+ def download_document(self, tube_id, path):
+ chan = self._shared_activity.telepathy_tubes_chan
+ iface = chan[telepathy.CHANNEL_TYPE_TUBES]
+ addr = iface.AcceptStreamTube(tube_id,
+ telepathy.SOCKET_ADDRESS_TYPE_IPV4,
+ telepathy.SOCKET_ACCESS_CONTROL_LOCALHOST,
+ 0,
+ utf8_strings=True)
+ logger.debug(
+ 'Accepted stream tube: listening address is %r',
+ addr)
+ assert isinstance(addr, dbus.Struct)
+ assert len(addr) == 2
+ assert isinstance(addr[0], str)
+ assert isinstance(addr[1], (int, long))
+ assert addr[1] &gt; 0 and addr[1] &lt; 65536
+ port = int(addr[1])
+
+ self.progressbar.show()
+ getter = ReadURLDownloader(
+ "http://%s:%d/document"
+ % (addr[0], port))
+ getter.connect("finished",
+ self.download_result_cb, tube_id)
+ getter.connect("progress",
+ self.download_progress_cb, tube_id)
+ getter.connect("error",
+ self.download_error_cb, tube_id)
+ logger.debug(
+ "Starting download to %s...", path)
+ getter.start(path)
+ self.download_content_length = \
+ getter.get_content_length()
+ self.download_content_type = \
+ getter.get_content_type()
+ return False
+
+ def download_progress_cb(self, getter,
+ bytes_downloaded, tube_id):
+ if self.download_content_length &gt; 0:
+ logger.debug(
+ "Downloaded %u of %u bytes from tube %u...",
+ bytes_downloaded,
+ self.download_content_length,
+ tube_id)
+ else:
+ logger.debug(
+ "Downloaded %u bytes from tube %u...",
+ bytes_downloaded, tube_id)
+ total = self.download_content_length
+ self.set_downloaded_bytes(bytes_downloaded,
+ total)
+ gtk.gdk.threads_enter()
+ while gtk.events_pending():
+ gtk.main_iteration()
+ gtk.gdk.threads_leave()
+
+ def download_error_cb(self, getter, err, tube_id):
+ self.progressbar.hide()
+ logger.debug(
+ "Error getting document from tube %u: %s",
+ tube_id, err)
+ self.alert(_('Failure'),
+ _('Error getting document from tube'))
+ self.want_document = True
+ self.download_content_length = 0
+ self.download_content_type = None
+ gobject.idle_add(self.get_document)
+
+ def download_result_cb(self, getter, tempfile,
+ suggested_name, tube_id):
+ if self.download_content_type.startswith(
+ 'text/html'):
+ # got an error page instead
+ self.download_error_cb(getter,
+ 'HTTP Error', tube_id)
+ return
+
+ del self.unused_download_tubes
+
+ self.tempfile = tempfile
+ file_path = os.path.join(self.get_activity_root(),
+ 'instance',
+ '%i' % time.time())
+ logger.debug(
+ "Saving file %s to datastore...", file_path)
+ os.link(tempfile, file_path)
+ self._jobject.file_path = file_path
+ datastore.write(self._jobject,
+ transfer_ownership=True)
+
+ logger.debug(
+ "Got document %s (%s) from tube %u",
+ tempfile, suggested_name, tube_id)
+ self.is_received_document = True
+ self.read_file(tempfile)
+ self.save()
+ self.progressbar.hide()
+</pre>
+<p>Telepathy gives us the address and port number associated with a Stream Tube and we set up the HTTP Client to read from it.&#160; The client reads the file in chunks and calls <em>download_progress_cb()</em> after every chunk so we can update a progress bar to show the user how the download is progressing.&#160; There are also callback methods for when there is a download error and for when the download is finished,&#160;
+</p>
+<p>The <strong>ReadURLDownloader</strong> class is not only useful for transferring files over Stream Tubes, it can also be used to interact with websites and web services.&#160; My Activity <strong>Get Internet Archive Books</strong> uses this class for that purpose.
+</p>
+<p>The one remaining piece is the code which handles getting Stream Tubes to download the book from.&#160; In this code, adapted from the <strong>Read</strong> Activity, as soon as an instance of an Activity receives a book it turns around and offers to share it, thus the Activity may have several possible Tubes it could get the book from:
+</p>
+<pre>READ_STREAM_SERVICE = 'read-etexts-activity-http'
+
+ <em>...</em>
+
+ def watch_for_tubes(self):
+ """Watch for new tubes."""
+ tubes_chan = self._shared_activity.\
+ telepathy_tubes_chan
+
+ tubes_chan[telepathy.CHANNEL_TYPE_TUBES].\
+ connect_to_signal(
+ 'NewTube',
+ self.new_tube_cb)
+ tubes_chan[telepathy.CHANNEL_TYPE_TUBES].\
+ ListTubes(
+ reply_handler=self.list_tubes_reply_cb,
+ error_handler=self.list_tubes_error_cb)
+
+ def new_tube_cb(self, tube_id, initiator,
+ tube_type, service, params, state):
+ """Callback when a new tube becomes available."""
+ logger.debug(
+ 'New tube: ID=%d initator=%d type=%d service=%s '
+ 'params=%r state=%d', tube_id, initiator,
+ tube_type,
+ service, params, state)
+ if service == READ_STREAM_SERVICE:
+ logger.debug('I could download from that tube')
+ self.unused_download_tubes.add(tube_id)
+ # if no download is in progress,
+ # let's fetch the document
+ if self.want_document:
+ gobject.idle_add(self.get_document)
+
+ def list_tubes_reply_cb(self, tubes):
+ """Callback when new tubes are available."""
+ for tube_info in tubes:
+ self.new_tube_cb(*tube_info)
+
+ def list_tubes_error_cb(self, e):
+ """Handle ListTubes error by logging."""
+ logger.error('ListTubes() failed: %s', e)</pre>
+<p>The <strong>READ_STREAM_SERVICE</strong> constant is defined near the top of the source file.
+</p>
+<h2>Using D-Bus Tubes
+</h2>
+<p> <strong>D-Bus</strong> is a method of supporting <strong>IPC</strong>, or <strong>Inter-Process Communication</strong>, that was created for the GNOME desktop environment.&#160; The idea of IPC is to allow two running programs to communicate with each other and execute each other's code.&#160; GNOME uses D-Bus to provide communication between the desktop environment and the programs running in it, and also between GNOME and the operating system.&#160; A <strong>D-Bus Tube</strong> is how Telepathy makes it possible for an instance of an Activity running on one computer to execute methods in another instance of the same Activity running on a different computer.&#160; Instead of just sending simple text messages back and forth or doing file transfers, your Activities can be truly shared.&#160; That is, your Activity can allow many people to work on the same task together.
+</p>
+<p>I have never written an Activity that uses D-Bus Tubes myself, but many others have.&#160; We're going to take a look at code from two of them: <strong>Scribble</strong> by Sayamindu Dasgupta and <strong>Batalla Naval</strong>, by Gerard J. Cerchio and Andr&#233;s Ambrois, which was written for the Ceibal Jam.
+</p>
+<p><strong>Scribble</strong> is a drawing program that allows many people to work on the same drawing at the same time.&#160; Instead of allowing you to choose what colors you will draw with, it uses the background and foreground colors of your Buddy icon (the XO stick figure) to draw with.&#160; That way, with many people drawing shapes it's easy to know who drew what.&#160; If you join the Activity in progress Scribble will update your screen so your drawing matches everyone else's screen.&#160; Scribble in action looks like this:
+</p>
+<p><img alt="Scribble in action" src="static/ActivitiesGuideSugar-scribble-en.jpg" width="600" height="440"/><br/></p>
+<p><strong>Batalla Naval</strong> is a version of the classic game <em>Battleship</em>.&#160; Each player has two grids: one for placing his own ships (actually the computer places the ships for you) and another blank grid representing the area where your opponent's ships are.&#160; You can't see his ships and he can't see yours.&#160; You click on the opponent's grid (on the right) to indicate where you want to aim an artillery shell.&#160; When you do the corresponding square will light up in both your grid and your opponent's own ship grid.&#160; If the square you picked corresponds to a square where your opponent has placed a ship that square will show up in a different color.&#160; The object is to find the squares containing your opponent's ships before he finds yours.&#160; The game in action looks like this:
+</p>
+<p><img alt="Batalla Naval in action!" src="static/ActivitiesGuideSugar-batallanaval-en.jpg" width="600" height="440"/></p>
+<p>I suggest that you download the latest code for these two Activities from Gitorious using these commands:
+</p>
+<pre>mkdir scribble
+cd scribble
+<code>git clone git://git.sugarlabs.org/scribble/mainline.git</code>
+<code/>cd ..
+mkdir batallanaval
+cd batallanaval
+<code>git clone git://git.sugarlabs.org/batalla-naval/mainline.git</code></pre>
+<p>You'll need to do some setup work to get these running in sugar-emulator. Scribble requires the <strong>goocanvas</strong> GTK component and the Python bindings that go with it. These were not installed by default in Fedora 10 but I was able to install them using <strong>Add/Remove Programs</strong> from the <strong>System</strong> menu in GNOME. Batalla Naval is missing <strong>setup.py</strong>, but that's easily fixed since every setup.py is identical.&#160; Copy the one from the book examples into the <strong>mainline/BatallaNaval.activity</strong> directory and run <strong>./setup.py dev</strong> on both Activities.
+</p>
+<p> These Activities use different strategies for collaboration.&#160; Scribble creates lines of Python code which it passes to all Buddies and the Buddies use <strong>exec</strong> to run the commands.&#160; This is the code used for drawing a circle:
+</p>
+<pre> def process_item_finalize(self, x, y):
+ if self.tool == 'circle':
+ self.cmd = "goocanvas.Ellipse(
+ parent=self._root,
+ center_x=%d,
+ center_y=%d, radius_x = %d,
+ radius_y = %d,
+ fill_color_rgba = %d,
+ stroke_color_rgba = %d,
+ title = '%s')" % (self.item.props.center_x,
+ self.item.props.center_y,
+ self.item.props.radius_x,
+ self.item.props.radius_y,
+ self._fill_color,
+ self._stroke_color, self.item_id)
+...
+
+ def process_cmd(self, cmd):
+ #print 'Processing cmd :' + cmd
+ exec(cmd)
+ #FIXME: Ugly hack, but I'm too lazy to
+ # do this nicely
+
+ if len(self.cmd_list) &gt; 0:
+ self.cmd_list += (';' + cmd)
+ else:
+ self.cmd_list = cmd</pre>
+<p> The <strong>cmd_list</strong> variable is used to create a long string containing all of the commands executed so far.&#160; When a new Buddy joins the Activity she is sent this variable to execute so that her drawing area has the same contents as the other Buddies have.
+</p>
+<p>This is an interesting approach but you could do the same thing with the TextChannel so it isn't the best use of D-Bus Tubes.&#160; Batalla Naval's use of D-Bus is more typical.
+</p>
+<h2>How D-Bus Tubes Work, More Or Less
+ <br/></h2>
+<p>D-Bus enables you to have two running programs send messages to each other.&#160; The programs have to be running on the same computer.&#160; Sending a message is sort of a roundabout way of having one program run code in another.&#160; A program defines the kind of messages it is willing to receive and act on.&#160;&#160; In the case of Batalla Naval it defines a message "tell me what square you want to fire a shell at and I'll figure out if part of one of my ships is in that square and tell you."&#160; The first program doesn't actually run code in the second one, but the end result is similar.&#160; D-Bus Tubes is a way of making D-Bus able to send messages like this to a program running on another computer.
+ <br/></p>
+<p>Think for a minute about how you might make a program on one computer run code in a running program on a different computer.&#160; You'd have to use the network, of course.&#160; Everyone is familiar with sending data over a network, but in this case you would have to send program code over the network.&#160; You would need to be able to tell the running program on the second computer what code you wanted it to run.&#160; You would have to send it a method call and all the parameters you needed to pass into the method, and you'd need a way to get a return value back.
+</p>
+<p>Isn't that kind of like what <strong>Scribble</strong> is doing in the code we just looked at?&#160; Maybe we could make our code do something like that?
+</p>
+<p>Of course if you did that then every program you wanted to run code in remotely would have to be written to deal with that.&#160; If you had a bunch of programs you wanted to do that with you'd have to have some way of letting the programs know which requests were meant for it.&#160; It would be nice if there was a program running on each machine that dealt with making the network connections, converting method calls to data that could be sent over the network and then converting the data back into method calls and running them, plus sending any return values back.&#160; This program should be able to know which program you wanted to run code in and see that the method call is run there.&#160; The program should run all the time, and it would be really good if it made running a method on a remote program as simple as running a method in my own program.
+</p>
+<p>As you might guess, what I've just described is more or less what D-Bus Tubes are.&#160; There are articles explaining how it works in detail but it is not necessary to know how it works to use it.&#160; You do need to know about a few things, though.&#160; First, you need to know how to use D-Bus Tubes to make objects in your Activity available for use by other instances of that Activity running elsewhere.
+</p>
+<p>An Activity that needs to use D-Bus Tubes needs to define what sorts of messages it is willing to act on, in effect what specific methods in in the program are available for this use.&#160; All Activities that use D-Bus Tubes have constants like this:
+</p>
+<pre>SERVICE = "org.randomink.sayamindu.Scribble"
+IFACE = SERVICE
+PATH = "/org/randomink/sayamindu/Scribble"
+</pre>
+<p>These are the constants used for the <strong>Scribble</strong> Activity.&#160; The first constant, named SERVICE, represents the <strong>bus name</strong> of the Activity.&#160; This is also called a <strong>well-known name</strong> because it uses a <strong>reversed domain name</strong> as part of the name.&#160; In this case Sayamindu Dasgupta has a website at <a href="http://sayamindu.randomink.org">http://sayamindu.randomink.org</a>&#160; so he reverses the dot-separated words of that URL to create the first part of his bus name.&#160; It is not necessary to own a domain name before you can create a bus name.&#160; You can use org.sugarlabs.ActivityName if you like.&#160; The point is that the bus name must be unique, and by convention this is made easier by starting with a reversed domain name.
+</p>
+<p>The PATH constant represents the <strong>object path</strong>.&#160; It looks like the bus name with slashes separating the words rather than periods.&#160; For most Activities that is exactly what it should be, but it is possible for an application to expose more than one object to D-Bus and in that case each object exposed would have its own unique name, by convention words separated by slashes.
+</p>
+<p>The third constant is IFACE, which is the <strong>interface name</strong>.&#160; An interface is a collection of related methods and <strong>signals</strong>, identified by a name that uses the same convention used by the bus name.&#160; In the example above, and probably in most Activities using a D-Bus Tube, the interface name and the bus name are identical.
+</p>
+<p>So what is a signal?&#160; A signal is like a method but instead of one running program calling a method in one other running program, a signal is <strong>broadcast</strong>.&#160; In other words, instead of executing a method in just one program it executes the same method in many running programs, in fact in every running program that has that method that it is connected to through the D-Bus.&#160; A signal can pass data into a method call but it can't receive anything back as a return value.&#160; It's like a radio station that broadcasts music to anyone that is tuned in.&#160; The flow of information is one way only.
+</p>
+<p>Of course a radio station often receives phone calls from its listeners.&#160; A disc jockey might play a new song and invite listeners to call the station and say what they thought about it.&#160; The phone call is two way communication between the disc jockey and the listener, but it was initiated by a request that was broadcast to all listeners.&#160; In the same way your Activity might use a signal to invite all listeners (Buddies) to use a method to call it back, and that method can both supply and receive information.
+</p>
+<p>In D-Bus methods and signals have <strong>signatures</strong>.&#160; A signature is a description of the parameters passed into a method or signal including its <strong>data types</strong>.&#160; Python is not a <strong>strongly typed</strong> language.&#160; In a strongly typed language every variable has a data type that limits what it can do.&#160; Data types include such things as <strong>strings</strong>, <strong>integers</strong>, <strong>long integers</strong>, <strong>floating point numbers</strong>, <strong>booleans</strong>, etc.&#160; Each one can be used for a specific purpose only.&#160; For instance a boolean can only hold the values <strong>True</strong> and <strong>False</strong>, nothing else.&#160; A string can be used to hold strings of characters, but even if those characters represent a number you cannot use a string for calculations.&#160; Instead you need to convert the string into one of the numeric data types.&#160; An integer can hold integers up to a certain size, and a long integer can hold much larger integers,&#160; A floating point number is a number with a decimal point in scientific notation.&#160; It is almost useless for business arithmetic, which requires rounded results.
+</p>
+<p>In Python you can put anything into any variable and the language itself will figure out how to deal with it.&#160; To make Python work with D-Bus, which requires strongly typed variables that Python doesn't have, you need a way to tell D-Bus what types the variables you pass into a method should have.&#160; You do this by using a signature string as an argument to the method or signal.&#160; Methods have two strings: an <strong>in_signature</strong> and an <strong>out_signature</strong>.&#160; Signals just have a <strong>signature</strong> parameter.&#160; Some examples of signature strings:
+</p>
+<p>
+ </p><table border="1" cellpadding="1" cellspacing="1"><tbody><tr><td>ii</td>
+ <td>Two parameters, both integers</td>
+ </tr><tr><td>sss</td>
+ <td>Three parameters, all strings</td>
+ </tr><tr><td>ixd</td>
+ <td>Three parameters, an integer, a long integer, and a double precision floating point number.</td>
+ </tr><tr><td>a(ssiii)</td>
+ <td>An array where each element of the array is a tuple containing two strings and three integers.</td>
+ </tr></tbody></table><p>There is more information on signature strings in the dbus-python tutorial at <a href="http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html">http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html</a>.
+ <br/></p>
+<h2>Introducing Hello Mesh And Friends
+ <br/></h2>
+<p>If you study the source code of a few shared Activities you'll conclude that many of them contain nearly identical methods, as if they were all copied from the same source.&#160; In fact, more likely than not they were.&#160; The Activity <strong>Hello Mesh</strong> was created to be an example of how to use D-Bus Tubes in a shared Activity.&#160; It is traditional in programming textbooks to have the first example program be something that just prints the words "Hello World" to the console or displays the same words in a window.&#160; In that tradition <strong>Hello Mesh</strong> is a program that doesn't do all that much.&#160; You can find the code in Gitorious at <a href="http://git.sugarlabs.org/projects/hello-mesh">http://git.sugarlabs.org/projects/hello-mesh</a>.
+</p>
+<p><strong>Hello Mesh</strong> is widely copied because it demonstrates how to do things that all shared Activities need to do.&#160; When you have a shared Activity you need to be able to do two things:
+</p>
+<ul><li>Send information or commands to other instances of your Activity.</li>
+ <li>Give Buddies joining your Activity a copy of the current state of the Activity.</li>
+</ul><p>It does this using two signals and one method:
+</p>
+<ul><li>A signal called <em>Hello()</em> that someone joining the Activity sends to all participants.&#160; The <em>Hello()</em> method takes no parameters.</li>
+ <li>A method called <em>World()</em> which instances of the Activity receiving <em>Hello()</em> send back to the sender.&#160; This method takes a text string as an argument, which is meant to represent the current state of the Activity.</li>
+ <li>Another signal called <em>SendText()</em> which sends a text string to all participants.&#160; This represents updating the state of the shared Activity.&#160; In the case of <strong>Scribble</strong> this would be informing the others that this instance has just drawn a new shape.</li>
+</ul><p>Rather than study <strong>Hello Mesh</strong> itself I'd like to look at the code derived from it used in <strong>Batalla Naval</strong>.&#160; I have taken the liberty of running the comments, originally in Spanish, through <em>Google Translate</em> to make everything in English.&#160; I have also removed some commented-out lines of code.
+ <br/></p>
+<p>This Activity does something clever to make it possible to run it either as a Sugar Activity or as a standalone Python program.&#160; The standalone program does not support sharing at all, and it runs in a Window.&#160; The class <strong>Activity</strong> is a subclass of <strong>Window</strong>, so when the code is run standalone the <em>init()</em> function in <strong>BatallaNaval.py</strong> gets a Window, and when the same code is run as an Activity the instance of class <strong>BatallaNavalActivity</strong> is passed to <em>init()</em>:
+ <br/></p>
+<pre>from sugar.activity.activity import Activity, ActivityToolbox
+import BatallaNaval
+from Collaboration import CollaborationWrapper
+
+class BatallaNavalActivity(Activity):
+ ''' The Sugar class called when you run this
+ program as an Activity. The name of this
+ class file is marked in the
+ activity/activity.info file.'''
+
+ def __init__(self, handle):
+ Activity.__init__(self, handle)
+
+ self.gamename = 'BatallaNaval'
+
+ # Create the basic Sugar toolbar
+ toolbox = ActivityToolbox(self)
+ self.set_toolbox(toolbox)
+ toolbox.show()
+
+ # Create an instance of the CollaborationWrapper
+ # so you can share the activity.
+ self.colaboracion = CollaborationWrapper(self)
+
+ # The activity is a subclass of Window, so it
+ # passses itself to the init function
+ BatallaNaval.init(False, self)
+</pre>
+<p> The other clever thing going on here is that all the collaboration code is placed in its own <strong>CollaborationWrapper</strong> class which takes the instance of the <strong>BatallNavalActivity</strong> class in its constructor.&#160; This separates the collaboration code from the rest of the program.&#160; Here is the code in <strong>CollaborationWrapper.py</strong>:
+ <br/></p>
+<pre>import logging
+
+from sugar.presence import presenceservice
+import telepathy
+from dbus.service import method, signal
+# In build 656 Sugar lacks sugartubeconn
+try:
+ from sugar.presence.sugartubeconn import \
+ SugarTubeConnection
+except:
+ from sugar.presence.tubeconn import TubeConnection as \
+ SugarTubeConnection
+from dbus.gobject_service import ExportedGObject
+
+''' In all collaborative Activities in Sugar we are
+ made aware when a player enters or leaves. So that
+ everyone knows the state of the Activity we use
+ the methods Hello and World. When a participant
+ enters Hello sends a signal that reaches
+ all participants and the participants
+ respond directly using the method "World",
+ which retrieves the current state of the Activity.
+ After the updates are given then the signal
+ Play is used by each participant to make his move.
+ In short this module encapsulates the logic of
+ "collaboration" with the following effect:
+ - When someone enters the collaboration
+ the Hello signal is sent.
+ - Whoever receives the Hello signal responds
+ with World
+ - Every time someone makes a move he uses
+ the method Play giving a signal which
+ communicates to each participant
+ what his move was.
+'''
+
+SERVICE = "org.ceibaljam.BatallaNaval"
+IFACE = SERVICE
+PATH = "/org/ceibaljam/BatallaNaval"
+
+logger = logging.getLogger('BatallaNaval')
+logger.setLevel(logging.DEBUG)
+
+class CollaborationWrapper(ExportedGObject):
+ ''' A wrapper for the collaboration methods.
+ Get the activity and the necessary callbacks.
+ '''
+
+ def __init__(self, activity):
+ self.activity = activity
+ self.presence_service = \
+ presenceservice.get_instance()
+ self.owner = \
+ self.presence_service.get_owner()
+
+ def set_up(self, buddy_joined_cb, buddy_left_cb,
+ World_cb, Play_cb, my_boats):
+ self.activity.connect('shared',
+ self._shared_cb)
+ if self.activity._shared_activity:
+ # We are joining the activity
+ self.activity.connect('joined',
+ self._joined_cb)
+ if self.activity.get_shared():
+ # We've already joined
+ self._joined_cb()
+
+ self.buddy_joined = buddy_joined_cb
+ self.buddy_left = buddy_left_cb
+ self.World_cb = World_cb
+ # Called when someone passes the board state.
+ self.Play_cb = Play_cb
+ # Called when someone makes a move.
+
+ # Submitted by making World on a new partner
+ self.my_boats = [(b.nombre, b.orientacion,
+ b.largo, b.pos[0],
+ b.pos[1]) for b in my_boats]
+ self.world = False
+ self.entered = False
+
+ def _shared_cb(self, activity):
+ self._sharing_setup()
+ self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].\
+ OfferDBusTube(
+ SERVICE, {})
+ self.is_initiator = True
+
+ def _joined_cb(self, activity):
+ self._sharing_setup()
+ self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].\
+ ListTubes(
+ reply_handler=self._list_tubes_reply_cb,
+ error_handler=self._list_tubes_error_cb)
+ self.is_initiator = False
+
+ def _sharing_setup(self):
+ if self.activity._shared_activity is None:
+ logger.error(
+ 'Failed to share or join activity')
+ return
+
+ self.conn = \
+ self.activity._shared_activity.telepathy_conn
+ self.tubes_chan = \
+ self.activity._shared_activity.telepathy_tubes_chan
+ self.text_chan = \
+ self.activity._shared_activity.telepathy_text_chan
+
+ self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].\
+ connect_to_signal(
+ 'NewTube', self._new_tube_cb)
+
+ self.activity._shared_activity.connect(
+ 'buddy-joined',
+ self._buddy_joined_cb)
+ self.activity._shared_activity.connect(
+ 'buddy-left',
+ self._buddy_left_cb)
+
+ # Optional - included for example:
+ # Find out who's already in the shared activity:
+ for buddy in \
+ self.activity._shared_activity.\
+ get_joined_buddies():
+ logger.debug(
+ 'Buddy %s is already in the activity',
+ buddy.props.nick)
+
+ def participant_change_cb(self, added, removed):
+ logger.debug(
+ 'Tube: Added participants: %r', added)
+ logger.debug(
+ 'Tube: Removed participants: %r', removed)
+ for handle, bus_name in added:
+ buddy = self._get_buddy(handle)
+ if buddy is not None:
+ logger.debug(
+ 'Tube: Handle %u (Buddy %s) was added',
+ handle, buddy.props.nick)
+ for handle in removed:
+ buddy = self._get_buddy(handle)
+ if buddy is not None:
+ logger.debug('Buddy %s was removed' %
+ buddy.props.nick)
+ if not self.entered:
+ if self.is_initiator:
+ logger.debug(
+ "I'm initiating the tube, "
+ "will watch for hellos.")
+ self.add_hello_handler()
+ else:
+ logger.debug(
+ 'Hello, everyone! What did I miss?')
+ self.Hello()
+ self.entered = True
+
+
+ # This is sent to all participants whenever we
+ # join an activity
+ @signal(dbus_interface=IFACE, signature='')
+ def Hello(self):
+ """Say Hello to whoever else is in the tube."""
+ logger.debug('I said Hello.')
+
+ # This is called by whoever receives our Hello signal
+ # This method receives the current game state and
+ # puts us in sync with the rest of the participants.
+ # The current game state is represented by the
+ # game object
+ @method(dbus_interface=IFACE, in_signature='a(ssiii)',
+ out_signature='a(ssiii)')
+ def World(self, boats):
+ """To be called on the incoming XO after
+ they Hello."""
+ if not self.world:
+ logger.debug('Somebody called World on me')
+ self.world = True # Instead of loading
+ # the world, I am
+ # receiving play by
+ # play.
+ self.World_cb(boats)
+ # now I can World others
+ self.add_hello_handler()
+ else:
+ self.world = True
+ logger.debug(
+ "I've already been welcomed, doing nothing")
+ return self.my_boats
+
+ @signal(dbus_interface=IFACE, signature='ii')
+ def Play(self, x, y):
+ """Say Hello to whoever else is in the tube."""
+ logger.debug('Running remote play:%s x %s.', x, y)
+
+ def add_hello_handler(self):
+ logger.debug('Adding hello handler.')
+ self.tube.add_signal_receiver(self.hello_signal_cb,
+ 'Hello', IFACE,
+ path=PATH, sender_keyword='sender')
+ self.tube.add_signal_receiver(self.play_signal_cb,
+ 'Play', IFACE,
+ path=PATH, sender_keyword='sender')
+
+ def hello_signal_cb(self, sender=None):
+ """Somebody Helloed me. World them."""
+ if sender == self.tube.get_unique_name():
+ # sender is my bus name, so ignore my own signal
+ return
+ logger.debug('Newcomer %s has joined', sender)
+ logger.debug(
+ 'Welcoming newcomer and sending them '
+ 'the game state')
+
+ self.other = sender
+
+ # I send my ships and I get theirs in return
+ enemy_boats = self.tube.get_object(self.other,
+ PATH).World(
+ self.my_boats, dbus_interface=IFACE)
+
+ # I call the callback World, to load the enemy ships
+ self.World_cb(enemy_boats)
+
+ def play_signal_cb(self, x, y, sender=None):
+ """Somebody placed a stone. """
+ if sender == self.tube.get_unique_name():
+ return # sender is my bus name,
+ # so ignore my own signal
+ logger.debug('Buddy %s placed a stone at %s x %s',
+ sender, x, y)
+ # Call our Play callback
+ self.Play_cb(x, y)
+ # In theory, no matter who sent him
+
+ def _list_tubes_error_cb(self, e):
+ logger.error('ListTubes() failed: %s', e)
+
+ def _list_tubes_reply_cb(self, tubes):
+ for tube_info in tubes:
+ self._new_tube_cb(*tube_info)
+
+ def _new_tube_cb(self, id, initiator, type,
+ service, params, state):
+ logger.debug('New tube: ID=%d initator=%d '
+ 'type=%d service=%s '
+ 'params=%r state=%d', id, initiator, '
+ 'type, service, params, state)
+ if (type == telepathy.TUBE_TYPE_DBUS and
+ service == SERVICE):
+ if state == telepathy.TUBE_STATE_LOCAL_PENDING:
+ self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES]
+ .AcceptDBusTube(id)
+ self.tube = SugarTubeConnection(self.conn,
+ self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES],
+ id, group_iface=
+ self.text_chan[telepathy.\
+ CHANNEL_INTERFACE_GROUP])
+ super(CollaborationWrapper,
+ self).__init__(self.tube, PATH)
+ self.tube.watch_participants(
+ self.participant_change_cb)
+
+ def _buddy_joined_cb (self, activity, buddy):
+ """Called when a buddy joins the shared
+ activity. """
+ logger.debug(
+ 'Buddy %s joined', buddy.props.nick)
+ if self.buddy_joined:
+ self.buddy_joined(buddy)
+
+ def _buddy_left_cb (self, activity, buddy):
+ """Called when a buddy leaves the shared
+ activity. """
+ if self.buddy_left:
+ self.buddy_left(buddy)
+
+ def _get_buddy(self, cs_handle):
+ """Get a Buddy from a channel specific handle."""
+ logger.debug('Trying to find owner of handle %u...',
+ cs_handle)
+ group = self.text_chan[telepathy.\
+ CHANNEL_INTERFACE_GROUP]
+ my_csh = group.GetSelfHandle()
+ logger.debug(
+ 'My handle in that group is %u', my_csh)
+ if my_csh == cs_handle:
+ handle = self.conn.GetSelfHandle()
+ logger.debug('CS handle %u belongs to me, %u',
+ cs_handle, handle)
+ elif group.GetGroupFlags() &amp; \
+ telepathy.\
+ CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES:
+ handle = group.GetHandleOwners([cs_handle])[0]
+ logger.debug('CS handle %u belongs to %u',
+ cs_handle, handle)
+ else:
+ handle = cs_handle
+ logger.debug('non-CS handle %u belongs to itself',
+ handle)
+ # XXX: deal with failure to get the handle owner
+ assert handle != 0
+ return self.presence_service.\
+ get_buddy_by_telepathy_handle(
+ self.conn.service_name,
+ self.conn.object_path, handle)
+</pre>
+<p> Most of the code above is similar to what we've seen in the other examples, and most of it can be used as is in any Activity that needs to make D-Bus calls.&#160; For this reason we'll focus on the code that is specific to using D-Bus.&#160; The logical place to start is the <em>Hello()</em> method.&#160; There is of course nothing magic about the name "Hello".&#160; <strong>Hello Mesh</strong> is meant to be a "Hello World" program for using D-Bus Tubes, so by convention the words "Hello" and "World" had to be used for <em>something</em>.&#160; The <em>Hello()</em> method is broadcast to all instances of the Activity to inform them that a new instance is ready to receive information about the current state of the shared Activity.&#160; Your own Activity will probably need something similar, but you should feel free to name it something else, and if you're writing the code for a school assignment you should definitely name it something else:
+</p>
+<pre> # This is sent to all participants whenever we
+ # join an activity
+ @signal(dbus_interface=IFACE, signature='')
+ def Hello(self):
+ """Say Hello to whoever else is in the tube."""
+ logger.debug('I said Hello.')
+
+ def add_hello_handler(self):
+ logger.debug('Adding hello handler.')
+ self.tube.add_signal_receiver(
+ self.hello_signal_cb,
+ 'Hello', IFACE,
+ path=PATH, sender_keyword='sender')
+...
+
+ def hello_signal_cb(self, sender=None):
+ """Somebody Helloed me. World them."""
+ if sender == self.tube.get_unique_name():
+ # sender is my bus name,
+ # so ignore my own signal
+ return
+ logger.debug('Newcomer %s has joined', sender)
+ logger.debug(
+ 'Welcoming newcomer and sending them '
+ 'the game state')
+
+ self.other = sender
+
+ # I send my ships and I returned theirs
+ enemy_boats = self.tube.get_object(
+ self.other, PATH).World(
+ self.my_boats, dbus_interface=IFACE)
+
+ # I call the callback World, to load the enemy ships
+ self.World_cb(enemy_boats)
+</pre>
+<p>&#160;The most interesting thing about this code is this line, which Python calls a <strong>Decorator</strong>:
+</p>
+<pre> @signal(dbus_interface=IFACE, signature='')
+</pre>
+<p> When you put <strong>@signal</strong> in front of a method name it has the effect of adding the two parameters shown to the method call whenever it is invoked, in effect changing it from a normal method call to a D-Bus call for a signal.&#160; The <strong>signature</strong> parameter is an empty string, indicating that the method call has no parameters.&#160; The <em>Hello()</em> method does nothing at all locally but when it is received by the other instances of the Activity it causes them to execute the <em>World()</em> method, which sends back the location of their boats and gets the new participants boats in return.
+</p>
+<p><strong>Batalla Naval</strong> is apparently meant to be a demonstration program.&#160; <em>Battleship</em> is a game for two players, but there is nothing in the code to prevent more players from joining and no way to handle it if they do.&#160; Ideally you would want code to make only the first joiner an actual player and make the rest only spectators.
+</p>
+<p>Next we'll look at the <em>World()</em> method:
+</p>
+<pre> # This is called by whoever receives our Hello signal
+ # This method receives the current game state and
+ # puts us in sync with the rest of the participants.
+ # The current game state is represented by the game
+ # object
+ @method(dbus_interface=IFACE, in_signature='a(ssiii)',
+ out_signature='a(ssiii)')
+ def World(self, boats):
+ """To be called on the incoming XO after
+ they Hello."""
+ if not self.world:
+ logger.debug('Somebody called World on me')
+ self.world = True # Instead of loading the world,
+ # I am receiving play by play.
+ self.World_cb(boats)
+ # now I can World others
+ self.add_hello_handler()
+ else:
+ self.world = True
+ logger.debug("I've already been welcomed, "
+ "doing nothing")
+ return self.my_boats
+</pre>
+<p>There is another decorator here, this one converting the <em>World()</em> method to a D-Bus call for a method.&#160; The signature is more interesting than <em>Hello()</em> had.&#160; It means an array of tuples where each tuple contains two strings and three integers.&#160; Each element in the array represents one ship and its attributes.&#160; <em>World_cb</em> is set to point to a method in <strong>BatallaNaval.py</strong>, (and so is <em>Play_cb</em>).&#160; If you study the <em>init()</em> code in <strong>BatallaNaval.py</strong> you'll see how this happens.&#160; <em>World()</em> is called in the <em>hello_signal_cb()</em> method we just looked at.&#160; It is sent to the joiner who sent <em>Hello()</em> to us.
+ <br/></p>
+<p>Finally we'll look at the <em>Play()</em> signal:
+</p>
+<pre> @signal(dbus_interface=IFACE, signature='ii')
+ def Play(self, x, y):
+ """Say Hello to whoever else is in the tube."""
+ logger.debug('Running remote play:%s x %s.', x, y)
+
+ def add_hello_handler(self):
+...
+ self.tube.add_signal_receiver(self.play_signal_cb,
+ 'Play', IFACE,
+ path=PATH, sender_keyword='sender')
+...
+ def play_signal_cb(self, x, y, sender=None):
+ """Somebody placed a stone. """
+ if sender == self.tube.get_unique_name():
+ return # sender is my bus name, so
+ # ignore my own signal
+ logger.debug('Buddy %s placed a stone at %s x %s',
+ sender, x, y)
+ # Call our Play callback
+ self.Play_cb(x, y)
+</pre>
+<p>This is a signal so there is only one signature string, this one indicating that the input parameters are two integers.
+</p>
+<p>There are several ways you could improve this Activity.&#160; When playing against the computer in non-sharing mode the game just makes random moves.&#160; The game does not limit the players to two and make the rest of the joiners spectators.&#160; It does not make the players take turns.&#160; When a player succeeds in sinking all the other players ships nothing happens to mark the event.&#160; Finally, <em>gettext()</em> is not used for the text strings displayed by the Activity so it cannot be translated into languages other than Spanish.
+</p>
+<p>In the tradition of textbooks everywhere I will leave making these improvements as an exercise for the student.
+</p>
+<p>
+ <br/></p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch018_adding-text-to-speech.xhtml b/app/static/doc/myosa/ch018_adding-text-to-speech.xhtml
new file mode 100644
index 0000000..b56bbb7
--- /dev/null
+++ b/app/static/doc/myosa/ch018_adding-text-to-speech.xhtml
@@ -0,0 +1,695 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>Adding Text To Speech
+</h1>
+<h2>Introduction
+</h2>
+<p>Certainly one of the most popular Activities available is <strong>Speak</strong>, which takes the words you type in and speaks them out loud, at the same time displaying a cartoon face that seems to be speaking the words.&#160; You might be surprised to learn how little of the code in that Activity is used to get the words spoken.&#160;&#160; If your Activity could benefit from having words spoken out loud (the possibilities for educational Activities and games are definitely there) this chapter will teach you how to make it happen.
+</p>
+<h2><img alt="SpeakActivity.png" src="static/ActivitiesGuideSugar-SpeakActivity-en.png" height="450" width="600"/></h2>
+<h2>We Have Ways To Make You Talk
+</h2>
+<p>A couple of ways, actually, and neither one is that painful.&#160; They are:
+</p>
+<ul><li>Running the <strong>espeak</strong> program directly</li>
+ <li>Using the <strong>gstreamer espeak plugin</strong></li>
+</ul><p>Both approaches have their advantages.&#160; The first one is the one used by Speak.&#160; (Technically, Speak uses the gstreamer plugin if it is available, and otherwise executes espeak directly.&#160; For what Speak is doing using the gstreamer plugin isn't really needed).&#160; Executing espeak is definitely the simplest method, and may be suitable for your own Activity.&#160; Its big advantage is that you do not need to have the gstreamer plugin installed.&#160; If your Activity needs to run on something other than the latest version of Sugar this will be something to consider.
+ <br/></p>
+<p>The gstreamer plugin is what is used by <strong>Read Etexts</strong> to do text to speech with highlighting.&#160; For this application we needed to be able to do things that are not possible by just running <strong>espeak</strong>.&#160; For example:
+</p>
+<ul><li>We needed to be able to pause and resume speech, because the Activity needs to speak a whole page worth of text, not just simple phrases.</li>
+ <li>We needed to highlight the words being spoken as they are spoken.</li>
+</ul><p>You might think that you could achieve these objectives by running espeak on one word at a time.&#160; If you do, don't feel bad because I thought that too.&#160; On a fast computer it sounds really awful, like HAL 9000 developing a stutter towards the end of being deactivated.&#160; On the XO no sounds came out at all.
+</p>
+<p> Originally Read Etexts used <strong>speech-dispatcher</strong> to do what the gstreamer plugin does.&#160; The developers of that program were very helpful in getting the highlighting in Read Etexts working, but speech-dispatcher needed to be configured before you could use it which was an issue for us.&#160; (There is more than one kind of text to speech software available and speech-dispatcher supports most of them.&#160; This makes configuration files inevitable).&#160; Aleksey Lim of Sugar Labs came up with the idea of using a gstreamer plugin and was the one who wrote it.&#160; He also rewrote much of <strong>Read Etexts</strong> so it would use the plugin if it was available, use speech-dispatcher if not, and would not support speech if neither was available.
+</p>
+<h2> Running espeak Directly
+</h2>
+<p>You can run the <strong>espeak</strong> program from the terminal to try out its options.&#160; To see what options are available for espeak you can use the <strong>man</strong> command:
+</p>
+<pre>man espeak</pre>
+<p>This will give you a manual page describing how to run the program and what options are available.&#160; The parts of the man page that are most interesting to us are these:
+</p>
+<pre><strong>NAME</strong>
+ espeak - A multi-lingual software speech synthesizer.
+
+<strong>SYNOPSIS</strong>
+ espeak [options] [&lt;words&gt;]
+
+<strong>DESCRIPTION</strong>
+ espeak is a software speech synthesizer for English,
+ and some other languages.
+
+<strong>OPTIONS</strong>
+ -p &lt;integer&gt;
+ Pitch adjustment, 0 to 99, default is 50
+
+ -s &lt;integer&gt;
+ Speed in words per minute, default is 160
+
+ -v &lt;voice name&gt;
+ Use voice file of this name from
+ espeak-data/voices
+
+ --voices[=&lt;language code&gt;]
+ Lists the available voices. If =&lt;language code&gt;
+ is present then only those voices which are
+ suitable for that language are listed.
+</pre>
+<p>Let's try out some of these options. First let's get a list of <strong>Voices</strong>:
+</p>
+<pre><strong>espeak --voices</strong>
+Pty Language Age/Gender VoiceName File Other Langs
+ 5 af M afrikaans af
+ 5 bs M bosnian bs
+ 5 ca M catalan ca
+ 5 cs M czech cs
+ 5 cy M welsh-test cy
+ 5 de M german de
+ 5 el M greek el
+ 5 en M default default
+ 5 en-sc M en-scottish en/en-sc (en 4)
+ 2 en-uk M english en/en (en 2)
+<em>... and many more ...</em>
+</pre>
+<p>Now that we know what the names of the voices are we can try them out. How about English with a French accent?
+</p>
+<pre>espeak "Your mother was a hamster and your father \
+smelled of elderberries." -v fr
+</pre>
+<p>Let's try experimenting with rate and pitch:
+</p>
+<pre>espeak "I'm sorry, Dave. I'm afraid I can't \
+do that." -s 120 -p 30
+</pre>
+<p>The next thing to do is to write some Python code to run espeak.&#160; Here is a short program adapted from the code in <strong>Speak</strong>:
+</p>
+<pre>import re
+import subprocess
+
+PITCH_MAX = 99
+RATE_MAX = 99
+PITCH_DEFAULT = PITCH_MAX/2
+RATE_DEFAULT = RATE_MAX/3
+
+def speak(text, rate=RATE_DEFAULT, pitch=PITCH_DEFAULT,
+ voice="default"):
+
+ # espeak uses 80 to 370
+ rate = 80 + (370-80) * int(rate) / 100
+
+ subprocess.call(["espeak", "-p", str(pitch),
+ "-s", str(rate), "-v", voice, text],
+ stdout=subprocess.PIPE)
+
+def voices():
+ out = []
+ result = subprocess.Popen(["espeak", "--voices"],
+ stdout=subprocess.PIPE).communicate()[0]
+
+ for line in result.split('\n'):
+ m = re.match(
+ r'\s*\d+\s+([\w-]+)\s+([MF])\s+([\w_-]+)\s+(.+)',
+ line)
+ if not m:
+ continue
+ language, gender, name, stuff = m.groups()
+ if stuff.startswith('mb/') or \
+ name in ('en-rhotic','english_rp',
+ 'english_wmids'):
+ # these voices don't produce sound
+ continue
+ out.append((language, name))
+
+ return out
+
+def main():
+ print voices()
+ speak("I'm afraid I can't do that, Dave.")
+ speak("Your mother was a hamster, and your father "
+ + "smelled of elderberries!", 30, 60, "fr")
+
+if __name__ == "__main__":
+ main()
+</pre>
+<p>In the Git repository in the directory <strong>Adding_TTS</strong> this file is named <strong>espeak.py</strong>.&#160; Load this file into <strong>Eric</strong> and do <strong>Run Script</strong> from the <strong>Start</strong> menu to run it.&#160; In addition to hearing speech you should see this text:
+</p>
+<p><em>[('af', 'afrikaans'), ('bs', 'bosnian'), ('ca', 'catalan'), ('cs', 'czech'), ('cy', 'welsh-test'), ('de', 'german'), ('el', 'greek'), ('en', 'default'), ('en-sc', 'en-scottish'), ('en-uk', 'english'), ('en-uk-north', 'lancashire'), ('en-us', 'english-us'), ('en-wi', 'en-westindies'), ('eo', 'esperanto'), ('es', 'spanish'), ('es-la', 'spanish-latin-american'), ('fi', 'finnish'), ('fr', 'french'), ('fr-be', 'french'), ('grc', 'greek-ancient'), ('hi', 'hindi-test'), ('hr', 'croatian'), ('hu', 'hungarian'), ('hy', 'armenian'), ('hy', 'armenian-west'), ('id', 'indonesian-test'), ('is', 'icelandic-test'), ('it', 'italian'), ('ku', 'kurdish'), ('la', 'latin'), ('lv', 'latvian'), ('mk', 'macedonian-test'), ('nl', 'dutch-test'), ('no', 'norwegian-test'), ('pl', 'polish'), ('pt', 'brazil'), ('pt-pt', 'portugal'), ('ro', 'romanian'), ('ru', 'russian_test'), ('sk', 'slovak'), ('sq', 'albanian'), ('sr', 'serbian'), ('sv', 'swedish'), ('sw', 'swahihi-test'), ('ta', 'tamil'), ('tr', 'turkish'), ('vi', 'vietnam-test'), ('zh', 'Mandarin'), ('zh-yue', 'cantonese-test')] </em>
+</p>
+<p>The <em>voices()</em> function returns a list of voices as one tuple per voice, and eliminates voices from the list that espeak cannot produce on its own.&#160; This list of tuples can be used to populate a drop down list.
+ <br/></p>
+<p>The <em>speak()</em> function adjusts the value of <strong>rate</strong> so you can input a value between 0 and 99 rather than between 80 to 370.&#160; <em>speak()</em> is more complex in the Speak Activity than what we have here because in that Activity it monitors the spoken audio and generates mouth movements based on the amplitude of the voice.&#160; Making the face move is most of what the Speak Activity does, and since we aren't doing that we need very little code to make our Activity speak.
+</p>
+<p>You can use <strong>import espeak</strong> to include this file in your own Activities.
+</p>
+<h2>Using The gstreamer espeak Plugin
+</h2>
+<p>The gstreamer espeak plugin can be installed in <strong>Fedora 10</strong> or later using <strong>Add/Remove Software</strong>.
+</p>
+<p><img alt="Installing the plugin." src="static/ActivitiesGuideSugar-espeak-en.jpg" height="363" width="600"/></p>
+<p> When you have this done you should be able to download the <strong>Read Etexts</strong> Activity (the real one, not the simplified version we're using for the book) from ASLO and try out the <strong>Speech</strong> tab.&#160; You should do that now.&#160; It will look something like this:
+</p>
+<p><img alt="espeak2_1.jpg" src="static/ActivitiesGuideSugar-espeak2_1-en.jpg" height="415" width="600"/><br/></p>
+<p>The book used in the earlier screenshots of this manual was <em>Pride and Prejudice</em> by Jane Austen.&#160; To balance things out the rest of the screen shots will be using <em>The Innocents Abroad</em> by Mark Twain.
+</p>
+<p><strong>Gstreamer</strong> is a framework for multimedia.&#160; If you've watched videos on the web you are familiar with the concept of streaming media.&#160; Instead of downloading a whole song or a whole movie clip and then playing it, streaming means the downloading and the playing happen at the same time, with the downloading just a bit behind the playing.&#160; There are many different kinds of media files: MP3's, DivX, WMV, Real Media, and so on.&#160; For every kind of media file Gstreamer has a plugin.
+</p>
+<p>Gstreamer makes use of a concept called <strong>pipelining</strong>.&#160; The idea is that the output of one program can become the input to another.&#160; One way to handle that situation is to put the output of the first program into a temporary file and have the second program read it.&#160; This would mean that the first program would have to finish running before the second one could begin.&#160; What if you could have both programs run at the same time and have the second program read the data as the first one wrote it out?&#160; It's possible, and the mechanism for getting data from one program to the other is called a <strong>pipe</strong>.&#160; A collection of programs joined together in this way is called a <strong>pipeline</strong>.&#160; The program that feeds data into a pipe is called a <strong>source</strong>, and the data that takes the data out of the pipe is called a <strong>sink</strong>.
+ <br/></p>
+<p>The gstreamer espeak plugin uses a simple pipe: text goes into espeak at one end and sound comes out the other and is sent to your soundcard.&#160; You might think that doesn't sound much different from what we were doing before, but it is.&#160; When you just run espeak the program has to load itself into memory, speak the text you give it into the sound card, then unload itself.&#160; This is one of the reasons you can't just use espeak a word at a time to achieve speech with highlighted words.&#160; There is a short lag while the program is loading.&#160; It isn't that noticeable if you give espeak a complete phrase or sentence to speak, but if it happens for every word it is <em>very</em> noticeable.&#160; Using the gstreamer plugin we can have espeak loaded into memory all the time, just waiting for us to send some words into its input pipe.&#160; It will speak them and then wait for the next batch.
+</p>
+<p>Since we can control what goes into the pipe it is possible to pause and resume speech.
+</p>
+<p> The example we'll use here is a version of <strong>Read Etexts</strong> again, but instead of the Activity we're going to modify the standalone version.&#160; There is nothing special about the gstreamer plugin that makes it only work with Activities.&#160; Any Python program can use it.&#160; I'm only including Text to Speech as a topic in this manual because every Sugar installation includes espeak and many Activities could find it useful.
+</p>
+<p>There is a in our Git repository named <strong>speech.py</strong> which looks like this:
+</p>
+<pre>import gst
+
+voice = 'default'
+pitch = 0
+
+rate = -20
+highlight_cb = None
+
+def _create_pipe():
+ pipeline = 'espeak name=source ! autoaudiosink'
+ pipe = gst.parse_launch(pipeline)
+
+ def stop_cb(bus, message):
+ pipe.set_state(gst.STATE_NULL)
+
+ def mark_cb(bus, message):
+ if message.structure.get_name() == 'espeak-mark':
+ mark = message.structure['mark']
+ highlight_cb(int(mark))
+
+ bus = pipe.get_bus()
+ bus.add_signal_watch()
+ bus.connect('message::eos', stop_cb)
+ bus.connect('message::error', stop_cb)
+ bus.connect('message::element', mark_cb)
+
+ return (pipe.get_by_name('source'), pipe)
+
+def _speech(source, pipe, words):
+ source.props.pitch = pitch
+ source.props.rate = rate
+ source.props.voice = voice
+ source.props.text = words;
+ pipe.set_state(gst.STATE_PLAYING)
+
+info_source, info_pipe = _create_pipe()
+play_source, play_pipe = _create_pipe()
+
+# track for marks
+play_source.props.track = 2
+
+def voices():
+ return info_source.props.voices
+
+def say(words):
+ _speech(info_source, info_pipe, words)
+ print words
+
+def play(words):
+ _speech(play_source, play_pipe, words)
+
+def is_stopped():
+ for i in play_pipe.get_state():
+ if isinstance(i, gst.State) and \
+ i == gst.STATE_NULL:
+ return True
+ return False
+
+def stop():
+ play_pipe.set_state(gst.STATE_NULL)
+
+def is_paused():
+ for i in play_pipe.get_state():
+ if isinstance(i, gst.State) and \
+ i == gst.STATE_PAUSED:
+ return True
+ return False
+
+def pause():
+ play_pipe.set_state(gst.STATE_PAUSED)
+
+def rate_up():
+ global rate
+ rate = min(99, rate + 10)
+
+def rate_down():
+ global rate
+ rate = max(-99, rate - 10)
+
+def pitch_up():
+ global pitch
+ pitch = min(99, pitch + 10)
+
+def pitch_down():
+ global pitch
+ pitch = max(-99, pitch - 10)
+
+def prepare_highlighting(label_text):
+ i = 0
+ j = 0
+ word_begin = 0
+ word_end = 0
+ current_word = 0
+ word_tuples = []
+ omitted = [' ', '\n', u'\r', '_', '[', '{', ']',\
+ '}', '|', '&lt;', '&gt;', '*', '+', '/', '\\' ]
+ omitted_chars = set(omitted)
+ while i &lt; len(label_text):
+ if label_text[i] not in omitted_chars:
+ word_begin = i
+ j = i
+ while j &lt; len(label_text) and \
+ label_text[j] not in omitted_chars:
+ j = j + 1
+ word_end = j
+ i = j
+ word_t = (word_begin, word_end, \
+ label_text[word_begin: word_end].strip())
+ if word_t[2] != u'\r':
+ word_tuples.append(word_t)
+ i = i + 1
+ return word_tuples
+
+def add_word_marks(word_tuples):
+ "Adds a mark between each word of text."
+ i = 0
+ marked_up_text = '&lt;speak&gt; '
+ while i &lt; len(word_tuples):
+ word_t = word_tuples[i]
+ marked_up_text = marked_up_text + \
+ '&lt;mark name="' + str(i) + '"/&gt;' + word_t[2]
+ i = i + 1
+ return marked_up_text + '&lt;/speak&gt;'</pre>
+<p>There is another file named <strong>ReadEtextsTTS.py</strong> which looks like this:
+ <br/></p>
+<pre>import sys
+import os
+import zipfile
+import pygtk
+import gtk
+import getopt
+import pango
+import gobject
+import time
+import speech
+
+speech_supported = True
+
+try:
+ import gst
+ gst.element_factory_make('espeak')
+ print 'speech supported!'
+except Exception, e:
+ speech_supported = False
+ print 'speech not supported!'
+
+page=0
+PAGE_SIZE = 45
+
+class ReadEtextsActivity():
+ def __init__(self):
+ "The entry point to the Activity"
+ speech.highlight_cb = self.highlight_next_word
+ # print speech.voices()
+
+ def highlight_next_word(self, word_count):
+ if word_count &lt;&#8286; len(self.word_tuples):
+ word_tuple = self.word_tuples[word_count]
+ textbuffer = self.textview.get_buffer()
+ tag = textbuffer.create_tag()
+ tag.set_property('weight', pango.WEIGHT_BOLD)
+ tag.set_property( 'foreground', "white")
+ tag.set_property( 'background', "black")
+ iterStart = \
+ textbuffer.get_iter_at_offset(word_tuple[0])
+ iterEnd = \
+ textbuffer.get_iter_at_offset(word_tuple[1])
+ bounds = textbuffer.get_bounds()
+ textbuffer.remove_all_tags(bounds[0], bounds[1])
+ textbuffer.apply_tag(tag, iterStart, iterEnd)
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ max = v_adjustment.upper - \
+ v_adjustment.page_size
+ max = max * word_count
+ max = max / len(self.word_tuples)
+ v_adjustment.value = max
+ return True
+
+ def keypress_cb(self, widget, event):
+ "Respond when the user presses one of the arrow keys"
+ global done
+ global speech_supported
+ keyname = gtk.gdk.keyval_name(event.keyval)
+ if keyname == 'KP_End' and speech_supported:
+ if speech.is_paused() or speech.is_stopped():
+ speech.play(self.words_on_page)
+ else:
+ speech.pause()
+ return True
+ if keyname == 'plus':
+ self.font_increase()
+ return True
+ if keyname == 'minus':
+ self.font_decrease()
+ return True
+ if speech_supported and speech.is_stopped() == False \
+ and speech.is_paused == False:
+ # If speech is in progress, ignore other keys.
+ return True
+ if keyname == '7':
+ speech.pitch_down()
+ speech.say('Pitch Adjusted')
+ return True
+ if keyname == '8':
+ speech.pitch_up()
+ speech.say('Pitch Adjusted')
+ return True
+ if keyname == '9':
+ speech.rate_down()
+ speech.say('Rate Adjusted')
+ return True
+ if keyname == '0':
+ speech.rate_up()
+ speech.say('Rate Adjusted')
+ return True
+ if keyname == 'KP_Right':
+ self.page_next()
+ return True
+ if keyname == 'Page_Up' or keyname == 'KP_Up':
+ self.page_previous()
+ return True
+ if keyname == 'KP_Left':
+ self.page_previous()
+ return True
+ if keyname == 'Page_Down' or keyname == 'KP_Down':
+ self.page_next()
+ return True
+ if keyname == 'Up':
+ self.scroll_up()
+ return True
+ if keyname == 'Down':
+ self.scroll_down()
+ return True
+ return False
+
+ def page_previous(self):
+ global page
+ page=page-1
+ if page &lt; 0: page=0
+ self.show_page(page)
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ v_adjustment.value = v_adjustment.upper - \
+ v_adjustment.page_size
+
+ def page_next(self):
+ global page
+ page=page+1
+ if page &gt;= len(self.page_index): page=0
+ self.show_page(page)
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ v_adjustment.value = v_adjustment.lower
+
+ def font_decrease(self):
+ font_size = self.font_desc.get_size() / 1024
+ font_size = font_size - 1
+ if font_size &lt; 1:
+ font_size = 1
+ self.font_desc.set_size(font_size * 1024)
+ self.textview.modify_font(self.font_desc)
+
+ def font_increase(self):
+ font_size = self.font_desc.get_size() / 1024
+ font_size = font_size + 1
+ self.font_desc.set_size(font_size * 1024)
+ self.textview.modify_font(self.font_desc)
+
+ def scroll_down(self):
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ if v_adjustment.value == v_adjustment.upper - \
+ v_adjustment.page_size:
+ self.page_next()
+ return
+ if v_adjustment.value &lt; v_adjustment.upper - \
+ v_adjustment.page_size:
+ new_value = v_adjustment.value + \
+ v_adjustment.step_increment
+ if new_value &gt; v_adjustment.upper - \
+ v_adjustment.page_size:
+ new_value = v_adjustment.upper - \
+ v_adjustment.page_size
+ v_adjustment.value = new_value
+
+ def scroll_up(self):
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ if v_adjustment.value == v_adjustment.lower:
+ self.page_previous()
+ return
+ if v_adjustment.value &gt; v_adjustment.lower:
+ new_value = v_adjustment.value - \
+ v_adjustment.step_increment
+ if new_value &lt; v_adjustment.lower:
+ new_value = v_adjustment.lower
+ v_adjustment.value = new_value
+
+ def show_page(self, page_number):
+ global PAGE_SIZE, current_word
+ position = self.page_index[page_number]
+ self.etext_file.seek(position)
+ linecount = 0
+ label_text = ''
+ textbuffer = self.textview.get_buffer()
+ while linecount &lt; PAGE_SIZE:
+ line = self.etext_file.readline()
+ label_text = label_text + \
+ unicode(line, 'iso-8859-1')
+ linecount = linecount + 1
+ textbuffer.set_text(label_text)
+ self.textview.set_buffer(textbuffer)
+ self.word_tuples = \
+ speech.prepare_highlighting(label_text)
+ self.words_on_page = \
+ speech.add_word_marks(self.word_tuples)
+
+ def save_extracted_file(self, zipfile, filename):
+ "Extract the file to a temp directory for viewing"
+ filebytes = zipfile.read(filename)
+ f = open("/tmp/" + filename, 'w')
+ try:
+ f.write(filebytes)
+ finally:
+ f.close()
+
+ def read_file(self, filename):
+ "Read the Etext file"
+ global PAGE_SIZE
+
+ if zipfile.is_zipfile(filename):
+ self.zf = zipfile.ZipFile(filename, 'r')
+ self.book_files = self.zf.namelist()
+ self.save_extracted_file(self.zf, \
+ self.book_files[0])
+ currentFileName = "/tmp/" + self.book_files[0]
+ else:
+ currentFileName = filename
+
+ self.etext_file = open(currentFileName,"r")
+ self.page_index = [ 0 ]
+ linecount = 0
+ while self.etext_file:
+ line = self.etext_file.readline()
+ if not line:
+ break
+ linecount = linecount + 1
+ if linecount &gt;= PAGE_SIZE:
+ position = self.etext_file.tell()
+ self.page_index.append(position)
+ linecount = 0
+ if filename.endswith(".zip"):
+ os.remove(currentFileName)
+
+ def delete_cb(self, widget, event, data=None):
+ speech.stop()
+ return False
+
+ def destroy_cb(self, widget, data=None):
+ speech.stop()
+ gtk.main_quit()
+
+ def main(self, file_path):
+ self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ self.window.connect("delete_event", self.delete_cb)
+ self.window.connect("destroy", self.destroy_cb)
+ self.window.set_title("Read Etexts Activity")
+ self.window.set_size_request(800, 600)
+ self.window.set_border_width(0)
+ self.read_file(file_path)
+ self.scrolled_window = gtk.ScrolledWindow(
+ hadjustment=None, vadjustment=None)
+ self.textview = gtk.TextView()
+ self.textview.set_editable(False)
+ self.textview.set_left_margin(50)
+ self.textview.set_cursor_visible(False)
+ self.textview.connect("key_press_event",
+ self.keypress_cb)
+ self.font_desc = pango.FontDescription("sans 12")
+ self.textview.modify_font(self.font_desc)
+ self.show_page(0)
+ self.scrolled_window.add(self.textview)
+ self.window.add(self.scrolled_window)
+ self.textview.show()
+ self.scrolled_window.show()
+ self.window.show()
+ gtk.main()
+
+if __name__ == "__main__":
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], "")
+ ReadEtextsActivity().main(args[0])
+ except getopt.error, msg:
+ print msg
+ print "This program has no options"
+ sys.exit(2)
+</pre>
+<p>The program <strong>ReadEtextsTTS</strong> has only a few changes to make it enabled for speech. The first one checks for the existence of the gstreamer plugin:
+</p>
+<pre>speech_supported = True
+
+try:
+ import gst
+ gst.element_factory_make('espeak')
+ print 'speech supported!'
+except Exception, e:
+ speech_supported = False
+ print 'speech not supported!'
+</pre>
+<p> This code detects whether the plugin is installed by attempting to import for the Python library associated with it named "gst". If the import fails it throws an <strong>Exception</strong> and we catch that Exception and use it to set a variable named <strong>speech_supported</strong> to <strong>False</strong>.&#160; We can check the value of this variable in other places in the program to make a program that works with Text To Speech if it is available and without it if it is not.&#160; Making a program work in different environments by doing these kinds of checks is called <em>degrading gracefully</em>.&#160; Catching exceptions on imports is a common technique in Python to achieve this.&#160; If you want your Activity to run on older versions of Sugar you may find yourself using it.
+</p>
+<p>The next bit of code we're going to look at highlights a word in the textview and scrolls the textview to keep the highlighted word visible.
+</p>
+<pre>class ReadEtextsActivity():
+ def __init__(self):
+ "The entry point to the Activity"
+ speech.highlight_cb = self.highlight_next_word
+ # print speech.voices()
+
+ def highlight_next_word(self, word_count):
+ if word_count &lt; len(self.word_tuples):
+ word_tuple = self.word_tuples[word_count]
+ textbuffer = self.textview.get_buffer()
+ tag = textbuffer.create_tag()
+ tag.set_property('weight', pango.WEIGHT_BOLD)
+ tag.set_property( 'foreground', "white")
+ tag.set_property( 'background', "black")
+ iterStart = \
+ textbuffer.get_iter_at_offset(word_tuple[0])
+ iterEnd = \
+ textbuffer.get_iter_at_offset(word_tuple[1])
+ bounds = textbuffer.get_bounds()
+ textbuffer.remove_all_tags(bounds[0], bounds[1])
+ textbuffer.apply_tag(tag, iterStart, iterEnd)
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ max = v_adjustment.upper - v_adjustment.page_size
+ max = max * word_count
+ max = max / len(self.word_tuples)
+ v_adjustment.value = max
+ return True
+</pre>
+<p>In the <em>__init__()</em> method we assign a variable called <em>highlight_cb</em> in <strong>speech.py</strong> with a method called <em>highlight_next_word()</em>.&#160; This gives <strong>speech.py</strong> a way to call that method every time a new word in the textview needs to be highlighted.
+</p>
+<p>The next line will print the list of tuples containing Voice names to the terminal if you uncomment it.&#160; We aren't letting the user change voices in this application but it would not be difficult to add that feature.
+ <br/></p>
+<p>The code for the method that highlights the words follows.&#160; What it does is look in a list of tuples that contain the starting and ending offsets of every word in the textarea's text buffer.&#160; The caller of this method passes in a word number (for instance the first word in the buffer is word 0, the second is word 1, and so on).&#160; The method looks up that entry in the list, gets its starting and ending offsets, removes any previous highlighting, then highlights the new text.&#160; In addition to that it figures out what fraction of the total number of words the current word is and scrolls the textviewer enough to make sure that word is visible.
+</p>
+<p>Of course this method works best on pages without many blank lines, which fortunately is most pages.&#160; It does not work so well on title pages.&#160; An experienced programmer could probably come up with a more elegant and reliable way of doing this scrolling.&#160; Let me know what you come up with.
+</p>
+<p>Further down we see the code that gets the keystrokes the user enters and does speech-related things with them:
+</p>
+<pre> def keypress_cb(self, widget, event):
+ "Respond when the user presses one of the arrow keys"
+ global done
+ global speech_supported
+ keyname = gtk.gdk.keyval_name(event.keyval)
+ if keyname == 'KP_End' and speech_supported:
+ if speech.is_paused() or speech.is_stopped():
+ speech.play(self.words_on_page)
+ else:
+ speech.pause()
+ return True
+ if speech_supported and speech.is_stopped() == False \
+ and speech.is_paused == False:
+ # If speech is in progress, ignore other keys.
+ return True
+ if keyname == '7':
+ speech.pitch_down()
+ speech.say('Pitch Adjusted')
+ return True
+ if keyname == '8':
+ speech.pitch_up()
+ speech.say('Pitch Adjusted')
+ return True
+ if keyname == '9':
+ speech.rate_down()
+ speech.say('Rate Adjusted')
+ return True
+ if keyname == '0':
+ speech.rate_up()
+ speech.say('Rate Adjusted')
+ return True
+</pre>
+<p>As you can see, the functions we're calling are all in the file <strong>speech.py</strong> that we imported.&#160; You don't have to fully understand how these functions work to make use of them in your own Activities.&#160; Notice that the code as written prevents the user from changing pitch or rate while speech is in progress.&#160; Notice also that there are two different methods in speech.py for doing speech.&#160; <strong>play()</strong> is the method for doing text to speech with word highlighting.&#160; <strong>say()</strong> is for saying short phrases produced by the user interface, in this case "Pitch adjusted" and "Rate adjusted".&#160; Of course if you put code like this in your Activity you would use the _() function so these phrases could be translated into other languages.
+</p>
+<p>There is one more bit of code we need to do text to speech with highlighting: we need to prepare the words to be spoken to be highlighted in the textviewer.
+</p>
+<pre> def show_page(self, page_number):
+ global PAGE_SIZE, current_word
+ position = self.page_index[page_number]
+ self.etext_file.seek(position)
+ linecount = 0
+ label_text = ''
+ textbuffer = self.textview.get_buffer()
+ while linecount &lt; PAGE_SIZE:
+ line = self.etext_file.readline()
+ label_text = label_text + unicode(line, \
+ 'iso-8859-1')
+ linecount = linecount + 1
+ textbuffer.set_text(label_text)
+ self.textview.set_buffer(textbuffer)
+ self.word_tuples = \
+ speech.prepare_highlighting(label_text)
+ self.words_on_page = \
+ speech.add_word_marks(self.word_tuples)
+</pre>
+<p>The beginning of this method reads a page's worth of text into a string called label_text and puts it into the textview's buffer.&#160; The last two lines splits the text into words, leaving in punctuation, and puts each word and its beginning and ending offsets into a tuple.&#160; The tuples are added to a List.
+</p>
+<p> <strong>speech.add_word_marks()</strong> converts the words in the List to a document in <em>SSML</em> (<em>Speech Synthesis Markup Language</em>) format.&#160; SSML is a standard for adding tags (sort of like the tags used to make web pages) to text to tell speech software what to do with the text.&#160; We're just using a very small part of this standard to produce a marked up document with a mark between each word, like this:
+</p>
+<pre>&lt;speak&gt;
+ &lt;mark name="0"/&gt;The&lt;mark name="1"/&gt;quick&lt;mark name-"2"/&gt;
+ brown&lt;mark name="3"/&gt;fox&lt;mark name="4"/&gt;jumps
+&lt;/speak&gt;</pre>
+<p>When espeak reads this file it will do a <em>callback</em> into our program every time it reads one of the mark tags.&#160; The callback will contain the number of the word in the <strong>word_tuples</strong> List which it will get from the <strong>name </strong>attribute of the <strong>mark </strong>tag.&#160; In this way the method being called will know which word to highlight.&#160; The advantage of using the mark name rather than just highlighting the next word in the textviewer is that if espeak should fail to do one of the callbacks the highlighting won't be thrown out of sync.&#160; This was a problem with speech-dispatcher.
+</p>
+<p>A callback is just what it sounds like.&#160; When one program calls another program it can pass in a function or method of its own that it wants the second program to call when something happens.
+ <br/></p>
+<p>To try out the new program run
+</p>
+<pre>./ReadEtextsTTS.py <em>bookfile</em></pre>
+<p>from the Terminal.&#160; You can adjust pitch and rate up and down using the keys <strong>7, 8, 9</strong>, and <strong>0</strong> on the top row of the keyboard.&#160; It should say "Pitch Adjusted" or "Rate Adjusted" when you do that.&#160; You can start, pause, and resume speech with highlighting by using the <strong>End</strong> key on the keypad.&#160; (On the XO laptop the "game" keys are mapped to what is the numeric keypad on a normal keyboard.&#160; This makes these keys handy for use when the XO is folded into tablet mode and the keyboard is not available).&#160; You cannot change pitch or rate while speech is in progress.&#160; Attempts to do that will be ignored.&#160; The program in action looks like this:
+</p>
+<p><img alt="espeak3.jpg" src="static/ActivitiesGuideSugar-espeak3-en.jpg" height="465" width="600"/></p>
+<p>That brings us to the end of the topic of Text to Speech.&#160; If you're like to see more, the Git repository for this book has a few more sample programs that use the gstreamer espeak plugin.&#160; These examples were created by the author of the plugin and demonstrate some other ways you can use it.&#160; There's even a "choir" program that demonstrates multiple voices speaking at the same time.
+ <br/></p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch019_fun-with-the-journal.xhtml b/app/static/doc/myosa/ch019_fun-with-the-journal.xhtml
new file mode 100644
index 0000000..981884d
--- /dev/null
+++ b/app/static/doc/myosa/ch019_fun-with-the-journal.xhtml
@@ -0,0 +1,1085 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>Fun With The Journal
+</h1>
+<h2>Introduction
+</h2>
+<p>By default every Activity creates and reads one Journal entry.&#160; Most Activities don't need to do any more with the Journal than that, and if your Activity is like that you won't need the information&#160; in this chapter.&#160; Chances are that someday you will want to do more than that, so if you do keep reading.
+</p>
+<p> First let's review what the Journal is.&#160; The Journal is a collection of files that each have <strong>metadata</strong> (data about data) associated with them.&#160; Metadata is stored as text strings and includes such things as the <strong>Title</strong>, <strong>Description</strong>, <strong>Tags</strong>, <strong>MIME Type</strong>, and a screen shot of the Activity when it was last used.
+</p>
+<p>Your Activity cannot read and write these files directly.&#160; Instead Sugar provides an API (Application Programming Interface) that gives you an indirect way to add, delete and modify entries in the Journal, as well as a way to search Journal entries and make a list of entries that meet the search criteria.
+</p>
+<p>The API we'll use is in the <strong>datastore</strong> package.&#160; After version .82 of Sugar this API was rewritten, so we'll need to learn how to support both versions in the same Activity.
+</p>
+<p>If you've read this far you've seen several examples where Sugar started out doing one thing and then changed to do the same thing a better way but still provided a way to create Activities that would work with either the old or the new way.&#160; You may be wondering if it is normal for a project to do this.&#160; As a professional programmer I can tell you that doing tricks like this to maintain backward compatibility is extremely common, and Sugar does no more of this than any other project.&#160; There are decisions made by Herman Hollerith when he tabulated the 1890 census using punched cards that computer programmers must live with to this day.
+</p>
+<h2>Introducing Sugar Commander
+</h2>
+<p> I am a big fan of the concept of the Journal but not so much of the <strong>Journal Activity</strong> that Sugar uses to navigate through it and maintain it.&#160; My biggest gripe against it is that it represents the contents of thumb drives and SD cards as if the files on these were also Journal entries.&#160; My feeling is that files and directories are one thing and the Journal is another, and the user interface should recognize that.
+</p>
+<p>Strictly speaking the Journal Activity is and is not an Activity.&#160; It inherits code from the Activity class just like any other Activity, and it is written in Python and uses the same datastore API that other Activities use.&#160; However, it is run in a special way that gives it powers and abilities far beyond those of an ordinary Activity.&#160; In particular it can do two things:
+</p>
+<ul><li>It can write to files on external media like thumb drives and SD cards.</li>
+ <li>It alone can be used to resume Journal entries using other Activities.</li>
+</ul><p>While I would like to write a Journal Activity that does everything the original does but has a user interface more to my own taste the Sugar security model won't allow that.&#160; Recently I came to the conclusion that a more mild-mannered version of the Journal Activity might be useful.&#160; Just as Kal-El sometimes finds it more useful to be Clark Kent than Superman, my own Activity might be a worthy alternative to the built-in Journal Activity when super powers are not needed.
+</p>
+<p> My Activity, which I call <strong>Sugar Commander</strong>, has two tabs.&#160; One represents the Journal and looks like this:
+</p>
+<p><img alt="Sugar Commander Journal Tab" src="static/ActivitiesGuideSugar-SCommander2-en.jpg" height="450" width="600"/></p>
+<p>This tab lets you browse through the Journal sorted by Title or MIME Type, select entries and view their details, update Title, Description or Tags, and delete entries you no longer want.&#160; The other tab shows files and folders and looks like this:
+</p>
+<p><img alt="Sugar Commander Files Tab" src="static/ActivitiesGuideSugar-SCommander1-en.jpg" height="450" width="600"/></p>
+<p>This tab lets you browse through the files and folders or the regular file system, including thumb drives and SD cards.&#160; You can select a file and make a Journal entry out of it by pushing the button at the bottom of the screen.
+</p>
+<p>&#160;This Activity has very little code and still manages to do everything an ordinary Activity can do with the Journal.&#160; You can download the Git repository using this command:
+ <br/></p>
+<pre><code>git clone git://git.sugarlabs.org/sugar-commander/\
+mainline.git</code></pre>
+<p>There is only one source file, <strong>sugarcommander.py</strong>:
+</p>
+<pre>import logging
+import os
+import gtk
+import pango
+import zipfile
+from sugar import mime
+from sugar.activity import activity
+from sugar.datastore import datastore
+from sugar.graphics.alert import NotifyAlert
+from sugar.graphics import style
+from gettext import gettext as _
+import gobject
+import dbus
+
+COLUMN_TITLE = 0
+COLUMN_MIME = 1
+COLUMN_JOBJECT = 2
+
+DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore'
+DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore'
+DS_DBUS_PATH = '/org/laptop/sugar/DataStore'
+
+_logger = logging.getLogger('sugar-commander')
+
+class SugarCommander(activity.Activity):
+ def __init__(self, handle, create_jobject=True):
+ "The entry point to the Activity"
+ activity.Activity.__init__(self, handle, False)
+ self.selected_journal_entry = None
+ self.selected_path = None
+
+ canvas = gtk.Notebook()
+ canvas.props.show_border = True
+ canvas.props.show_tabs = True
+ canvas.show()
+
+ self.ls_journal = gtk.ListStore(
+ gobject.TYPE_STRING,
+ gobject.TYPE_STRING,
+ gobject.TYPE_PYOBJECT)
+ self.tv_journal = gtk.TreeView(self.ls_journal)
+ self.tv_journal.set_rules_hint(True)
+ self.tv_journal.set_search_column(COLUMN_TITLE)
+ self.selection_journal = \
+ self.tv_journal.get_selection()
+ self.selection_journal.set_mode(
+ gtk.SELECTION_SINGLE)
+ self.selection_journal.connect("changed",
+ self.selection_journal_cb)
+ renderer = gtk.CellRendererText()
+ renderer.set_property('wrap-mode', gtk.WRAP_WORD)
+ renderer.set_property('wrap-width', 500)
+ renderer.set_property('width', 500)
+ self.col_journal = gtk.TreeViewColumn(_('Title'),
+ renderer, text=COLUMN_TITLE)
+ self.col_journal.set_sort_column_id(COLUMN_TITLE)
+ self.tv_journal.append_column(self.col_journal)
+
+ mime_renderer = gtk.CellRendererText()
+ mime_renderer.set_property('width', 500)
+ self.col_mime = gtk.TreeViewColumn(_('MIME'),
+ mime_renderer, text=COLUMN_MIME)
+ self.col_mime.set_sort_column_id(COLUMN_MIME)
+ self.tv_journal.append_column(self.col_mime)
+
+ self.list_scroller_journal = gtk.ScrolledWindow(
+ hadjustment=None, vadjustment=None)
+ self.list_scroller_journal.set_policy(
+ gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+ self.list_scroller_journal.add(self.tv_journal)
+
+ label_attributes = pango.AttrList()
+ label_attributes.insert(pango.AttrSize(
+ 14000, 0, -1))
+ label_attributes.insert(pango.AttrForeground(
+ 65535, 65535, 65535, 0, -1))
+
+ tab1_label = gtk.Label(_("Journal"))
+ tab1_label.set_attributes(label_attributes)
+ tab1_label.show()
+ self.tv_journal.show()
+ self.list_scroller_journal.show()
+
+ column_table = gtk.Table(rows=1, columns=2,
+ homogeneous = False)
+
+ image_table = gtk.Table(rows=2, columns=2,
+ homogeneous=False)
+ self.image = gtk.Image()
+ image_table.attach(self.image, 0, 2, 0, 1,
+ xoptions=gtk.FILL|gtk.SHRINK,
+ yoptions=gtk.FILL|gtk.SHRINK,
+ xpadding=10,
+ ypadding=10)
+
+ self.btn_save = gtk.Button(_("Save"))
+ self.btn_save.connect('button_press_event',
+ self.save_button_press_event_cb)
+ image_table.attach(self.btn_save, 0, 1, 1, 2,
+ xoptions=gtk.SHRINK,
+ yoptions=gtk.SHRINK, xpadding=10,
+ ypadding=10)
+ self.btn_save.props.sensitive = False
+ self.btn_save.show()
+
+ self.btn_delete = gtk.Button(_("Delete"))
+ self.btn_delete.connect('button_press_event',
+ self.delete_button_press_event_cb)
+ image_table.attach(self.btn_delete, 1, 2, 1, 2,
+ xoptions=gtk.SHRINK,
+ yoptions=gtk.SHRINK, xpadding=10,
+ ypadding=10)
+ self.btn_delete.props.sensitive = False
+ self.btn_delete.show()
+
+ column_table.attach(image_table, 0, 1, 0, 1,
+ xoptions=gtk.FILL|gtk.SHRINK,
+ yoptions=gtk.SHRINK, xpadding=10,
+ ypadding=10)
+
+ entry_table = gtk.Table(rows=3, columns=2,
+ homogeneous=False)
+
+ title_label = gtk.Label(_("Title"))
+ entry_table.attach(title_label, 0, 1, 0, 1,
+ xoptions=gtk.SHRINK,
+ yoptions=gtk.SHRINK,
+ xpadding=10, ypadding=10)
+ title_label.show()
+
+ self.title_entry = gtk.Entry(max=0)
+ entry_table.attach(self.title_entry, 1, 2, 0, 1,
+ xoptions=gtk.FILL|gtk.SHRINK,
+ yoptions=gtk.SHRINK, xpadding=10, ypadding=10)
+ self.title_entry.connect('key_press_event',
+ self.key_press_event_cb)
+ self.title_entry.show()
+
+ description_label = gtk.Label(_("Description"))
+ entry_table.attach(description_label, 0, 1, 1, 2,
+ xoptions=gtk.SHRINK,
+ yoptions=gtk.SHRINK,
+ xpadding=10, ypadding=10)
+ description_label.show()
+
+ self.description_textview = gtk.TextView()
+ self.description_textview.set_wrap_mode(
+ gtk.WRAP_WORD)
+ entry_table.attach(self.description_textview,
+ 1, 2, 1, 2,
+ xoptions=gtk.EXPAND|gtk.FILL|gtk.SHRINK,
+ yoptions=gtk.EXPAND|gtk.FILL|gtk.SHRINK,
+ xpadding=10, ypadding=10)
+ self.description_textview.props.accepts_tab = False
+ self.description_textview.connect('key_press_event',
+ self.key_press_event_cb)
+ self.description_textview.show()
+
+ tags_label = gtk.Label(_("Tags"))
+ entry_table.attach(tags_label, 0, 1, 2, 3,
+ xoptions=gtk.SHRINK,
+ yoptions=gtk.SHRINK,
+ xpadding=10, ypadding=10)
+ tags_label.show()
+
+ self.tags_textview = gtk.TextView()
+ self.tags_textview.set_wrap_mode(gtk.WRAP_WORD)
+ entry_table.attach(self.tags_textview, 1, 2, 2, 3,
+ xoptions=gtk.FILL,
+ yoptions=gtk.EXPAND|gtk.FILL,
+ xpadding=10, ypadding=10)
+ self.tags_textview.props.accepts_tab = False
+ self.tags_textview.connect('key_press_event',
+ self.key_press_event_cb)
+ self.tags_textview.show()
+
+ entry_table.show()
+
+ self.scroller_entry = gtk.ScrolledWindow(
+ hadjustment=None, vadjustment=None)
+ self.scroller_entry.set_policy(gtk.POLICY_NEVER,
+ gtk.POLICY_AUTOMATIC)
+ self.scroller_entry.add_with_viewport(entry_table)
+ self.scroller_entry.show()
+
+ column_table.attach(self.scroller_entry,
+ 1, 2, 0, 1,
+ xoptions=gtk.FILL|gtk.EXPAND|gtk.SHRINK,
+ yoptions=gtk.FILL|gtk.EXPAND|gtk.SHRINK,
+ xpadding=10, ypadding=10)
+ image_table.show()
+ column_table.show()
+
+ vbox = gtk.VBox(homogeneous=True, spacing=5)
+ vbox.pack_start(column_table)
+ vbox.pack_end(self.list_scroller_journal)
+
+ canvas.append_page(vbox, tab1_label)
+
+ self._filechooser = gtk.FileChooserWidget(
+ action=gtk.FILE_CHOOSER_ACTION_OPEN,
+ backend=None)
+ self._filechooser.set_current_folder("/media")
+ self.copy_button = gtk.Button(
+ _("Copy File To The Journal"))
+ self.copy_button.connect('clicked',
+ self.create_journal_entry)
+ self.copy_button.show()
+ self._filechooser.set_extra_widget(self.copy_button)
+ preview = gtk.Image()
+ self._filechooser.set_preview_widget(preview)
+ self._filechooser.connect("update-preview",
+ self.update_preview_cb, preview)
+ tab2_label = gtk.Label(_("Files"))
+ tab2_label.set_attributes(label_attributes)
+ tab2_label.show()
+ canvas.append_page(self._filechooser, tab2_label)
+
+ self.set_canvas(canvas)
+ self.show_all()
+
+ toolbox = activity.ActivityToolbox(self)
+ activity_toolbar = toolbox.get_activity_toolbar()
+ activity_toolbar.keep.props.visible = False
+ activity_toolbar.share.props.visible = False
+ self.set_toolbox(toolbox)
+ toolbox.show()
+
+ self.load_journal_table()
+
+ bus = dbus.SessionBus()
+ remote_object = bus.get_object(
+ DS_DBUS_SERVICE, DS_DBUS_PATH)
+ _datastore = dbus.Interface(remote_object,
+ DS_DBUS_INTERFACE)
+ _datastore.connect_to_signal('Created',
+ self.datastore_created_cb)
+ _datastore.connect_to_signal('Updated',
+ self.datastore_updated_cb)
+ _datastore.connect_to_signal('Deleted',
+ self.datastore_deleted_cb)
+
+ self.selected_journal_entry = None
+
+ def update_preview_cb(self, file_chooser, preview):
+ filename = file_chooser.get_preview_filename()
+ try:
+ file_mimetype = mime.get_for_file(filename)
+ if file_mimetype.startswith('image/'):
+ pixbuf = \
+ gtk.gdk.pixbuf_new_from_file_at_size(
+ filename,
+ style.zoom(320), style.zoom(240))
+ preview.set_from_pixbuf(pixbuf)
+ have_preview = True
+ elif file_mimetype == 'application/x-cbz':
+ fname = self.extract_image(filename)
+ pixbuf = \
+ gtk.gdk.pixbuf_new_from_file_at_size(
+ fname,
+ style.zoom(320), style.zoom(240))
+ preview.set_from_pixbuf(pixbuf)
+ have_preview = True
+ os.remove(fname)
+ else:
+ have_preview = False
+ except:
+ have_preview = False
+ file_chooser.set_preview_widget_active(
+ have_preview)
+ return
+
+ def key_press_event_cb(self, entry, event):
+ self.btn_save.props.sensitive = True
+
+ def save_button_press_event_cb(self, entry, event):
+ self.update_entry()
+
+ def delete_button_press_event_cb(self, entry, event):
+ datastore.delete(
+ self.selected_journal_entry.object_id)
+
+ def datastore_created_cb(self, uid):
+ new_jobject = datastore.get(uid)
+ iter = self.ls_journal.append()
+ title = new_jobject.metadata['title']
+ self.ls_journal.set(iter, COLUMN_TITLE, title)
+ mime = new_jobject.metadata['mime_type']
+ self.ls_journal.set(iter, COLUMN_MIME, mime)
+ self.ls_journal.set(iter, COLUMN_JOBJECT,
+ new_jobject)
+
+ def datastore_updated_cb(self, uid):
+ new_jobject = datastore.get(uid)
+ iter = self.ls_journal.get_iter_first()
+ for row in self.ls_journal:
+ jobject = row[COLUMN_JOBJECT]
+ if jobject.object_id == uid:
+ title = new_jobject.metadata['title']
+ self.ls_journal.set_value(iter,
+ COLUMN_TITLE, title)
+ break
+ iter = self.ls_journal.iter_next(iter)
+ object_id = self.selected_journal_entry.object_id
+ if object_id == uid:
+ self.set_form_fields(new_jobject)
+
+ def datastore_deleted_cb(self, uid):
+ save_path = self.selected_path
+ iter = self.ls_journal.get_iter_first()
+ for row in self.ls_journal:
+ jobject = row[COLUMN_JOBJECT]
+ if jobject.object_id == uid:
+ self.ls_journal.remove(iter)
+ break
+ iter = self.ls_journal.iter_next(iter)
+
+ try:
+ self.selection_journal.select_path(save_path)
+ self.tv_journal.grab_focus()
+ except:
+ self.title_entry.set_text('')
+ description_textbuffer = \
+ self.description_textview.get_buffer()
+ description_textbuffer.set_text('')
+ tags_textbuffer = \
+ self.tags_textview.get_buffer()
+ tags_textbuffer.set_text('')
+ self.btn_save.props.sensitive = False
+ self.btn_delete.props.sensitive = False
+ self.image.clear()
+ self.image.show()
+
+ def update_entry(self):
+ needs_update = False
+
+ if self.selected_journal_entry is None:
+ return
+
+ object_id = self.selected_journal_entry.object_id
+ jobject = datastore.get(object_id)
+
+ old_title = jobject.metadata.get('title', None)
+ if old_title != self.title_entry.props.text:
+ jobject.metadata['title'] = \
+ self.title_entry.props.text
+ jobject.metadata['title_set_by_user'] = '1'
+ needs_update = True
+
+ old_tags = jobject.metadata.get('tags', None)
+ new_tags = \
+ self.tags_textview.props.buffer.props.text
+ if old_tags != new_tags:
+ jobject.metadata['tags'] = new_tags
+ needs_update = True
+
+ old_description = jobject.metadata.get(
+ 'description', None)
+ new_description = \
+ self.description_textview.props.buffer.props.text
+ if old_description != new_description:
+ jobject.metadata['description'] = new_description
+ needs_update = True
+
+ if needs_update:
+ datastore.write(jobject, update_mtime=False,
+ reply_handler=self.datastore_write_cb,
+ error_handler=self.datastore_write_error_cb)
+ self.btn_save.props.sensitive = False
+
+ def datastore_write_cb(self):
+ pass
+
+ def datastore_write_error_cb(self, error):
+ logging.error(
+ 'sugarcommander.datastore_write_error_cb:'
+ ' %r' % error)
+
+ def close(self, skip_save=False):
+ "Override the close method so we don't try to
+ create a Journal entry."
+ activity.Activity.close(self, True)
+
+ def selection_journal_cb(self, selection):
+ self.btn_delete.props.sensitive = True
+ tv = selection.get_tree_view()
+ model = tv.get_model()
+ sel = selection.get_selected()
+ if sel:
+ model, iter = sel
+ jobject = model.get_value(iter,COLUMN_JOBJECT)
+ jobject = datastore.get(jobject.object_id)
+ self.selected_journal_entry = jobject
+ self.set_form_fields(jobject)
+ self.selected_path = model.get_path(iter)
+
+ def set_form_fields(self, jobject):
+ self.title_entry.set_text(jobject.metadata['title'])
+ description_textbuffer = \
+ self.description_textview.get_buffer()
+ if jobject.metadata.has_key('description'):
+ description_textbuffer.set_text(
+ jobject.metadata['description'])
+ else:
+ description_textbuffer.set_text('')
+ tags_textbuffer = self.tags_textview.get_buffer()
+ if jobject.metadata.has_key('tags'):
+ tags_textbuffer.set_text(jobject.metadata['tags'])
+ else:
+ tags_textbuffer.set_text('')
+ self.create_preview(jobject.object_id)
+
+ def create_preview(self, object_id):
+ jobject = datastore.get(object_id)
+
+ if jobject.metadata.has_key('preview'):
+ preview = jobject.metadata['preview']
+ if preview is None or preview == '' \
+ or preview == 'None':
+ if jobject.metadata['mime_type'].startswith(
+ 'image/'):
+ filename = jobject.get_file_path()
+ self.show_image(filename)
+ return
+ if jobject.metadata['mime_type'] == \
+ 'application/x-cbz':
+ filename = jobject.get_file_path()
+ fname = self.extract_image(filename)
+ self.show_image(fname)
+ os.remove(fname)
+ return
+
+ if jobject.metadata.has_key('preview') and \
+ len(jobject.metadata['preview']) &gt; 4:
+
+ if jobject.metadata['preview'][1:4] == 'PNG':
+ preview_data = jobject.metadata['preview']
+ else:
+ import base64
+ preview_data = \
+ base64.b64decode(
+ jobject.metadata['preview'])
+
+ loader = gtk.gdk.PixbufLoader()
+ loader.write(preview_data)
+ scaled_buf = loader.get_pixbuf()
+ loader.close()
+ self.image.set_from_pixbuf(scaled_buf)
+ self.image.show()
+ else:
+ self.image.clear()
+ self.image.show()
+
+ def load_journal_table(self):
+ self.btn_save.props.sensitive = False
+ self.btn_delete.props.sensitive = False
+ ds_mounts = datastore.mounts()
+ mountpoint_id = None
+ if len(ds_mounts) == 1 and \
+ ds_mounts[0]['id'] == 1:
+ pass
+ else:
+ for mountpoint in ds_mounts:
+ id = mountpoint['id']
+ uri = mountpoint['uri']
+ if uri.startswith('/home'):
+ mountpoint_id = id
+
+ query = {}
+ if mountpoint_id is not None:
+ query['mountpoints'] = [ mountpoint_id ]
+ ds_objects, num_objects = \
+ datastore.find(query, properties=['uid',
+ 'title', 'mime_type'])
+
+ self.ls_journal.clear()
+ for i in xrange (0, num_objects, 1):
+ iter = self.ls_journal.append()
+ title = ds_objects[i].metadata['title']
+ self.ls_journal.set(iter, COLUMN_TITLE, title)
+ mime = ds_objects[i].metadata['mime_type']
+ self.ls_journal.set(iter, COLUMN_MIME, mime)
+ self.ls_journal.set(iter, COLUMN_JOBJECT,
+ ds_objects[i])
+ if not self.selected_journal_entry is None and \
+ self.selected_journal_entry.object_id == \
+ ds_objects[i].object_id:
+ self.selection_journal.select_iter(iter)
+
+ self.ls_journal.set_sort_column_id(COLUMN_TITLE,
+ gtk.SORT_ASCENDING)
+ v_adjustment = \
+ self.list_scroller_journal.get_vadjustment()
+ v_adjustment.value = 0
+ return ds_objects[0]
+
+ def create_journal_entry(self, widget, data=None):
+ filename = self._filechooser.get_filename()
+ journal_entry = datastore.create()
+ journal_entry.metadata['title'] = \
+ self.make_new_filename(filename)
+ journal_entry.metadata['title_set_by_user'] = '1'
+ journal_entry.metadata['keep'] = '0'
+ file_mimetype = mime.get_for_file(filename)
+ if not file_mimetype is None:
+ journal_entry.metadata['mime_type'] = \
+ file_mimetype
+ journal_entry.metadata['buddies'] = ''
+ if file_mimetype.startswith('image/'):
+ preview = \
+ self.create_preview_metadata(filename)
+ elif file_mimetype == 'application/x-cbz':
+ fname = self.extract_image(filename)
+ preview = self.create_preview_metadata(fname)
+ os.remove(fname)
+ else:
+ preview = ''
+ if not preview == '':
+ journal_entry.metadata['preview'] = \
+ dbus.ByteArray(preview)
+ else:
+ journal_entry.metadata['preview'] = ''
+
+ journal_entry.file_path = filename
+ datastore.write(journal_entry)
+ self.alert(_('Success'), _('%s added to Journal.')
+ % self.make_new_filename(filename))
+
+ def alert(self, title, text=None):
+ alert = NotifyAlert(timeout=20)
+ alert.props.title = title
+ alert.props.msg = text
+ self.add_alert(alert)
+ alert.connect('response', self.alert_cancel_cb)
+ alert.show()
+
+ def alert_cancel_cb(self, alert, response_id):
+ self.remove_alert(alert)
+
+ def show_image(self, filename):
+ "display a resized image in a preview"
+ scaled_buf = gtk.gdk.pixbuf_new_from_file_at_size(
+ filename,
+ style.zoom(320), style.zoom(240))
+ self.image.set_from_pixbuf(scaled_buf)
+ self.image.show()
+
+ def extract_image(self, filename):
+ zf = zipfile.ZipFile(filename, 'r')
+ image_files = zf.namelist()
+ image_files.sort()
+ file_to_extract = image_files[0]
+ extract_new_filename = self.make_new_filename(
+ file_to_extract)
+ if extract_new_filename is None or \
+ extract_new_filename == '':
+ # skip over directory name if the images
+ # are in a subdirectory.
+ file_to_extract = image_files[1]
+ extract_new_filename = self.make_new_filename(
+ file_to_extract)
+
+ if len(image_files) &gt; 0:
+ if self.save_extracted_file(zf, file_to_extract):
+ fname = os.path.join(self.get_activity_root(),
+ 'instance',
+ extract_new_filename)
+ return fname
+
+ def save_extracted_file(self, zipfile, filename):
+ "Extract the file to a temp directory for viewing"
+ try:
+ filebytes = zipfile.read(filename)
+ except zipfile.BadZipfile, err:
+ print 'Error opening the zip file: %s' % (err)
+ return False
+ except KeyError, err:
+ self.alert('Key Error', 'Zipfile key not found: '
+ + str(filename))
+ return
+ outfn = self.make_new_filename(filename)
+ if (outfn == ''):
+ return False
+ fname = os.path.join(self.get_activity_root(),
+ 'instance', outfn)
+ f = open(fname, 'w')
+ try:
+ f.write(filebytes)
+ finally:
+ f.close()
+ return True
+
+ def make_new_filename(self, filename):
+ partition_tuple = filename.rpartition('/')
+ return partition_tuple[2]
+
+ def create_preview_metadata(self, filename):
+
+ file_mimetype = mime.get_for_file(filename)
+ if not file_mimetype.startswith('image/'):
+ return ''
+
+ scaled_pixbuf = \
+ gtk.gdk.pixbuf_new_from_file_at_size(
+ filename,
+ style.zoom(320), style.zoom(240))
+ preview_data = []
+
+ def save_func(buf, data):
+ data.append(buf)
+
+ scaled_pixbuf.save_to_callback(save_func,
+ 'png',
+ user_data=preview_data)
+ preview_data = ''.join(preview_data)
+
+ return preview_data</pre>
+<p>Let's look at this code one method at a time.
+ <br/></p>
+<h2> Adding A Journal Entry
+ <br/></h2>
+<p>We add a Journal entry when someone pushes a button on the gtk.FileChooser.&#160; This is the code that gets run:
+</p>
+<pre> def create_journal_entry(self, widget, data=None):
+ filename = self._filechooser.get_filename()
+ journal_entry = datastore.create()
+ journal_entry.metadata['title'] = \
+ self.make_new_filename(
+ filename)
+ journal_entry.metadata['title_set_by_user'] = '1'
+ journal_entry.metadata['keep'] = '0'
+ file_mimetype = mime.get_for_file(filename)
+ if not file_mimetype is None:
+ journal_entry.metadata['mime_type'] = \
+ file_mimetype
+ journal_entry.metadata['buddies'] = ''
+ if file_mimetype.startswith('image/'):
+ preview = self.create_preview_metadata(filename)
+ elif file_mimetype == 'application/x-cbz':
+ fname = self.extract_image(filename)
+ preview = self.create_preview_metadata(fname)
+ os.remove(fname)
+ else:
+ preview = ''
+ if not preview == '':
+ journal_entry.metadata['preview'] = \
+ dbus.ByteArray(preview)
+ else:
+ journal_entry.metadata['preview'] = ''
+ journal_entry.file_path = filename
+ datastore.write(journal_entry)
+</pre>
+<p>The only thing worth commenting on here is the metadata.&#160; <strong>title</strong> is what appears as #3 in the picture below.&#160; <strong>title_set_by_user</strong> is set to 1 so that the Activity won't prompt the user to change the title when the Activity closes.&#160; <strong>keep</strong> refers to the little star that appears at the beginning of the Journal entry (see #1 in the picture below).&#160; Highlight it by setting this to 1, otherwise set to 0.&#160;&#160; <strong>buddies</strong> is a list of users that collaborated on the Journal entry, and in this case there aren't any (these show up as #4 in the picture below).&#160;
+ <br/></p><img alt="Journal Legend" src="static/ActivitiesGuideSugar-journal_main_screen-en.png" height="415" width="600"/><h2>
+</h2>
+<p><strong>preview</strong> is an image file in the PNG format that is a screenshot of the Activity in action.&#160; This is created by the Activity itself when it is run so there is no need to make one when you add a Journal entry.&#160; You can simply use an empty string ('') for this property.
+</p>
+<p>Because previews are much more visible in Sugar Commander than they are in the regular Journal Activity I decided that Sugar Commander should make a preview image for image files and comic books as soon as they are added to the Journal.&#160; To do this I made a pixbuf of the image that would fit within the scaled dimensions of 320x240 pixels and made a <strong>dbus.ByteArray</strong> out of it, which is the format that the Journal uses to store preview images.
+ <br/></p>
+<p><strong>mime_type</strong> describes the format of the file and is generally assigned based on the filename suffix.&#160; For instance, files ending in .html have a MIME type of 'text/html'.&#160; Python has a package called <strong>mimetypes </strong>that takes a file name and figures out what its MIME type should be, but Sugar provides its own package to do the same thing.&#160; For most files either one would give the correct answer, but Sugar has its own MIME types for things like Activity bundles, etc. so for best results you really should use Sugar's mime package.&#160; You can import it like this:
+</p>
+<pre>from sugar import mime</pre>
+<p>The rest of the metadata (icon, modified time) is created automatically.&#160;
+ <br/></p>
+<h2>NOT Adding A Journal Entry
+</h2>
+<p>Sugar Activities by default create a Journal entry using the <em>write_file()</em> method.&#160; There will be Activities that don't need to do this.&#160; For instance, <strong>Get Internet Archive Books</strong> downloads e-books to the Journal, but has no need for a Journal entry of its own.&#160; The same thing is true of <strong>Sugar Commander</strong>.&#160; You might make a game that keeps track of high scores.&#160; You could keep those scores in a Journal entry, but that would require players to resume the game from the Journal rather than just starting it up from the Activity Ring.&#160; For that reason you might prefer to store the high scores in a file in the <strong>data </strong>directory rather than the Journal, and not leave a Journal entry behind at all.
+</p>
+<p>Sugar gives you a way to do that.&#160; First you need to specify an extra argument in your Activity's <em>__init__()</em> method like this:
+</p>
+<pre>class SugarCommander(activity.Activity):
+ def __init__(self, handle, create_jobject=True):
+ "The entry point to the Activity"
+ activity.Activity.__init__(self, handle, False)
+</pre>
+<p>Second, you need to override the <em>close()</em> method like this:
+ <br/></p>
+<pre> def close(self, skip_save=False):
+ "Override the close method so we don't try to
+ create a Journal entry."
+ activity.Activity.close(self, True)
+</pre>
+<p>That's all there is to it.
+</p>
+<h2>Listing Out Journal Entries
+</h2>
+<p>If you need to list out Journal entries you can use the <em>find()</em> method of <strong>datastore</strong>.&#160; The find method takes an argument containing search criteria.&#160; If you want to search for image files you can search by mime-type using a statement like this:
+</p>
+<pre> ds_objects, num_objects = datastore.find(
+ {'mime_type':['image/jpeg',
+ 'image/gif', 'image/tiff', 'image/png']},
+ properties=['uid',
+ 'title', 'mime_type']))
+</pre>
+<p>You can use any metadata attribute to search on.&#160; If you want to list out everything in the Journal you can use an empty search criteria like this:
+</p>
+<pre> ds_objects, num_objects = datastore.find({},
+ properties=['uid',
+ 'title', 'mime_type']))
+</pre>
+<p>The properties argument specifies what metadata to return for each object in the list.&#160; You should limit these to what you plan to use, but always include <strong>uid</strong>.&#160; One thing you should <em>never</em> include in a list is <strong>preview</strong>.&#160; This is an image file showing what the Activity for the Journal object looked like when it was last used.&#160; If for some reason you need this there is a simple way to get it for an individual Journal object, but you never want to include it in a list because it will slow down your Activity enormously.
+ <br/></p>
+<p>Listing out what is in the Journal is complicated because of the datastore rewrite done for Sugar .84.&#160; Before .84 the <em>datastore.find()</em> method listed out both Journal entries and files on external media like thumb drives and SD cards and you need to figure out which is which.&#160; In .84 and later it only lists out Journal entries.&#160; Fortunately it is possible to write code that supports either behavior.&#160; Here is code in <strong>Sugar Commander</strong> that only lists Journal entries:
+ <br/></p>
+<pre> def load_journal_table(self):
+ self.btn_save.props.sensitive = False
+ self.btn_delete.props.sensitive = False
+ ds_mounts = datastore.mounts()
+ mountpoint_id = None
+ if len(ds_mounts) == 1 and ds_mounts[0]['id'] == 1:
+ pass
+ else:
+ for mountpoint in ds_mounts:
+ id = mountpoint['id']
+ uri = mountpoint['uri']
+ if uri.startswith('/home'):
+ mountpoint_id = id
+
+ query = {}
+ if mountpoint_id is not None:
+ query['mountpoints'] = [ mountpoint_id ]
+ ds_objects, num_objects = datastore.find(
+ query, properties=['uid',
+ 'title', 'mime_type'])
+
+ self.ls_journal.clear()
+ for i in xrange (0, num_objects, 1):
+ iter = self.ls_journal.append()
+ title = ds_objects[i].metadata['title']
+ self.ls_journal.set(iter,
+ COLUMN_TITLE, title)
+ mime = ds_objects[i].metadata['mime_type']
+ self.ls_journal.set(iter, COLUMN_MIME, mime)
+ self.ls_journal.set(iter, COLUMN_JOBJECT,
+ ds_objects[i])
+ if not self.selected_journal_entry is None and \
+ self.selected_journal_entry.object_id == \
+ ds_objects[i].object_id:
+ self.selection_journal.select_iter(iter)
+
+ self.ls_journal.set_sort_column_id(COLUMN_TITLE,
+ gtk.SORT_ASCENDING)
+ v_adjustment = \
+ self.list_scroller_journal.get_vadjustment()
+ v_adjustment.value = 0
+ return ds_objects[0]</pre>
+<p>We need to use the <em>datastore.mounts()</em> method for two purposes:
+</p>
+<ul><li>In Sugar .82 and below it will list out all mount points, including the place the Journal is mounted on and the places external media is mounted on.&#160; The mountpoint is a Python dictionary that contains a <strong>uri</strong> property (which is the path to the mount point) and an <strong>id</strong> property (which is a name given to the mount point).&#160; Every Journal entry has a metadata attribute named <strong>mountpoint</strong>.&#160; The Journal <strong>uri</strong> will be the only one starting with <strong>/home</strong>, so if we limit the search to Journal objects where the <strong>id</strong> of that mountpoint equals the <strong>mountpoint</strong> metadata in the Journal objects we can easily list only objects from the Journal.</li>
+ <li>In Sugar .84 and later the <em>datastore.mounts()</em> method still exists but doesn't tell you anything about mountpoints.&#160; However, you can use the code above to see if there is only one mountpoint and if its id is 1.&#160; If it is you know you're dealing with the rewritten datastore of .84 and later.&#160; The other difference is that the Journal objects no longer have metadata with a key of <strong>mountpoint</strong>.&#160; If you use the code above it will account for this difference and work with either version of Sugar.</li>
+</ul><p>What if you want the Sugar .82 behavior, listing both Journal entries and USB files as Journal objects, in both .82 and .84 and up?&#160; I wanted to do that for <strong>View Slides</strong> and ended up using this code:
+</p>
+<pre> def load_journal_table(self):
+ ds_objects, num_objects = datastore.find(
+ {'mime_type':['image/jpeg',
+ 'image/gif', 'image/tiff', 'image/png']},
+ properties=['uid', 'title', 'mime_type'])
+ self.ls_right.clear()
+ for i in xrange (0, num_objects, 1):
+ iter = self.ls_right.append()
+ title = ds_objects[i].metadata['title']
+ mime_type = ds_objects[i].metadata['mime_type']
+ if mime_type == 'image/jpeg' \
+ and not title.endswith('.jpg') \
+ and not title.endswith('.jpeg') \
+ and not title.endswith('.JPG') \
+ and not title.endswith('.JPEG') :
+ title = title + '.jpg'
+ if mime_type == 'image/png' \
+ and not title.endswith('.png') \
+ and not title.endswith('.PNG'):
+ title = title + '.png'
+ if mime_type == 'image/gif' \
+ and not title.endswith('.gif')\
+ and not title.endswith('.GIF'):
+ title = title + '.gif'
+ if mime_type == 'image/tiff' \
+ and not title.endswith('.tiff')\
+ and not title.endswith('.TIFF'):
+ title = title + '.tiff'
+ self.ls_right.set(iter, COLUMN_IMAGE, title)
+ jobject_wrapper = JobjectWrapper()
+ jobject_wrapper.set_jobject(ds_objects[i])
+ self.ls_right.set(iter, COLUMN_PATH,
+ jobject_wrapper)
+
+ valid_endings = ('.jpg', '.jpeg', '.JPEG',
+ '.JPG', '.gif', '.GIF', '.tiff',
+ '.TIFF', '.png', '.PNG')
+ ds_mounts = datastore.mounts()
+ if len(ds_mounts) == 1 and ds_mounts[0]['id'] == 1:
+ # datastore.mounts() is stubbed out,
+ # we're running .84 or better
+ for dirname, dirnames, filenames in os.walk(
+ '/media'):
+ if '.olpc.store' in dirnames:
+ dirnames.remove('.olpc.store')
+ # don't visit .olpc.store directories
+ for filename in filenames:
+ if filename.endswith(valid_endings):
+ iter = self.ls_right.append()
+ jobject_wrapper = JobjectWrapper()
+ jobject_wrapper.set_file_path(
+ os.path.join(dirname, filename))
+ self.ls_right.set(iter, COLUMN_IMAGE,
+ filename)
+ self.ls_right.set(iter, COLUMN_PATH,
+ jobject_wrapper)
+
+ self.ls_right.set_sort_column_id(COLUMN_IMAGE,
+ gtk.SORT_ASCENDING)
+</pre>
+<p>In this case I use the <em>datastore.mounts()</em> method to figure out what version of the datastore I have and then if I'm running .84 and later I use <em>os.walk()</em> to create a flat list of all files in all directories found under the directory <strong>/media</strong> (which is where USB and SD cards are always mounted).&#160; I can't make these files into directories, but what I can do is make a wrapper class that can contain either a Journal object or a file and use those objects where I would normally use Journal objects.&#160; The wrapper class looks like this:
+</p>
+<pre>class JobjectWrapper():
+ def __init__(self):
+ self.__jobject = None
+ self.__file_path = None
+
+ def set_jobject(self, jobject):
+ self.__jobject = jobject
+
+ def set_file_path(self, file_path):
+ self.__file_path = file_path
+
+ def get_file_path(self):
+ if self.__jobject != None:
+ return self.__jobject.get_file_path()
+ else:
+ return self.__file_path
+</pre>
+<h2> Using Journal Entries
+</h2>
+<p>When you're ready to read a file stored in a Journal object you can use the <em>get_file_path()</em> method of the Journal object to get a file path and open it for reading, like this:
+ <br/></p>
+<pre> fname = jobject.get_file_path()
+</pre>
+<p>One word of caution: be aware that this path does not exist until you call <em>get_file_path()</em> and will not exist long after.&#160; With the Journal you work with copies of files in the Journal, not the originals.&#160; For that reason you don't want to store the return value of <em>get_file_path()</em> for later use because later it may not be valid.&#160; Instead, store the Journal object itself and call the method right before you need the path.
+</p>
+<p>Metadata entries for Journal objects generally contain strings and work the way you would expect, with one exception, which is the <strong>preview</strong>.
+ <br/></p>
+<pre> def create_preview(self, object_id):
+ jobject = datastore.get(object_id)
+
+ if jobject.metadata.has_key('preview'):
+ preview = jobject.metadata['preview']
+ if preview is None or preview == '' or
+ preview == 'None':
+ if jobject.metadata['mime_type'].startswith(
+ 'image/'):
+ filename = jobject.get_file_path()
+ self.show_image(filename)
+ return
+ if jobject.metadata['mime_type'] == \
+ 'application/x-cbz':
+ filename = jobject.get_file_path()
+ fname = self.extract_image(filename)
+ self.show_image(fname)
+ os.remove(fname)
+ return
+
+ if jobject.metadata.has_key('preview') and \
+ len(jobject.metadata['preview']) &gt; 4:
+
+ if jobject.metadata['preview'][1:4] == 'PNG':
+ preview_data = jobject.metadata['preview']
+ else:
+ import base64
+ preview_data = base64.b64decode(
+ jobject.metadata['preview'])
+
+ loader = gtk.gdk.PixbufLoader()
+ loader.write(preview_data)
+ scaled_buf = loader.get_pixbuf()
+ loader.close()
+ self.image.set_from_pixbuf(scaled_buf)
+ self.image.show()
+ else:
+ self.image.clear()
+ self.image.show()
+</pre>
+<p> The <strong>preview </strong>metadata attribute is different in two ways:
+</p>
+<ul><li>We should never request <strong>preview</strong> as metadata to be returned in our list of Journal objects.&#160; We'll need to get a complete copy of the Journal object to get it.&#160; Since we already have a Journal object we can get the complete Journal object by getting its <strong>object id</strong> then requesting a new copy from the datastore using the id.</li>
+ <li>The preview image is a <strong>binary</strong> object (<strong>dbus.ByteArray</strong>) but in versions of Sugar older than .82 it will be stored as a text string.&#160; To accomplish this it is <strong>base 64 encoded</strong>.</li>
+</ul><p>The code you would use to get a complete copy of a Journal object looks like this:
+</p>
+<pre> object_id = jobject.object_id
+ jobject = datastore.get(object_id)
+</pre>
+<p>Now for an explanation of base 64 encoding.&#160; You've probably heard that computers use the base two numbering system, in which the only digits used are 1 and 0.&#160; A unit of data storage that can hold either a zero or a one is called a <strong>bit</strong>.&#160; Computers need to store information besides numbers, so to accomodate this we group bits into groups of 8 (usually) and these groups are called <strong>bytes</strong>.&#160; If you only use 7 of the 8 bits in a byte you can store a letter of the Roman alphabet, a punctuation mark, or a single digit, plus things like tabs and line feed characters.&#160; Any file that can be created using only 7 bits out of the 8 is called a <strong>text file</strong>.&#160; Everything that needs all 8 bits of each byte to make, including computer programs, movies, music, and pictures of Jessica Alba is a <strong>binary</strong>.&#160; In versions of Sugar before .82 Journal object metadata can only store text strings.&#160; Somehow we need to represent 8-bit bytes in 7 bits.&#160; We do this by grouping the bytes together into a larger collection of bits and then splitting them back out into groups of 7 bits.&#160; Python has the <strong>base64</strong> package to do this for us.
+</p>
+<p>Base 64 encoding is actually a pretty common technique.&#160; If you've ever sent an email with an attached file the file was base 64 encoded.
+</p>
+<p>The code above has a couple of ways of creating a preview image.&#160; If the preview metadata contains a PNG image it is loaded into a pixbuf and displayed.&#160; If there is no preview metadata but the MIME type is for an image file or a comic book zip file we create the preview from the Journal entry itself.
+ <br/></p>
+<p>The code checks the first three characters of the preview metadata to see if they are 'PNG'.&#160; If so, the file is a <strong>Portable Network Graphics</strong> image stored as a binary and does not need to be converted from base 64 encoding, otherwise it does.
+</p>
+<h2>Updating A Journal Object
+</h2>
+<p>The code to update a Journal object looks like this:
+</p>
+<pre> def update_entry(self):
+ needs_update = False
+
+ if self.selected_journal_entry is None:
+ return
+
+ object_id = self.selected_journal_entry.object_id
+ jobject = datastore.get(object_id)
+
+ old_title = jobject.metadata.get('title', None)
+ if old_title != self.title_entry.props.text:
+ jobject.metadata['title'] = \
+ self.title_entry.props.text
+ jobject.metadata['title_set_by_user'] = '1'
+ needs_update = True
+
+ old_tags = jobject.metadata.get('tags', None)
+ new_tags = \
+ self.tags_textview.props.buffer.props.text
+ if old_tags != new_tags:
+ jobject.metadata['tags'] = new_tags
+ needs_update = True
+
+ old_description = \
+ jobject.metadata.get('description', None)
+ new_description = \
+ self.description_textview.props.buffer.props.text
+ if old_description != new_description:
+ jobject.metadata['description'] = \
+ new_description
+ needs_update = True
+
+ if needs_update:
+ datastore.write(jobject, update_mtime=False,
+ reply_handler=self.datastore_write_cb,
+ error_handler=self.datastore_write_error_cb)
+ self.btn_save.props.sensitive = False
+
+ def datastore_write_cb(self):
+ pass
+
+ def datastore_write_error_cb(self, error):
+ logging.error(
+ 'sugarcommander.datastore_write_error_cb:'
+ ' %r' % error)
+</pre>
+<h2>Deleting A Journal Entry
+</h2>
+<p>The code to delete a Journal entry is this:
+</p>
+<pre> def delete_button_press_event_cb(self, entry, event):
+ datastore.delete(
+ self.selected_journal_entry.object_id)
+</pre>
+<h2>Getting Callbacks From The Journal Using D-Bus
+</h2>
+<p>In the chapter on <strong>Making Shared Activities</strong> we saw how D-Bus calls sent over Telepathy Tubes could be used to send messages from an Activity running on one computer to the same Activity running on a different computer.&#160;&#160; D-Bus is not normally used that way; typically it is used to send messages between programs running on the same computer.&#160;
+</p>
+<p>For example, if you're working with the Journal you can get callbacks whenever the Journal is updated.&#160; You get the callbacks whether the update was done by your Activity or elsewhere.&#160; If it is important for your Activity to know when the Journal has been updated you'll want to get these callbacks.
+</p>
+<p>The first thing you need to do is define some constants and import the dbus package:
+ <br/></p>
+<pre>DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore'
+DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore'
+DS_DBUS_PATH = '/org/laptop/sugar/DataStore'
+import dbus
+</pre>
+<p>Next, in your __init__() method put code to connect to the signals and do the callbacks:
+ <br/></p>
+<pre> bus = dbus.SessionBus()
+ remote_object = bus.get_object(
+ DS_DBUS_SERVICE, DS_DBUS_PATH)
+ _datastore = dbus.Interface(remote_object,
+ DS_DBUS_INTERFACE)
+ _datastore.connect_to_signal('Created',
+ self._datastore_created_cb)
+ _datastore.connect_to_signal('Updated',
+ self._datastore_updated_cb)
+ _datastore.connect_to_signal('Deleted',
+ self._datastore_deleted_cb)
+</pre>
+<p>The methods being run by the callbacks might look something like this:
+</p>
+<pre> def datastore_created_cb(self, uid):
+ new_jobject = datastore.get(uid)
+ iter = self.ls_journal.append()
+ title = new_jobject.metadata['title']
+ self.ls_journal.set(iter,
+ COLUMN_TITLE, title)
+ mime = new_jobject.metadata['mime_type']
+ self.ls_journal.set(iter,
+ COLUMN_MIME, mime)
+ self.ls_journal.set(iter,
+ COLUMN_JOBJECT, new_jobject)
+
+ def datastore_updated_cb(self, uid):
+ new_jobject = datastore.get(uid)
+ iter = self.ls_journal.get_iter_first()
+ for row in self.ls_journal:
+ jobject = row[COLUMN_JOBJECT]
+ if jobject.object_id == uid:
+ title = new_jobject.metadata['title']
+ self.ls_journal.set_value(iter,
+ COLUMN_TITLE, title)
+ break
+ iter = self.ls_journal.iter_next(iter)
+ object_id = \
+ self.selected_journal_entry.object_id
+ if object_id == uid:
+ self.set_form_fields(new_jobject)
+
+ def datastore_deleted_cb(self, uid):
+ save_path = self.selected_path
+ iter = self.ls_journal.get_iter_first()
+ for row in self.ls_journal:
+ jobject = row[COLUMN_JOBJECT]
+ if jobject.object_id == uid:
+ self.ls_journal.remove(iter)
+ break
+ iter = self.ls_journal.iter_next(iter)
+
+ try:
+ self.selection_journal.select_path(
+ save_path)
+ self.tv_journal.grab_focus()
+ except:
+ self.title_entry.set_text('')
+ description_textbuffer = \
+ self.description_textview.get_buffer()
+ description_textbuffer.set_text('')
+ tags_textbuffer = \
+ self.tags_textview.get_buffer()
+ tags_textbuffer.set_text('')
+ self.btn_save.props.sensitive = False
+ self.btn_delete.props.sensitive = False
+ self.image.clear()
+ self.image.show()
+</pre>
+<p>The <strong>uid</strong> passed to each callback method is the <strong>object id</strong> of the Journal object that has been added, updated, or deleted.&#160; If an entry is added to the Journal I get the Journal object from the datastore by its uid, then add it to the gtk.ListStore for the gtk.TreeModel I'm using to list out Journal entries.&#160; If an entry is updated or deleted I need to account for the possibility that the Journal entry I am viewing or editing may have been updated or removed.&#160;&#160;&#160; I use the uid to figure out which row in the gtk.ListStore needs to be removed or modified by looping through the entries in the gtk.ListStore looking for a match.
+ <br/></p>
+<p>Now you know everything you'll ever need to know to work with the Journal.
+ <br/></p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch020_making-activities-using-pygame.xhtml b/app/static/doc/myosa/ch020_making-activities-using-pygame.xhtml
new file mode 100644
index 0000000..f4ad6ee
--- /dev/null
+++ b/app/static/doc/myosa/ch020_making-activities-using-pygame.xhtml
@@ -0,0 +1,536 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>Making Activities Using PyGame
+</h1>
+<h2>Introduction
+</h2>
+<p><strong>PyGame</strong> and <strong>PyGTK</strong> are two different ways to make a Python program with a graphical user interface.&#160; Normally you would not use both in the same program.&#160; Each of them has its own way of creating a window and each has its own way of handling events.
+</p>
+<p>The base class Activity we have been using is an extension of the PyGTK Window class and uses PyGTK event handling.&#160; The toolbars all Activities use are PyGTK components.&#160; In short, any Activity written in Python must use PyGTK. &#160; Putting a PyGame program in the middle of a PyGTK program is a bit like putting a model ship in a bottle.&#160; Fortunately there is some Python code called <strong>SugarGame</strong> that will make it possible to do that.
+</p>
+<p>Before we figure out how we'll get it in the bottle, let's have a look at our ship.
+</p>
+<h2>Making A Standalone Game Using PyGame
+</h2>
+<p>As you might expect, it's a good idea to make a standalone Python game using PyGame before you make an Activity out of it.&#160; I am not an experienced PyGame developer, but using the tutorial <em>Rapid Game Development with Python</em> by Richard Jones at this URL:
+</p>
+<p><a href="http://richard.cgpublisher.com/product/pub.84/prod.11">http://richard.cgpublisher.com/product/pub.84/prod.11</a>
+ <br/></p>
+<p>I was able to put together a modest game in about a day.&#160; It would have been sooner but the tutorial examples had bugs in them and I had to spend a fair amount of time using <strong>The GIMP</strong> to create image files for the sprites in the game.&#160;
+</p>
+<p><strong>Sprites</strong> are small images, often animated, that represent objects in a game.&#160; They generally have a transparent background so they can be drawn on top of a background image.&#160; I used the <strong>PNG</strong> format for my sprite files because it supports having an <strong>alpha channel</strong> (another term that indicates that part of the image is transparent).
+</p>
+<p>PyGame has code to display background images, to create sprites and move them around on the background, and to detect when sprites collide with one another and do something when that happens.&#160; This is the basis for making a lot of 2D games.&#160; There are lots of games written with PyGame that could be easily adapted to be Sugar Activities.
+ <br/></p>
+<p>My game is similar to the car game in the tutorial, but instead of a car I have an airplane.&#160; The airplane is the <em>Demoiselle</em> created by Alberto Santos-Dumont in 1909.&#160; Instead of having "pads" to collide with I have four students of Otto Lilienthal hovering motionless in their hang gliders.&#160; The hang gliders pitch downwards when Santos-Dumont collides with them.&#160; The controls used for the game have been modified too.&#160; I use the Plus and Minus keys on both the main keyboard and the keypad, plus the keypad 9 and 3 keys, to open and close the throttle and the Up and Down arrows on both the main keyboard and the keypad to move the joystick forward and back.&#160; Using the keypad keys is useful for a couple of reasons.&#160; First, some versions of <strong>sugar-emulator</strong> don't recognize the arrow keys on the main keyboard.&#160; Second, the arrow keys on the keypad map to the game controller on the XO laptop, and the non-arrow keys on the keypad map to the other buttons on the XO laptop screen.&#160; These buttons can be used to play the game when the XO is in tablet mode.
+</p>
+<p>As a flight simulator it isn't much, but it does demonstrate at least some of the things PyGame can do.&#160; Here is the code for the game, which I'm calling <strong>Demoiselle</strong>:
+</p>
+<pre>#! /usr/bin/env python
+import pygame
+import math
+import sys
+
+class Demoiselle:
+ "This is a simple demonstration of using PyGame \
+ sprites and collision detection."
+ def __init__(self):
+ self.background = pygame.image.load('sky.jpg')
+ self.screen = pygame.display.get_surface()
+ self.screen.blit(self.background, (0, 0))
+ self.clock = pygame.time.Clock()
+ self.running = True
+
+ gliders = [
+ GliderSprite((200, 200)),
+ GliderSprite((800, 200)),
+ GliderSprite((200, 600)),
+ GliderSprite((800, 600)),
+ ]
+ self. glider_group = pygame.sprite.RenderPlain(
+ gliders)
+
+ def run(self):
+ "This method processes PyGame messages"
+ rect = self.screen.get_rect()
+ airplane = AirplaneSprite('demoiselle.png',
+ rect.center)
+ airplane_sprite = pygame.sprite.RenderPlain(
+ airplane)
+
+ while self.running:
+ self.clock.tick(30)
+
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ self.running = False
+ return
+ elif event.type == pygame.VIDEORESIZE:
+ pygame.display.set_mode(event.size,
+ pygame.RESIZABLE)
+ self.screen.blit(self.background,
+ (0, 0))
+
+ if not hasattr(event, 'key'):
+ continue
+ down = event.type == pygame.KEYDOWN
+ if event.key == pygame.K_DOWN or \
+ event.key == pygame.K_KP2:
+ airplane.joystick_back = down * 5
+ elif event.key == pygame.K_UP or \
+ event.key == pygame.K_KP8:
+ airplane.joystick_forward = down * -5
+ elif event.key == pygame.K_EQUALS or \
+ event.key == pygame.K_KP_PLUS or \
+ event.key == pygame.K_KP9:
+ airplane.throttle_up = down * 2
+ elif event.key == pygame.K_MINUS or \
+ event.key == pygame.K_KP_MINUS or \
+ event.key == pygame.K_KP3:
+ airplane.throttle_down = down * -2
+
+ self.glider_group.clear(self.screen,
+ self.background)
+ airplane_sprite.clear(self.screen,
+ self.background)
+ collisions = pygame.sprite.spritecollide(
+ airplane,
+ self.glider_group, False)
+ self.glider_group.update(collisions)
+ self.glider_group.draw(self.screen)
+ airplane_sprite.update()
+ airplane_sprite.draw(self.screen)
+ pygame.display.flip()
+
+class AirplaneSprite(pygame.sprite.Sprite):
+ "This class represents an airplane, the Demoiselle \
+ created by Alberto Santos-Dumont"
+ MAX_FORWARD_SPEED = 10
+ MIN_FORWARD_SPEED = 1
+ ACCELERATION = 2
+ TURN_SPEED = 5
+ def __init__(self, image, position):
+ pygame.sprite.Sprite.__init__(self)
+ self.src_image = pygame.image.load(image)
+ self.rect = pygame.Rect(
+ self.src_image.get_rect())
+ self.position = position
+ self.rect.center = self.position
+ self.speed = 1
+ self.direction = 0
+ self.joystick_back = self.joystick_forward = \
+ self.throttle_down = self.throttle_up = 0
+
+ def update(self):
+ "This method redraws the airplane in response\
+ to events."
+ self.speed += (self.throttle_up +
+ self.throttle_down)
+ if self.speed &gt; self.MAX_FORWARD_SPEED:
+ self.speed = self.MAX_FORWARD_SPEED
+ if self.speed &lt; self.MIN_FORWARD_SPEED:
+ self.speed = self.MIN_FORWARD_SPEED
+ self.direction += (self.joystick_forward + \
+ self.joystick_back)
+ x_coord, y_coord = self.position
+ rad = self.direction * math.pi / 180
+ x_coord += -self.speed * math.cos(rad)
+ y_coord += -self.speed * math.sin(rad)
+ screen = pygame.display.get_surface()
+ if y_coord &lt; 0:
+ y_coord = screen.get_height()
+
+ if x_coord &lt; 0:
+ x_coord = screen.get_width()
+
+ if x_coord &gt; screen.get_width():
+ x_coord = 0
+
+ if y_coord &gt; screen.get_height():
+ y_coord = 0
+ self.position = (x_coord, y_coord)
+ self.image = pygame.transform.rotate(
+ self.src_image, -self.direction)
+ self.rect = self.image.get_rect()
+ self.rect.center = self.position
+
+class GliderSprite(pygame.sprite.Sprite):
+ "This class represents an individual hang \
+ glider as developed by Otto Lilienthal."
+ def __init__(self, position):
+ pygame.sprite.Sprite.__init__(self)
+ self.normal = pygame.image.load(
+ 'glider_normal.png')
+ self.rect = pygame.Rect(self.normal.get_rect())
+ self.rect.center = position
+ self.image = self.normal
+ self.hit = pygame.image.load('glider_hit.png')
+ def update(self, hit_list):
+ "This method redraws the glider when it collides\
+ with the airplane and when it is no longer \
+ colliding with the airplane."
+ if self in hit_list:
+ self.image = self.hit
+ else:
+ self.image = self.normal
+
+def main():
+ "This function is called when the game is run \
+ from the command line"
+ pygame.init()
+ pygame.display.set_mode((0, 0), pygame.RESIZABLE)
+ game = Demoiselle()
+ game.run()
+ sys.exit(0)
+
+if __name__ == '__main__':
+ main()
+</pre>
+<p>And here is the game in action:
+</p>
+<p><img alt="The Demoiselle standalone game." src="static/ActivitiesGuideSugar-demoiselle1-en.jpg" height="454" width="600"/></p>
+<p>You'll find the code for this game in the file <strong>demoiselle.py</strong> in the book examples project in Git.
+</p>
+<h2>Introducing SugarGame
+</h2>
+<p><strong>SugarGame</strong> is not part of Sugar proper.&#160; If you want to use it you'll need to include the Python code for SugarGame inside your Activity bundle.&#160; I've included the version of SugarGame I'm using in the book examples project in the <strong>sugargame</strong> directory, but when you make your own games you'll want to be sure and get the latest code to include.&#160; You can do that by downloading the project from Gitorious using these commands:
+</p>
+<pre><code>mkdir sugargame
+cd sugargame
+git clone git://git.sugarlabs.org/sugargame/mainline.git</code></pre>
+<p>You'll see two subdirectories in this project: <strong>sugargame</strong> and <strong>test</strong>, plus a <strong>README.txt</strong> file that contains information on using sugargame in your own Activities.&#160; The test directory contains a simple PyGame program that can be run either standalone or as an Activity.&#160; The standalone program is in the file named <strong>TestGame.py</strong>.&#160; The Activity, which is a sort of wrapper around the standalone version, is in file <strong>TestActivity.py</strong>.
+ <br/></p>
+<p>If you run <strong>TestGame.py</strong> from the command line you'll see it displays a bouncing ball on a white background.&#160; To try running the Activity version you'll need to run
+</p>
+<pre>./setup.py dev</pre>
+<p>from the command line first.&#160; I was not able to get the Activity to work under sugar-emulator until I made two changes to it:
+</p>
+<ul><li>I made a copy of the <strong>sugargame</strong> directory within the <strong>test</strong> directory.</li>
+ <li>I removed the line reading "<strong>sys.path.append(<span class="String"><span class="String">'</span>..<span class="String">'</span></span>) </strong><span class="Comment"><strong><span class="Comment">#</span> Import sugargame package from top directory.</strong>" from <strong>TestActivity.py</strong>.&#160; Obviously this line is supposed to help the program find the <strong>sugargame</strong> directory in the project but it didn't work in Fedora 10.&#160; Your own experience may be different.</span>
+ <br/></li>
+</ul><p><span class="Comment">The Activity looks like this:</span>
+</p>
+<p><span class="Comment"><img alt="The SugarGame demo Activity" src="static/ActivitiesGuideSugar-sugargame_1-en.jpg" height="453" width="600"/></span>
+</p>
+<p><span class="Comment">The <strong>PyGame</strong> toolbar has a single button that lets you make the bouncing ball pause and resume bouncing.</span>
+</p>
+<h2><span class="Comment">Making A Sugar Activity Out Of A PyGame Program</span>
+</h2>
+<p><span class="Comment">Now it's time to put our ship in that bottle.&#160; The first thing we need to do is make a copy of the <strong>sugargame</strong> directory of the SugarGame project into the mainline directory of our own project.</span>
+</p>
+<p><span class="Comment">The <strong>README.txt</strong> file in the SugarGame project is worth reading.&#160; It tells us to make an Activity based on the <strong>TestActivity.py</strong> example in the SugarGame project.&#160; This will be our bottle.&#160; Here is the code for mine, which is named <strong>DemoiselleActivity.py</strong>:</span>
+</p>
+<pre># DemoiselleActivity.py
+
+from gettext import gettext as _
+
+import gtk
+import pygame
+from sugar.activity import activity
+from sugar.graphics.toolbutton import ToolButton
+import gobject
+import sugargame.canvas
+import demoiselle2
+
+class DemoiselleActivity(activity.Activity):
+ def __init__(self, handle):
+ super(DemoiselleActivity, self).__init__(handle)
+
+ # Build the activity toolbar.
+ self.build_toolbar()
+
+ # Create the game instance.
+ self.game = demoiselle2.Demoiselle()
+
+ # Build the Pygame canvas.
+ self._pygamecanvas = \
+ sugargame.canvas.PygameCanvas(self)
+ # Note that set_canvas implicitly calls
+ # read_file when resuming from the Journal.
+ self.set_canvas(self._pygamecanvas)
+ self.score = ''
+
+ # Start the game running.
+ self._pygamecanvas.run_pygame(self.game.run)
+
+ def build_toolbar(self):
+ toolbox = activity.ActivityToolbox(self)
+ activity_toolbar = toolbox.get_activity_toolbar()
+ activity_toolbar.keep.props.visible = False
+ activity_toolbar.share.props.visible = False
+
+ self.view_toolbar = ViewToolbar()
+ toolbox.add_toolbar(_('View'), self.view_toolbar)
+ self.view_toolbar.connect('go-fullscreen',
+ self.view_toolbar_go_fullscreen_cb)
+ self.view_toolbar.show()
+
+ toolbox.show()
+ self.set_toolbox(toolbox)
+
+ def view_toolbar_go_fullscreen_cb(self, view_toolbar):
+ self.fullscreen()
+
+ def read_file(self, file_path):
+ score_file = open(file_path, "r")
+ while score_file:
+ self.score = score_file.readline()
+ self.game.set_score(int(self.score))
+ score_file.close()
+
+ def write_file(self, file_path):
+ score = self.game.get_score()
+ f = open(file_path, 'wb')
+ try:
+ f.write(str(score))
+ finally:
+ f.close
+
+class ViewToolbar(gtk.Toolbar):
+ __gtype_name__ = 'ViewToolbar'
+
+ __gsignals__ = {
+ 'needs-update-size': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([])),
+ 'go-fullscreen': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([]))
+ }
+
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+ self.fullscreen = ToolButton('view-fullscreen')
+ self.fullscreen.set_tooltip(_('Fullscreen'))
+ self.fullscreen.connect('clicked',
+ self.fullscreen_cb)
+ self.insert(self.fullscreen, -1)
+ self.fullscreen.show()
+
+ def fullscreen_cb(self, button):
+ self.emit('go-fullscreen')
+</pre>
+<p><span class="Comment">This is a bit fancier than <strong>TestActivity.py</strong>.&#160; I decided that my game didn't really need to be paused and resumed, so I replaced the <strong>PyGame</strong> toolbar with a <strong>View</strong> toolbar that lets the user hide the toolbar when it is not needed.&#160; I use the <em>read_file()</em> and <em>write_file()</em> methods to save and restore the game score.&#160; (Actually this is faked, because I never put in any scoring logic in the game).&#160; I also hide the <strong>Keep</strong> and <strong>Share</strong> controls in the main toolbar.</span>
+</p>
+<p><span class="Comment">As you would expect, getting a ship in a bottle does require the ship to be modified.&#160; Here is <strong>demoiselle2.py</strong>, which has the modifications:</span>
+</p>
+<pre>#! /usr/bin/env python
+import pygame
+import gtk
+import math
+import sys
+
+class Demoiselle:
+ "This is a simple demonstration of using PyGame \
+ sprites and collision detection."
+ def __init__(self):
+ self.clock = pygame.time.Clock()
+ self.running = True
+ self.background = pygame.image.load('sky.jpg')
+
+ def get_score(self):
+ return '99'
+
+ def run(self):
+ "This method processes PyGame messages"
+
+ screen = pygame.display.get_surface()
+ screen.blit(self.background, (0, 0))
+
+ gliders = [
+ GliderSprite((200, 200)),
+ GliderSprite((800, 200)),
+ GliderSprite((200, 600)),
+ GliderSprite((800, 600)),
+ ]
+ glider_group = pygame.sprite.RenderPlain(gliders)
+
+ rect = screen.get_rect()
+ airplane = AirplaneSprite('demoiselle.png',
+ rect.center)
+ airplane_sprite = pygame.sprite.RenderPlain(
+ airplane)
+
+ while self.running:
+ self.clock.tick(30)
+
+ # Pump GTK messages.
+ while gtk.events_pending():
+ gtk.main_iteration()
+
+ # Pump PyGame messages.
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ self.running = False
+ return
+ elif event.type == pygame.VIDEORESIZE:
+ pygame.display.set_mode(event.size,
+ pygame.RESIZABLE)
+ screen.blit(self.background, (0, 0))
+
+ if not hasattr(event, 'key'):
+ continue
+ down = event.type == pygame.KEYDOWN
+ if event.key == pygame.K_DOWN or \
+ event.key == pygame.K_KP2:
+ airplane.joystick_back = down * 5
+ elif event.key == pygame.K_UP or \
+ event.key == pygame.K_KP8:
+ airplane.joystick_forward = down * -5
+ elif event.key == pygame.K_EQUALS or \
+ event.key == pygame.K_KP_PLUS or \
+ event.key == pygame.K_KP9:
+ airplane.throttle_up = down * 2
+ elif event.key == pygame.K_MINUS or \
+ event.key == pygame.K_KP_MINUS or \
+ event.key == pygame.K_KP3:
+ airplane.throttle_down = down * -2
+
+ glider_group.clear(screen, self.background)
+ airplane_sprite.clear(screen, self.background)
+ collisions = pygame.sprite.spritecollide(
+ airplane,
+ glider_group, False)
+ glider_group.update(collisions)
+ glider_group.draw(screen)
+ airplane_sprite.update()
+ airplane_sprite.draw(screen)
+ pygame.display.flip()
+
+class AirplaneSprite(pygame.sprite.Sprite):
+ "This class represents an airplane, the Demoiselle \
+ created by Alberto Santos-Dumont"
+ MAX_FORWARD_SPEED = 10
+ MIN_FORWARD_SPEED = 1
+ ACCELERATION = 2
+ TURN_SPEED = 5
+ def __init__(self, image, position):
+ pygame.sprite.Sprite.__init__(self)
+ self.src_image = pygame.image.load(image)
+ self.rect = pygame.Rect(self.src_image.get_rect())
+ self.position = position
+ self.rect.center = self.position
+ self.speed = 1
+ self.direction = 0
+ self.joystick_back = self.joystick_forward = \
+ self.throttle_down = self.throttle_up = 0
+
+ def update(self):
+ "This method redraws the airplane in response\
+ to events."
+ self.speed += (self.throttle_up +
+ self.throttle_down)
+ if self.speed &gt; self.MAX_FORWARD_SPEED:
+ self.speed = self.MAX_FORWARD_SPEED
+ if self.speed &lt; self.MIN_FORWARD_SPEED:
+ self.speed = self.MIN_FORWARD_SPEED
+ self.direction += (self.joystick_forward +
+ self.joystick_back)
+ x_coord, y_coord = self.position
+ rad = self.direction * math.pi / 180
+ x_coord += -self.speed * math.cos(rad)
+ y_coord += -self.speed * math.sin(rad)
+ screen = pygame.display.get_surface()
+ if y_coord &lt; 0:
+ y_coord = screen.get_height()
+
+ if x_coord &lt; 0:
+ x_coord = screen.get_width()
+
+ if x_coord &gt; screen.get_width():
+ x_coord = 0
+
+ if y_coord &gt; screen.get_height():
+ y_coord = 0
+ self.position = (x_coord, y_coord)
+ self.image = pygame.transform.rotate(
+ self.src_image, -self.direction)
+ self.rect = self.image.get_rect()
+ self.rect.center = self.position
+
+class GliderSprite(pygame.sprite.Sprite):
+ "This class represents an individual hang \
+ glider as developed by Otto Lilienthal."
+ def __init__(self, position):
+ pygame.sprite.Sprite.__init__(self)
+ self.normal = pygame.image.load(
+ 'glider_normal.png')
+ self.rect = pygame.Rect(self.normal.get_rect())
+ self.rect.center = position
+ self.image = self.normal
+ self.hit = pygame.image.load('glider_hit.png')
+ def update(self, hit_list):
+ "This method redraws the glider when it collides\
+ with the airplane and when it is no longer \
+ colliding with the airplane."
+ if self in hit_list:
+ self.image = self.hit
+ else:
+ self.image = self.normal
+
+def main():
+ "This function is called when the game is run \
+ from the command line"
+ pygame.init()
+ pygame.display.set_mode((0, 0), pygame.RESIZABLE)
+ game = Demoiselle()
+ game.run()
+ sys.exit(0)
+
+if __name__ == '__main__':
+ main()
+</pre>
+<p><span class="Comment">Why not load both <strong>demoiselle.py</strong> and <strong>demoiselle2.py</strong> in Eric and take a few minutes to see if you can figure out what changed between the two versions?</span>
+</p>
+<p><span class="Comment">Surprisingly little is different.&#160; I added some code to the PyGame main loop to check for PyGTK events and deal with them:</span>
+</p>
+<pre> while self.running:
+ self.clock.tick(30)
+
+ <strong># Pump GTK messages.
+ while gtk.events_pending():
+ gtk.main_iteration()</strong>
+
+ # Pump PyGame messages.
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ self.running = False
+ return
+ elif event.type == pygame.VIDEORESIZE:
+ pygame.display.set_mode(event.size,
+ pygame.RESIZABLE)
+ screen.blit(self.background, (0, 0))
+
+ if not hasattr(event, 'key'):
+ continue
+ down = event.type == pygame.KEYDOWN
+ if event.key == pygame.K_DOWN or \
+
+<em>... continue dealing with PyGame events ...</em>
+</pre>
+<p><span class="Comment">This has the effect of making PyGame and PyGTK take turns handling events.&#160; If this code was not present GTK events would be ignored and you'd have no way to close the Activity, hide the toolbar, etc.&#160; You need to add <strong>import gtk</strong> at the top of the file so these methods can be found.</span>
+</p>
+<p><span class="Comment">Of course I also added the methods to set and return scores:</span>
+</p>
+<pre>&#160; def get_score(self):
+ return self.score
+
+ def set_score(self, score):
+ self.score = score
+</pre>
+<p><span class="Comment">The biggest change is in the <em>__init__()</em> method of the <strong>Demoiselle</strong> class.&#160; Originally I had code to display the background image on the screen:</span>
+</p>
+<pre> def __init__(self):
+ self.background = pygame.image.load('sky.jpg')
+ self.screen = pygame.display.get_surface()
+ self.screen.blit(self.background, (0, 0))
+</pre>
+<p><span class="Comment">The problem with this is that sugargame is going to create a special PyGTK Canvas object to replace the PyGame display and the DemoiselleActivity code hasn't done that yet, so <strong>self.screen</strong> will have a value of None.&#160; The only way to get around that is to move any code that refers to the <strong>display</strong> out of the <em>__init__()</em> method of the class and into the beginning of the method that contains the event loop.&#160; This may leave you with an <em>__init__()</em> method that does little or nothing.&#160; About the only thing you'll want there is code to create instance variables.</span>
+</p>
+<p><span class="Comment">Nothing we have done to <strong>demoiselle2.py</strong> will prevent it from being run as a standalone Python program.</span>
+</p>To try out the game <span class="Comment">run <strong>./setup.py dev</strong> from within the <strong>Making_Activities_Using_PyGame</strong> directory.&#160; When you try out the Activity it should look like this:</span>
+<p>
+</p>
+<p><span class="Comment"><img alt="The Demoiselle Activity." src="static/ActivitiesGuideSugar-demoiselle2_1-en.jpg" height="448" width="600"/><br/></span>
+</p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch021_making-new-style-toolbars.xhtml b/app/static/doc/myosa/ch021_making-new-style-toolbars.xhtml
new file mode 100644
index 0000000..73e662e
--- /dev/null
+++ b/app/static/doc/myosa/ch021_making-new-style-toolbars.xhtml
@@ -0,0 +1,1150 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>Making New Style Toolbars
+</h1>
+<h2>Introduction
+</h2>
+<p>They say "There's no Toolbar like an old Toolbar" and if your users are not running the very latest version of Sugar they're right.&#160; Activities will need to support the original style toolbars for some time to come.&#160; However, it is possible to make an Activity that supports both and that is what we'll do in this chapter.
+</p>
+<p>The new style toolbars came about because of problems with the old toolbars.&#160; Activity users were having a hard time figuring out how to quit an Activity because the <strong>Close</strong> button is only on the Activity toolbar.&#160; If the Activity starts on a different toolbar, as many do, it is not obvious that you need to switch to the Activity toolbar to quit the Activity.&#160; Another issue brought up was that the Tabs for the toolbars took up screen real estate that could be better used elsewhere.&#160; Let's compare toolbars for similar Activities.&#160; First, the old style toolbar for <strong>Read Etexts</strong>:
+</p>
+<p><img alt="Old style toolbar, Read Etexts" src="static/ActivitiesGuideSugar-oldtoolbar-en.jpg" width="600" height="80"/></p>
+<p>Now compare it with the new style toolbar for the <strong>Read</strong> Activity:
+</p>
+<p><img alt="New style toolbar, closed." src="static/ActivitiesGuideSugar-newtoolbar1_1-en.jpg" width="600" height="48"/></p>
+<p>This is thinner than the older version and the <strong>Close</strong> button is always visible.&#160; Some functions are on the main toolbar and others are attached to toolbars that drop down when you click on their icon.&#160; First, the new Activity drop down toolbar:
+</p>
+<p><img alt="The Activity drop down menu." src="static/ActivitiesGuideSugar-newtoolbar2-en.jpg" width="600" height="87"/></p>
+<p>Next the <strong>Edit</strong> toolbar:
+</p>
+<p><img alt="New Edit Toolbar" src="static/ActivitiesGuideSugar-newtoolbar3-en.jpg" width="598" height="90"/></p>
+<p>Finally, the <strong>View</strong> toolbar:
+</p>
+<p><img alt="The new View Toolbar." src="static/ActivitiesGuideSugar-newtoolbar4-en.jpg" width="600" height="100"/></p>
+<h2> Adding New Style Toolbars to Read Etexts II
+</h2>
+<p>When working on the original <strong>Read Etexts</strong> Activity I borrowed a lot of user interface code from the original <strong>Read</strong> Activity and I see no reason to stop doing that now.&#160; One complication to doing this is that <strong>Read</strong> has some dependencies that prevent the latest version of <strong>Read</strong> from working with older versions of Sugar, and that being the case there is no need at all for Read to support both old and new toolbars.&#160; <strong>Read Etexts IV</strong> will not be so fortunate; it will need to figure out at runtime what kind of toolbar is supported and use that.
+</p>
+<p>I am able to test the Activity with both old and new style toolbars on the same box because I'm running Fedora 11, which has an installed Sugar environment that supports the old toolbars, plus I have downloaded and run <strong>sugar-jhbuild</strong>, which supports the new toolbars in its version of Sugar.
+</p>
+<p>Here is the code for <strong>ReadEtextsActivity4.py</strong>:
+</p>
+<pre>import os
+import re
+import logging
+import time
+import zipfile
+import gtk
+import pango
+import dbus
+import gobject
+import telepathy
+from sugar.activity import activity
+
+from sugar.graphics.toolbutton import ToolButton
+
+_NEW_TOOLBAR_SUPPORT = True
+try:
+ from sugar.graphics.toolbarbox import ToolbarBox
+ from sugar.graphics.toolbarbox import ToolbarButton
+ from sugar.activity.widgets import StopButton
+ from toolbar import ViewToolbar
+ from mybutton import MyActivityToolbarButton
+except:
+ _NEW_TOOLBAR_SUPPORT = False
+ from toolbar import ReadToolbar, ViewToolbar
+
+from sugar.graphics.toggletoolbutton import ToggleToolButton
+from sugar.graphics.menuitem import MenuItem
+
+from sugar.graphics import style
+from sugar import network
+from sugar.datastore import datastore
+from sugar.graphics.alert import NotifyAlert
+from gettext import gettext as _
+
+page=0
+PAGE_SIZE = 45
+TOOLBAR_READ = 2
+
+logger = logging.getLogger('read-etexts2-activity')
+
+class ReadHTTPRequestHandler(
+ network.ChunkedGlibHTTPRequestHandler):
+ """HTTP Request Handler for transferring document while
+ collaborating.
+
+ RequestHandler class that integrates with Glib mainloop.
+ It writes the specified file to the client in chunks,
+ returning control to the mainloop between chunks.
+
+ """
+ def translate_path(self, path):
+ """Return the filepath to the shared document."""
+ return self.server.filepath
+
+
+class ReadHTTPServer(network.GlibTCPServer):
+ """HTTP Server for transferring document while
+ collaborating."""
+ def __init__(self, server_address, filepath):
+ """Set up the GlibTCPServer with the
+ ReadHTTPRequestHandler.
+
+ filepath -- path to shared document to be served.
+ """
+ self.filepath = filepath
+ network.GlibTCPServer.__init__(self,
+ server_address,
+ ReadHTTPRequestHandler)
+
+
+class ReadURLDownloader(network.GlibURLDownloader):
+ """URLDownloader that provides content-length
+ and content-type."""
+
+ def get_content_length(self):
+ """Return the content-length of the download."""
+ if self._info is not None:
+ return int(self._info.headers.get(
+ 'Content-Length'))
+
+ def get_content_type(self):
+ """Return the content-type of the download."""
+ if self._info is not None:
+ return self._info.headers.get('Content-type')
+ return None
+
+READ_STREAM_SERVICE = 'read-etexts-activity-http'
+
+class ReadEtextsActivity(activity.Activity):
+ def __init__(self, handle):
+ "The entry point to the Activity"
+ global page
+ activity.Activity.__init__(self, handle)
+
+ self.fileserver = None
+ self.object_id = handle.object_id
+
+ if _NEW_TOOLBAR_SUPPORT:
+ self.create_new_toolbar()
+ else:
+ self.create_old_toolbar()
+
+ self.scrolled_window = gtk.ScrolledWindow()
+ self.scrolled_window.set_policy(gtk.POLICY_NEVER,
+ gtk.POLICY_AUTOMATIC)
+ self.scrolled_window.props.shadow_type = \
+ gtk.SHADOW_NONE
+
+ self.textview = gtk.TextView()
+ self.textview.set_editable(False)
+ self.textview.set_cursor_visible(False)
+ self.textview.set_left_margin(50)
+ self.textview.connect("key_press_event",
+ self.keypress_cb)
+
+ self.progressbar = gtk.ProgressBar()
+ self.progressbar.set_orientation(
+ gtk.PROGRESS_LEFT_TO_RIGHT)
+ self.progressbar.set_fraction(0.0)
+
+ self.scrolled_window.add(self.textview)
+ self.textview.show()
+ self.scrolled_window.show()
+
+ vbox = gtk.VBox()
+ vbox.pack_start(self.progressbar, False,
+ False, 10)
+ vbox.pack_start(self.scrolled_window)
+ self.set_canvas(vbox)
+ vbox.show()
+
+ page = 0
+ self.clipboard = gtk.Clipboard(
+ display=gtk.gdk.display_get_default(),
+ selection="CLIPBOARD")
+ self.textview.grab_focus()
+ self.font_desc = pango.FontDescription(
+ "sans %d" % style.zoom(10))
+ self.textview.modify_font(self.font_desc)
+
+ buffer = self.textview.get_buffer()
+ self.markset_id = buffer.connect("mark-set",
+ self.mark_set_cb)
+
+ self.unused_download_tubes = set()
+ self.want_document = True
+ self.download_content_length = 0
+ self.download_content_type = None
+ # Status of temp file used for write_file:
+ self.tempfile = None
+ self.close_requested = False
+ self.connect("shared", self.shared_cb)
+
+ self.is_received_document = False
+
+ if self._shared_activity and \
+ handle.object_id == None:
+ # We're joining, and we don't already have
+ # the document.
+ if self.get_shared():
+ # Already joined for some reason,
+ # just get the document
+ self.joined_cb(self)
+ else:
+ # Wait for a successful join before
+ # trying to get the document
+ self.connect("joined", self.joined_cb)
+
+ def create_old_toolbar(self):
+ toolbox = activity.ActivityToolbox(self)
+ activity_toolbar = toolbox.get_activity_toolbar()
+ activity_toolbar.keep.props.visible = False
+
+ self.edit_toolbar = activity.EditToolbar()
+ self.edit_toolbar.undo.props.visible = False
+ self.edit_toolbar.redo.props.visible = False
+ self.edit_toolbar.separator.props.visible = False
+ self.edit_toolbar.copy.set_sensitive(False)
+ self.edit_toolbar.copy.connect('clicked',
+ self.edit_toolbar_copy_cb)
+ self.edit_toolbar.paste.props.visible = False
+ toolbox.add_toolbar(_('Edit'), self.edit_toolbar)
+ self.edit_toolbar.show()
+
+ self.read_toolbar = ReadToolbar()
+ toolbox.add_toolbar(_('Read'), self.read_toolbar)
+ self.read_toolbar.back.connect('clicked',
+ self.go_back_cb)
+ self.read_toolbar.forward.connect('clicked',
+ self.go_forward_cb)
+ self.read_toolbar.num_page_entry.connect('activate',
+ self.num_page_entry_activate_cb)
+ self.read_toolbar.show()
+
+ self.view_toolbar = ViewToolbar()
+ toolbox.add_toolbar(_('View'), self.view_toolbar)
+ self.view_toolbar.connect('go-fullscreen',
+ self.view_toolbar_go_fullscreen_cb)
+ self.view_toolbar.zoom_in.connect('clicked',
+ self.zoom_in_cb)
+ self.view_toolbar.zoom_out.connect('clicked',
+ self.zoom_out_cb)
+ self.view_toolbar.show()
+
+ self.set_toolbox(toolbox)
+ toolbox.show()
+ self.toolbox.set_current_toolbar(TOOLBAR_READ)
+
+ def create_new_toolbar(self):
+ toolbar_box = ToolbarBox()
+
+ activity_button = MyActivityToolbarButton(self)
+ toolbar_box.toolbar.insert(activity_button, 0)
+ activity_button.show()
+
+ self.edit_toolbar = activity.EditToolbar()
+ self.edit_toolbar.undo.props.visible = False
+ self.edit_toolbar.redo.props.visible = False
+ self.edit_toolbar.separator.props.visible = False
+ self.edit_toolbar.copy.set_sensitive(False)
+ self.edit_toolbar.copy.connect('clicked',
+ self.edit_toolbar_copy_cb)
+ self.edit_toolbar.paste.props.visible = False
+
+ edit_toolbar_button = ToolbarButton(
+ page=self.edit_toolbar,
+ icon_name='toolbar-edit')
+ self.edit_toolbar.show()
+ toolbar_box.toolbar.insert(edit_toolbar_button, -1)
+ edit_toolbar_button.show()
+
+ self.view_toolbar = ViewToolbar()
+ self.view_toolbar.connect('go-fullscreen',
+ self.view_toolbar_go_fullscreen_cb)
+ self.view_toolbar.zoom_in.connect('clicked',
+ self.zoom_in_cb)
+ self.view_toolbar.zoom_out.connect('clicked',
+ self.zoom_out_cb)
+ self.view_toolbar.show()
+ view_toolbar_button = ToolbarButton(
+ page=self.view_toolbar,
+ icon_name='toolbar-view')
+ toolbar_box.toolbar.insert(view_toolbar_button, -1)
+ view_toolbar_button.show()
+
+ self.back = ToolButton('go-previous')
+ self.back.set_tooltip(_('Back'))
+ self.back.props.sensitive = False
+ self.back.connect('clicked', self.go_back_cb)
+ toolbar_box.toolbar.insert(self.back, -1)
+ self.back.show()
+
+ self.forward = ToolButton('go-next')
+ self.forward.set_tooltip(_('Forward'))
+ self.forward.props.sensitive = False
+ self.forward.connect('clicked',
+ self.go_forward_cb)
+ toolbar_box.toolbar.insert(self.forward, -1)
+ self.forward.show()
+
+ num_page_item = gtk.ToolItem()
+ self.num_page_entry = gtk.Entry()
+ self.num_page_entry.set_text('0')
+ self.num_page_entry.set_alignment(1)
+ self.num_page_entry.connect('insert-text',
+ self.__new_num_page_entry_insert_text_cb)
+ self.num_page_entry.connect('activate',
+ self.__new_num_page_entry_activate_cb)
+ self.num_page_entry.set_width_chars(4)
+ num_page_item.add(self.num_page_entry)
+ self.num_page_entry.show()
+ toolbar_box.toolbar.insert(num_page_item, -1)
+ num_page_item.show()
+
+ total_page_item = gtk.ToolItem()
+ self.total_page_label = gtk.Label()
+
+ label_attributes = pango.AttrList()
+ label_attributes.insert(pango.AttrSize(
+ 14000, 0, -1))
+ label_attributes.insert(pango.AttrForeground(
+ 65535, 65535, 65535, 0, -1))
+ self.total_page_label.set_attributes(
+ label_attributes)
+
+ self.total_page_label.set_text(' / 0')
+ total_page_item.add(self.total_page_label)
+ self.total_page_label.show()
+ toolbar_box.toolbar.insert(total_page_item, -1)
+ total_page_item.show()
+
+ separator = gtk.SeparatorToolItem()
+ separator.props.draw = False
+ separator.set_expand(True)
+ toolbar_box.toolbar.insert(separator, -1)
+ separator.show()
+
+ stop_button = StopButton(self)
+ stop_button.props.accelerator = '&lt;Ctrl&gt;&lt;Shift&gt;Q'
+ toolbar_box.toolbar.insert(stop_button, -1)
+ stop_button.show()
+
+ self.set_toolbar_box(toolbar_box)
+ toolbar_box.show()
+
+ def __new_num_page_entry_insert_text_cb(self, entry,
+ text, length, position):
+ if not re.match('[0-9]', text):
+ entry.emit_stop_by_name('insert-text')
+ return True
+ return False
+
+ def __new_num_page_entry_activate_cb(self, entry):
+ global page
+ if entry.props.text:
+ new_page = int(entry.props.text) - 1
+ else:
+ new_page = 0
+
+ if new_page &gt;= self.total_pages:
+ new_page = self.total_pages - 1
+ elif new_page &lt; 0:
+ new_page = 0
+
+ self.current_page = new_page
+ self.set_current_page(new_page)
+ self.show_page(new_page)
+ entry.props.text = str(new_page + 1)
+ self.update_nav_buttons()
+ page = new_page
+
+ def update_nav_buttons(self):
+ current_page = self.current_page
+ self.back.props.sensitive = current_page &gt; 0
+ self.forward.props.sensitive = \
+ current_page &lt; self.total_pages - 1
+
+ self.num_page_entry.props.text = str(
+ current_page + 1)
+ self.total_page_label.props.label = \
+ ' / ' + str(self.total_pages)
+
+ def set_total_pages(self, pages):
+ self.total_pages = pages
+
+ def set_current_page(self, page):
+ self.current_page = page
+ self.update_nav_buttons()
+
+ def keypress_cb(self, widget, event):
+ "Respond when the user presses one of the \
+ arrow keys"
+ keyname = gtk.gdk.keyval_name(event.keyval)
+ print keyname
+ if keyname == 'plus':
+ self.font_increase()
+ return True
+ if keyname == 'minus':
+ self.font_decrease()
+ return True
+ if keyname == 'Page_Up' :
+ self.page_previous()
+ return True
+ if keyname == 'Page_Down':
+ self.page_next()
+ return True
+ if keyname == 'Up' or keyname == 'KP_Up' \
+ or keyname == 'KP_Left':
+ self.scroll_up()
+ return True
+ if keyname == 'Down' or keyname == 'KP_Down' \
+ or keyname == 'KP_Right':
+ self.scroll_down()
+ return True
+ return False
+
+ def num_page_entry_activate_cb(self, entry):
+ global page
+ if entry.props.text:
+ new_page = int(entry.props.text) - 1
+ else:
+ new_page = 0
+
+ if new_page &gt;= self.read_toolbar.total_pages:
+ new_page = self.read_toolbar.total_pages - 1
+ elif new_page &lt; 0:
+ new_page = 0
+
+ self.read_toolbar.current_page = new_page
+ self.read_toolbar.set_current_page(new_page)
+ self.show_page(new_page)
+ entry.props.text = str(new_page + 1)
+ self.read_toolbar.update_nav_buttons()
+ page = new_page
+
+ def go_back_cb(self, button):
+ self.page_previous()
+
+ def go_forward_cb(self, button):
+ self.page_next()
+
+ def page_previous(self):
+ global page
+ page=page-1
+ if page &lt; 0: page=0
+ if _NEW_TOOLBAR_SUPPORT:
+ self.set_current_page(page)
+ else:
+ self.read_toolbar.set_current_page(page)
+ self.show_page(page)
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ v_adjustment.value = v_adjustment.upper - \
+ v_adjustment.page_size
+
+ def page_next(self):
+ global page
+ page=page+1
+ if page &gt;= len(self.page_index): page=0
+ if _NEW_TOOLBAR_SUPPORT:
+ self.set_current_page(page)
+ else:
+ self.read_toolbar.set_current_page(page)
+ self.show_page(page)
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ v_adjustment.value = v_adjustment.lower
+
+ def zoom_in_cb(self, button):
+ self.font_increase()
+
+ def zoom_out_cb(self, button):
+ self.font_decrease()
+
+ def font_decrease(self):
+ font_size = self.font_desc.get_size() / 1024
+ font_size = font_size - 1
+ if font_size &lt; 1:
+ font_size = 1
+ self.font_desc.set_size(font_size * 1024)
+ self.textview.modify_font(self.font_desc)
+
+ def font_increase(self):
+ font_size = self.font_desc.get_size() / 1024
+ font_size = font_size + 1
+ self.font_desc.set_size(font_size * 1024)
+ self.textview.modify_font(self.font_desc)
+
+ def mark_set_cb(self, textbuffer, iter, textmark):
+
+ if textbuffer.get_has_selection():
+ begin, end = textbuffer.get_selection_bounds()
+ self.edit_toolbar.copy.set_sensitive(True)
+ else:
+ self.edit_toolbar.copy.set_sensitive(False)
+
+ def edit_toolbar_copy_cb(self, button):
+ textbuffer = self.textview.get_buffer()
+ begin, end = textbuffer.get_selection_bounds()
+ copy_text = textbuffer.get_text(begin, end)
+ self.clipboard.set_text(copy_text)
+
+ def view_toolbar_go_fullscreen_cb(self, view_toolbar):
+ self.fullscreen()
+
+ def scroll_down(self):
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ if v_adjustment.value == v_adjustment.upper - \
+ v_adjustment.page_size:
+ self.page_next()
+ return
+ if v_adjustment.value &lt; v_adjustment.upper - \
+ v_adjustment.page_size:
+ new_value = v_adjustment.value + \
+ v_adjustment.step_increment
+ if new_value &gt; v_adjustment.upper - \
+ v_adjustment.page_size:
+ new_value = v_adjustment.upper - \
+ v_adjustment.page_size
+ v_adjustment.value = new_value
+
+ def scroll_up(self):
+ v_adjustment = \
+ self.scrolled_window.get_vadjustment()
+ if v_adjustment.value == v_adjustment.lower:
+ self.page_previous()
+ return
+ if v_adjustment.value &gt; v_adjustment.lower:
+ new_value = v_adjustment.value - \
+ v_adjustment.step_increment
+ if new_value &lt; v_adjustment.lower:
+ new_value = v_adjustment.lower
+ v_adjustment.value = new_value
+
+ def show_page(self, page_number):
+ global PAGE_SIZE, current_word
+ position = self.page_index[page_number]
+ self.etext_file.seek(position)
+ linecount = 0
+ label_text = '\n\n\n'
+ textbuffer = self.textview.get_buffer()
+ while linecount &lt; PAGE_SIZE:
+ line = self.etext_file.readline()
+ label_text = label_text + unicode(line,
+ 'iso-8859-1')
+ linecount = linecount + 1
+ label_text = label_text + '\n\n\n'
+ textbuffer.set_text(label_text)
+ self.textview.set_buffer(textbuffer)
+
+ def save_extracted_file(self, zipfile, filename):
+ "Extract the file to a temp directory for viewing"
+ filebytes = zipfile.read(filename)
+ outfn = self.make_new_filename(filename)
+ if (outfn == ''):
+ return False
+ f = open(os.path.join(self.get_activity_root(),
+ 'tmp', outfn), 'w')
+ try:
+ f.write(filebytes)
+ finally:
+ f.close()
+
+ def get_saved_page_number(self):
+ global page
+ title = self.metadata.get('title', '')
+ if title == '' or not title[len(title)-1].isdigit():
+ page = 0
+ else:
+ i = len(title) - 1
+ newPage = ''
+ while (title[i].isdigit() and i &gt; 0):
+ newPage = title[i] + newPage
+ i = i - 1
+ if title[i] == 'P':
+ page = int(newPage) - 1
+ else:
+ # not a page number; maybe a volume number.
+ page = 0
+
+ def save_page_number(self):
+ global page
+ title = self.metadata.get('title', '')
+ if title == '' or not title[len(title)-1].isdigit():
+ title = title + ' P' + str(page + 1)
+ else:
+ i = len(title) - 1
+ while (title[i].isdigit() and i &gt; 0):
+ i = i - 1
+ if title[i] == 'P':
+ title = title[0:i] + 'P' + str(page + 1)
+ else:
+ title = title + ' P' + str(page + 1)
+ self.metadata['title'] = title
+
+ def read_file(self, filename):
+ "Read the Etext file"
+ global PAGE_SIZE, page
+
+ tempfile = os.path.join(self.get_activity_root(),
+ 'instance', 'tmp%i' % time.time())
+ os.link(filename, tempfile)
+ self.tempfile = tempfile
+
+ if zipfile.is_zipfile(filename):
+ self.zf = zipfile.ZipFile(filename, 'r')
+ self.book_files = self.zf.namelist()
+ self.save_extracted_file(self.zf,
+ self.book_files[0])
+ currentFileName = os.path.join(
+ self.get_activity_root(),
+ 'tmp', self.book_files[0])
+ else:
+ currentFileName = filename
+
+ self.etext_file = open(currentFileName,"r")
+ self.page_index = [ 0 ]
+ pagecount = 0
+ linecount = 0
+ while self.etext_file:
+ line = self.etext_file.readline()
+ if not line:
+ break
+ linecount = linecount + 1
+ if linecount &gt;= PAGE_SIZE:
+ position = self.etext_file.tell()
+ self.page_index.append(position)
+ linecount = 0
+ pagecount = pagecount + 1
+ if filename.endswith(".zip"):
+ os.remove(currentFileName)
+ self.get_saved_page_number()
+ self.show_page(page)
+ if _NEW_TOOLBAR_SUPPORT:
+ self.set_total_pages(pagecount + 1)
+ self.set_current_page(page)
+ else:
+ self.read_toolbar.set_total_pages(
+ pagecount + 1)
+ self.read_toolbar.set_current_page(page)
+
+ # We've got the document, so if we're a shared
+ # activity, offer it
+ if self.get_shared():
+ self.watch_for_tubes()
+ self.share_document()
+
+ def make_new_filename(self, filename):
+ partition_tuple = filename.rpartition('/')
+ return partition_tuple[2]
+
+ def write_file(self, filename):
+ "Save meta data for the file."
+ if self.is_received_document:
+ # This document was given to us by someone,
+ # so we have to save it to the Journal.
+ self.etext_file.seek(0)
+ filebytes = self.etext_file.read()
+ print 'saving shared document'
+ f = open(filename, 'wb')
+ try:
+ f.write(filebytes)
+ finally:
+ f.close()
+ elif self.tempfile:
+ if self.close_requested:
+ os.link(self.tempfile, filename)
+ logger.debug(
+ "Removing temp file %s because "
+ "we will close",
+ self.tempfile)
+ os.unlink(self.tempfile)
+ self.tempfile = None
+ else:
+ # skip saving empty file
+ raise NotImplementedError
+
+ self.metadata['activity'] = self.get_bundle_id()
+ self.save_page_number()
+
+ def can_close(self):
+ self.close_requested = True
+ return True
+
+ def joined_cb(self, also_self):
+ """Callback for when a shared activity is joined.
+
+ Get the shared document from another participant.
+ """
+ self.watch_for_tubes()
+ gobject.idle_add(self.get_document)
+
+ def get_document(self):
+ if not self.want_document:
+ return False
+
+ # Assign a file path to download if one
+ # doesn't exist yet
+ if not self._jobject.file_path:
+ path = os.path.join(self.get_activity_root(),
+ 'instance', 'tmp%i' % time.time())
+ else:
+ path = self._jobject.file_path
+
+ # Pick an arbitrary tube we can try to download
+ # the document from
+ try:
+ tube_id = self.unused_download_tubes.pop()
+ except (ValueError, KeyError), e:
+ logger.debug(
+ 'No tubes to get the document from '
+ 'right now: %s', e)
+ return False
+
+ # Avoid trying to download the document
+ # multiple times at once
+ self.want_document = False
+ gobject.idle_add(self.download_document,
+ tube_id, path)
+ return False
+
+ def download_document(self, tube_id, path):
+ chan = self._shared_activity.telepathy_tubes_chan
+ iface = chan[telepathy.CHANNEL_TYPE_TUBES]
+ addr = iface.AcceptStreamTube(tube_id,
+ telepathy.SOCKET_ADDRESS_TYPE_IPV4,
+ telepathy.SOCKET_ACCESS_CONTROL_LOCALHOST,
+ 0,
+ utf8_strings=True)
+ logger.debug(
+ 'Accepted stream tube: '
+ 'listening address is %r',
+ addr)
+ assert isinstance(addr, dbus.Struct)
+ assert len(addr) == 2
+ assert isinstance(addr[0], str)
+ assert isinstance(addr[1], (int, long))
+ assert addr[1] &gt; 0 and addr[1] &lt; 65536
+ port = int(addr[1])
+
+ self.progressbar.show()
+ getter = ReadURLDownloader(
+ "http://%s:%d/document"
+ % (addr[0], port))
+ getter.connect("finished",
+ self.download_result_cb, tube_id)
+ getter.connect("progress",
+ self.download_progress_cb, tube_id)
+ getter.connect("error",
+ self.download_error_cb, tube_id)
+ logger.debug("Starting download to %s...", path)
+ getter.start(path)
+ self.download_content_length = \
+ getter.get_content_length()
+ self.download_content_type = \
+ getter.get_content_type()
+ return False
+
+ def download_progress_cb(self, getter,
+ bytes_downloaded, tube_id):
+ if self.download_content_length &gt; 0:
+ logger.debug(
+ "Downloaded %u of %u bytes from tube %u...",
+ bytes_downloaded,
+ self.download_content_length,
+ tube_id)
+ else:
+ logger.debug(
+ "Downloaded %u bytes from tube %u...",
+ bytes_downloaded, tube_id)
+ total = self.download_content_length
+ self.set_downloaded_bytes(bytes_downloaded,
+ total)
+ gtk.gdk.threads_enter()
+ while gtk.events_pending():
+ gtk.main_iteration()
+ gtk.gdk.threads_leave()
+
+ def set_downloaded_bytes(self, bytes, total):
+ fraction = float(bytes) / float(total)
+ self.progressbar.set_fraction(fraction)
+ logger.debug("Downloaded percent", fraction)
+
+ def clear_downloaded_bytes(self):
+ self.progressbar.set_fraction(0.0)
+ logger.debug("Cleared download bytes")
+
+ def download_error_cb(self, getter, err, tube_id):
+ self.progressbar.hide()
+ logger.debug(
+ "Error getting document from tube %u: %s",
+ tube_id, err)
+ self.alert(_('Failure'),
+ _('Error getting document from tube'))
+ self.want_document = True
+ self.download_content_length = 0
+ self.download_content_type = None
+ gobject.idle_add(self.get_document)
+
+ def download_result_cb(self, getter, tempfile,
+ suggested_name, tube_id):
+ if self.download_content_type.startswith(
+ 'text/html'):
+ # got an error page instead
+ self.download_error_cb(getter,
+ 'HTTP Error', tube_id)
+ return
+
+ del self.unused_download_tubes
+
+ self.tempfile = tempfile
+ file_path = os.path.join(
+ self.get_activity_root(),
+ 'instance', '%i' % time.time())
+ logger.debug(
+ "Saving file %s to datastore...", file_path)
+ os.link(tempfile, file_path)
+ self._jobject.file_path = file_path
+ datastore.write(self._jobject,
+ transfer_ownership=True)
+
+ logger.debug("Got document %s (%s) from tube %u",
+ tempfile, suggested_name, tube_id)
+ self.is_received_document = True
+ self.read_file(tempfile)
+ self.save()
+ self.progressbar.hide()
+
+ def shared_cb(self, activityid):
+ """Callback when activity shared.
+
+ Set up to share the document.
+
+ """
+ # We initiated this activity and have now
+ # shared it, so by definition we have the file.
+ logger.debug('Activity became shared')
+ self.watch_for_tubes()
+ self.share_document()
+
+ def share_document(self):
+ """Share the document."""
+ h = hash(self._activity_id)
+ port = 1024 + (h % 64511)
+ logger.debug(
+ 'Starting HTTP server on port %d', port)
+ self.fileserver = ReadHTTPServer(("", port),
+ self.tempfile)
+
+ # Make a tube for it
+ chan = self._shared_activity.telepathy_tubes_chan
+ iface = chan[telepathy.CHANNEL_TYPE_TUBES]
+ self.fileserver_tube_id = iface.OfferStreamTube(
+ READ_STREAM_SERVICE,
+ {},
+ telepathy.SOCKET_ADDRESS_TYPE_IPV4,
+ ('127.0.0.1', dbus.UInt16(port)),
+ telepathy.SOCKET_ACCESS_CONTROL_LOCALHOST, 0)
+
+ def watch_for_tubes(self):
+ """Watch for new tubes."""
+ tubes_chan = \
+ self._shared_activity.telepathy_tubes_chan
+
+ tubes_chan[telepathy.CHANNEL_TYPE_TUBES].\
+ connect_to_signal(
+ 'NewTube',
+ self.new_tube_cb)
+ tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes(
+ reply_handler=self.list_tubes_reply_cb,
+ error_handler=self.list_tubes_error_cb)
+
+ def new_tube_cb(self, tube_id, initiator, tube_type,
+ service, params, state):
+ """Callback when a new tube becomes available."""
+ logger.debug(
+ 'New tube: ID=%d initator=%d type=%d service=%s '
+ 'params=%r state=%d', tube_id,
+ initiator, tube_type,
+ service, params, state)
+ if service == READ_STREAM_SERVICE:
+ logger.debug('I could download from that tube')
+ self.unused_download_tubes.add(tube_id)
+ # if no download is in progress, let's
+ # fetch the document
+ if self.want_document:
+ gobject.idle_add(self.get_document)
+
+ def list_tubes_reply_cb(self, tubes):
+ """Callback when new tubes are available."""
+ for tube_info in tubes:
+ self.new_tube_cb(*tube_info)
+
+ def list_tubes_error_cb(self, e):
+ """Handle ListTubes error by logging."""
+ logger.error('ListTubes() failed: %s', e)
+
+ def alert(self, title, text=None):
+ alert = NotifyAlert(timeout=20)
+ alert.props.title = title
+ alert.props.msg = text
+ self.add_alert(alert)
+ alert.connect('response', self.alert_cancel_cb)
+ alert.show()
+
+ def alert_cancel_cb(self, alert, response_id):
+ self.remove_alert(alert)
+ self.textview.grab_focus()
+</pre>
+<div class="objavi-forcebreak">
+</div>
+<p>Here is what it looks like running under <strong>sugar-jhbuild</strong>:
+</p>
+<p><img alt="Read Etexts II, New Toolbar" src="static/ActivitiesGuideSugar-newtoolbar5-en.jpg" width="600" height="450"/></p>
+<p>Let's have a look at how it works.&#160;&#8286; If you've paid attention to other chapters when I've talked about the idea of "degrading gracefully" the imports in this code will be about what you would expect:
+</p>
+<pre>_NEW_TOOLBAR_SUPPORT = True
+try:
+ from sugar.graphics.toolbarbox import ToolbarBox
+ from sugar.graphics.toolbarbox import ToolbarButton
+ from sugar.activity.widgets import StopButton
+ from toolbar import ViewToolbar
+ from mybutton import MyActivityToolbarButton
+except:
+ _NEW_TOOLBAR_SUPPORT = False
+ from toolbar import ReadToolbar, ViewToolbar
+</pre>
+<p>Here we try to import a bunch of stuff that only exists in versions of Sugar that support the new toolbars.&#160; If we succeed, then _NEW_TOOLBAR_SUPPORT will remain set to True.&#160; If any of the imports fail then the variable is set to False.&#160; Note that a couple of imports that should always succeed are placed after the three that might fail.&#160; If any of the first three fail I don't want these imports to be done.
+</p>
+<p>This next bit of code in the <em>__init__()</em> method should not be surprising:
+</p>
+<pre> if _NEW_TOOLBAR_SUPPORT:
+ self.create_new_toolbar()
+ else:
+ self.create_old_toolbar()
+</pre>
+<p>I moved creating the toolbars into their own methods to make it easier to compare how the two different toolbars are created.&#160; The old toolbar code is unchanged.&#160; Here is the new toolbar code:
+</p>
+<pre> def create_new_toolbar(self):
+ toolbar_box = ToolbarBox()
+
+ activity_button = MyActivityToolbarButton(self)
+ toolbar_box.toolbar.insert(activity_button, 0)
+ activity_button.show()
+
+ self.edit_toolbar = activity.EditToolbar()
+ self.edit_toolbar.undo.props.visible = False
+ self.edit_toolbar.redo.props.visible = False
+ self.edit_toolbar.separator.props.visible = False
+ self.edit_toolbar.copy.set_sensitive(False)
+ self.edit_toolbar.copy.connect('clicked',
+ self.edit_toolbar_copy_cb)
+ self.edit_toolbar.paste.props.visible = False
+
+ edit_toolbar_button = ToolbarButton(
+ page=self.edit_toolbar,
+ icon_name='toolbar-edit')
+ self.edit_toolbar.show()
+ toolbar_box.toolbar.insert(edit_toolbar_button, -1)
+ edit_toolbar_button.show()
+
+ self.view_toolbar = ViewToolbar()
+ self.view_toolbar.connect('go-fullscreen',
+ self.view_toolbar_go_fullscreen_cb)
+ self.view_toolbar.zoom_in.connect('clicked',
+ self.zoom_in_cb)
+ self.view_toolbar.zoom_out.connect('clicked',
+ self.zoom_out_cb)
+ self.view_toolbar.show()
+ view_toolbar_button = ToolbarButton(
+ page=self.view_toolbar,
+ icon_name='toolbar-view')
+ toolbar_box.toolbar.insert(
+ view_toolbar_button, -1)
+ view_toolbar_button.show()
+
+ self.back = ToolButton('go-previous')
+ self.back.set_tooltip(_('Back'))
+ self.back.props.sensitive = False
+ self.back.connect('clicked', self.go_back_cb)
+ toolbar_box.toolbar.insert(self.back, -1)
+ self.back.show()
+
+ self.forward = ToolButton('go-next')
+ self.forward.set_tooltip(_('Forward'))
+ self.forward.props.sensitive = False
+ self.forward.connect('clicked',
+ self.go_forward_cb)
+ toolbar_box.toolbar.insert(self.forward, -1)
+ self.forward.show()
+
+ num_page_item = gtk.ToolItem()
+ self.num_page_entry = gtk.Entry()
+ self.num_page_entry.set_text('0')
+ self.num_page_entry.set_alignment(1)
+ self.num_page_entry.connect('insert-text',
+ self.__new_num_page_entry_insert_text_cb)
+ self.num_page_entry.connect('activate',
+ self.__new_num_page_entry_activate_cb)
+ self.num_page_entry.set_width_chars(4)
+ num_page_item.add(self.num_page_entry)
+ self.num_page_entry.show()
+ toolbar_box.toolbar.insert(num_page_item, -1)
+ num_page_item.show()
+
+ total_page_item = gtk.ToolItem()
+ self.total_page_label = gtk.Label()
+
+ label_attributes = pango.AttrList()
+ label_attributes.insert(pango.AttrSize(
+ 14000, 0, -1))
+ label_attributes.insert(pango.AttrForeground(
+ 65535, 65535, 65535, 0, -1))
+ self.total_page_label.set_attributes(
+ label_attributes)
+
+ self.total_page_label.set_text(' / 0')
+ total_page_item.add(self.total_page_label)
+ self.total_page_label.show()
+ toolbar_box.toolbar.insert(total_page_item, -1)
+ total_page_item.show()
+
+ separator = gtk.SeparatorToolItem()
+ separator.props.draw = False
+ separator.set_expand(True)
+ toolbar_box.toolbar.insert(separator, -1)
+ separator.show()
+
+ stop_button = StopButton(self)
+ stop_button.props.accelerator = '&lt;Ctrl&gt;&lt;Shift&gt;Q'
+ toolbar_box.toolbar.insert(stop_button, -1)
+ stop_button.show()
+
+ self.set_toolbar_box(toolbar_box)
+ toolbar_box.show()
+
+ def __new_num_page_entry_insert_text_cb(self, entry,
+ text, length, position):
+ if not re.match('[0-9]', text):
+ entry.emit_stop_by_name('insert-text')
+ return True
+ return False
+
+ def __new_num_page_entry_activate_cb(self, entry):
+ global page
+ if entry.props.text:
+ new_page = int(entry.props.text) - 1
+ else:
+ new_page = 0
+
+ if new_page &gt;= self.total_pages:
+ new_page = self.total_pages - 1
+ elif new_page &lt; 0:
+ new_page = 0
+
+ self.current_page = new_page
+ self.set_current_page(new_page)
+ self.show_page(new_page)
+ entry.props.text = str(new_page + 1)
+ self.update_nav_buttons()
+ page = new_page
+
+ def update_nav_buttons(self):
+ current_page = self.current_page
+ self.back.props.sensitive = current_page &gt; 0
+ self.forward.props.sensitive = \
+ current_page &lt; self.total_pages - 1
+
+ self.num_page_entry.props.text = str(
+ current_page + 1)
+ self.total_page_label.props.label = \
+ ' / ' + str(self.total_pages)
+
+ def set_total_pages(self, pages):
+ self.total_pages = pages
+
+ def set_current_page(self, page):
+ self.current_page = page
+ self.update_nav_buttons()
+</pre>
+<p>Much of the code in the two methods is the same.&#160; In particular, the <strong>View</strong> toolbar and the <strong>Edit</strong> toolbar are exactly the same in both.&#160; Instead of becoming the active toolbar they drop down from the toolbar to become sub toolbars.&#160; If we had done the Read toolbar the same way we could have implemented both old and new toolbars with very little code.&#160; However, the <strong>Read</strong> toolbar contains controls that are important enough to the Activity that they should be available at all times, so we put them in the main toolbar instead.&#160; Because of this every place where the code refers to the <strong>Read</strong> toolbar has to have two ways it can be performed, like this:
+</p>
+<pre> if _NEW_TOOLBAR_SUPPORT:
+ self.set_total_pages(pagecount + 1)
+ self.set_current_page(page)
+ else:
+ self.read_toolbar.set_total_pages(
+ pagecount + 1)
+ self.read_toolbar.set_current_page(page)
+</pre>
+<p>There is one more point of interest when it comes to the main toolbar.&#160; When you have an old style toolbar you get the stop button as part of the <strong>Activity</strong> toolbar.&#160; With the new style toolbar you need to add it to the end of the main toolbar yourself:
+</p>
+<pre> separator = gtk.SeparatorToolItem()
+ separator.props.draw = False
+ separator.set_expand(True)
+ toolbar_box.toolbar.insert(separator, -1)
+ separator.show()
+
+ stop_button = StopButton(self)
+ stop_button.props.accelerator = '&lt;Ctrl&gt;&lt;Shift&gt;Q'
+ toolbar_box.toolbar.insert(stop_button, -1)
+ stop_button.show()
+</pre>
+<p>Note that you must put a <strong>gtk.SeparatorToolItem</strong> with <em>set_expand()</em> equal to True before the <strong>StopButton</strong>.&#160; This will push the button all the way to the right of the toolbar, where it belongs.
+ <br/></p>
+<p>That just leaves the <strong>Activity</strong> toolbar to discuss:
+</p>
+<pre> toolbar_box = ToolbarBox()
+
+ activity_button = MyActivityToolbarButton(self)
+ toolbar_box.toolbar.insert(activity_button, 0)
+ activity_button.show()
+</pre>
+<p>Normally you would use the class <strong>ActivityToolbarButton</strong> to create the default drop down Activity toolbar.&#160; The problem I have with that is if I do that there is no way to hide the <strong>Keep</strong> button or the <strong>Share</strong> control.&#160; This version of the Activity needs the <strong>Share</strong> control, but has no use at all for the <strong>Keep</strong> button.
+</p>
+<p>There have been some spirited discussions about the <strong>Keep</strong> button on the mailing lists.&#160; New computer users don't know what it's for, and experienced computer users expect it to be like a <strong>Save Game</strong> button or a <strong>Save As...</strong> menu option in a regular application.&#160; It isn't quite like either one, and that can lead to confusion.&#160; For these reasons I've decided that no Activity of mine will leave the <strong>Keep</strong> button unhidden.&#160; To hide the button I copied a bit of the code for the original <strong>ActivityToolbarButton</strong> in a file named <strong>mybutton.py</strong>:
+</p>
+<pre>import gtk
+import gconf
+
+from sugar.graphics.toolbarbox import ToolbarButton
+from sugar.activity.widgets import ActivityToolbar
+from sugar.graphics.xocolor import XoColor
+from sugar.graphics.icon import Icon
+from sugar.bundle.activitybundle import ActivityBundle
+
+def _create_activity_icon(metadata):
+ if metadata.get('icon-color', ''):
+ color = XoColor(metadata['icon-color'])
+ else:
+ client = gconf.client_get_default()
+ color = XoColor(client.get_string(
+ '/desktop/sugar/user/color'))
+
+ from sugar.activity.activity import get_bundle_path
+ bundle = ActivityBundle(get_bundle_path())
+ icon = Icon(file=bundle.get_icon(), xo_color=color)
+
+ return icon
+
+class MyActivityToolbarButton(ToolbarButton):
+
+ def __init__(self, activity, **kwargs):
+ toolbar = ActivityToolbar(activity,
+ orientation_left=True)
+ toolbar.stop.hide()
+ <strong>toolbar.keep.hide()</strong>
+
+ ToolbarButton.__init__(self, page=toolbar,
+ **kwargs)
+
+ icon = _create_activity_icon(activity.metadata)
+ self.set_icon_widget(icon)
+ icon.show()
+</pre>
+<p>The line in <strong>bold</strong> is the one difference between mine and the original.&#160; If <strong>toolbar</strong> had been made an instance variable (<strong>self.toolbar</strong>) I could have used the original class.
+ <br/></p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch023_where-to-go-from-here.xhtml b/app/static/doc/myosa/ch023_where-to-go-from-here.xhtml
new file mode 100644
index 0000000..aa718e2
--- /dev/null
+++ b/app/static/doc/myosa/ch023_where-to-go-from-here.xhtml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>Where To Go From Here?
+</h1>
+<p>This book attempts to give a beginning programmer the information she needs to develop and publish her own Sugar Activities.&#160; It already contains many URL's of websites containing information not covered in the book.&#160; This chapter will contain URL's and pointers to still more resources that will be useful to any Sugar developer.
+</p>
+<h2>PyGTK Book by Peter Gill
+ <br/></h2>
+<p>Much of the work you will do writing Activities involves PyGTK.&#160; Peter Gill is working on a PyGTK book that covers the subject in great detail.&#160; You can download the book here:
+</p>
+<p><a href="http://www.majorsilence.com/PyGTK_Book">http://www.majorsilence.com/PyGTK_Book</a>&#160;
+ <br/></p>
+<h2>OLPC Austria Activity Handbook
+</h2>
+<p>This is the first attempt to write a manual on creating Sugar Activities.&#160; It is aimed at experienced programmers and covers topics that this book does not, like how to write Activities using languages other than Python.&#160; The book was written in 2008 and as a result some of the advice is a bit dated.&#160; It's still an excellent source of information.&#160; The authors are Christoph Derndorfer and Daniel Jahre.
+</p>
+<p><a href="http://wiki.sugarlabs.org/images/5/51/Activity_Handbook_200805_online.pdf">http://wiki.sugarlabs.org/images/5/51/Activity_Handbook_200805_online.pdf</a>
+ <br/></p>
+<p><a href="http://www.olpcaustria.org">http://www.olpcaustria.org</a>
+ <br/></p>
+<h2>The Sugar Almanac
+</h2>
+<p>This is a series of Wiki articles covering the Sugar <strong>API</strong> (<strong>Application Programming Interface</strong>).&#160; It's a good source of information that I have referred to many times.
+</p>
+<p> <a href="http://wiki.sugarlabs.org/go/Development_Team/Almanac">http://wiki.sugarlabs.org/go/Development_Team/Almanac</a>&#160;
+</p>
+<div class="objavi-forcebreak">
+</div>
+<h2> Sugar Labs Mailing Lists
+</h2>
+<p>Sugar Labs has several email mailing lists that might be worth subscribing to.&#160; The ones I follow most are the <strong>IAEP</strong> (<strong>It's An Education Proiject</strong>) list and <strong>Sugar-Devel</strong>.&#160; Sugar-Devel is a good place to ask questions about developing Sugar Activities and learn about the latest work being done on Sugar itself.&#160; IAEP is a good place to get ideas on what kinds of Activities teachers and students want and to get feedback on your own Activities.&#160; Anyone can sign up to these mailing lists here:
+</p>
+<p><a href="http://lists.sugarlabs.org">http://lists.sugarlabs.org/</a>&#160;
+</p>
+<h2>PyDoc
+</h2>
+<p><strong>PyDoc</strong> is a utility for viewing documentation generated from the Python libraries on your computer, including the Sugar libraries.&#160; To run it use this command from a terminal:
+</p>
+<pre>pydoc -p <em>1234</em></pre>
+<p>This command will not finish.&#160; It runs a kind of web server on your system where <em>1234</em> is a port number.&#160; You can access the website it serves at <strong>http://localhost:1234</strong>.&#160; There is nothing magic about the number 1234.&#160; You can use any number you like.
+</p>
+<p>The website lets you follow links to documentation on all the Python libraries you have installed.&#160; When you are done browsing the documentation you can stop the pydoc command bt returning to the terminal and hitting Ctrl-C (hold down the Ctrl key and hit the "c" key).
+</p>
+<p>
+ <br/></p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch024_about-the-authors.xhtml b/app/static/doc/myosa/ch024_about-the-authors.xhtml
new file mode 100644
index 0000000..6f6b90d
--- /dev/null
+++ b/app/static/doc/myosa/ch024_about-the-authors.xhtml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>About The Authors
+ <br/></h1>
+<h2>James Simmons
+ <br/></h2>
+<p><strong>James Simmons</strong> has programmed professionally since 1978.&#160; Back then computer programs were made using a special machine that punched holes into cards, reels of tape were the most common data storage medium, and hard disks were so expensive and exotic that the hard disk inventory of a Fortune 500 company would today be considered barely large enough to hold a nice picture of Jessica Alba.
+</p>
+<p>The industry has come a long way since then, and to a lesser extent so has James.
+ <br/></p>
+<p>James learned to program at Oakton Community College in Morton Grove, Illinois and Western Illinois University in Macomb, Illinois.&#160; Times were hard back then and a young man's best chance of being employed after graduation was to become an Accountant or a Computer Programmer.&#160; It was while he attended OCC that James saw a Monty Python sketch about an Accountant who wished to become a Lion Tamer.&#160; This convinced James that he should become a Computer Programmer.
+</p>
+<p>James' studies at WIU got off to a rough start when he signed up for Basic Assembly Language as his first real computer class, erroneously thinking that the word "Basic" meant "for beginners". &#160; From the computer's point of view it was basic, but for students not so much.&#160; He barely passed the course with a "D" but in the process learned that he enjoyed programming computers.&#160; He decided to continue his computer studies and graduated with a Bachelor's Degree in Information Science.
+</p>
+<p>James was born in 1956, the year before Sputnik went up.&#160; He was a nerdy kid.&#160; At various times he fooled around with Erector sets, chemistry sets, microscopes, dissecting kits, model cars, model planes, model rockets, amateur radio, film making, and writing science fiction stories.&#160; He achieved no real success with any of these activities.
+</p>
+<p>James participated in the first <em>Give One Get One</em> promotion of the <em>One Laptop Per Child</em> project and started developing Activities for the Sugar platform soon after.&#160; He has written the Activities <strong>Read Etexts</strong>, <strong>View Slides</strong>, <strong>Sugar Commander</strong> and <strong>Get Internet Archive Books</strong>.
+</p>
+<div class="objavi-forcebreak">
+</div>
+<h2>Oceana Rain Fields
+</h2>
+<h2>
+</h2>
+<p> <strong><a href="http://sixes.net/rdcHQ/about/meet-the-rdc/oceana-rain-fields/">Oceana Rain Fields</a></strong> &#8211; Oceana is a visual artist and creative spirit with a flair for the unexpected and the desire to support worthy causes with her art. She graduated in 2010 from Pacific High School, earning several notable scholarships. In 2010, her painting &#8220;Malaria&#8221; won first in show in the Vision 2010 high school art competition at the Coos Art Museum in Coos Bay, Oregon.&#160; Oceana plans to continue her art education at Southwestern Oregon Community College in Fall 2010.&#160;
+</p>
+<p>Oceana is responsible for the cover art of the bound and printed version of this book.&#160; As a "mentee" of the Rural Design Collective, she also did cover and interior illustrations for another FLOSS Manual: <em>An E-Book Revolution: Reading and Leading with One Laptop Per Child</em>.
+</p>
+<p>
+ <br/></p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/ch025_credits.xhtml b/app/static/doc/myosa/ch025_credits.xhtml
new file mode 100644
index 0000000..c88beaa
--- /dev/null
+++ b/app/static/doc/myosa/ch025_credits.xhtml
@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>License
+</h1>
+<p>All chapters copyright of the authors (see below). Unless otherwise stated all chapters in this manual licensed with <strong>GNU General Public License version 2</strong>
+</p> This documentation is free documentation; 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 2 of the License, or (at your option) any later version.
+<p> This documentation 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.
+</p>
+<p> You should have received a copy of the GNU General Public License along with this documentation; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+</p>
+<h2>Acknowledgements
+</h2>
+<p>Many people contributed to this book besides the authors listed.&#160; They offered advice, technical support, corrections, and much code.&#160; If I tried to list all of their names I might leave someone out, so let me just thank all the members of the Sugar-Devel mailing list.
+</p>
+<p>Cover art of the printed version&#160; Copyright (C) 2010 by Oceana Rain Fields.
+ <br/></p>
+<h2>Authors
+</h2>
+<p>
+</p><em>ABOUT THE AUTHORS</em>
+<br/>&#169; James Simmons 2010
+<hr/><em>MAKING ACTIVITIES USING PYGAME</em>
+<br/>&#169; James Simmons 2010
+<hr/><em>ADD REFINEMENTS</em>
+<br/>&#169; James Simmons 2009, 2010
+<br/><br/><hr/><em>CREATING YOUR FIRST ACTIVITY</em>
+<br/>&#169; James Simmons 2009
+<br/><br/><hr/><em>CREDITS</em>
+<br/>&#169; James Simmons 2010
+<br/><br/><hr/><em>DISTRIBUTE YOUR ACTIVITY</em>
+<br/>&#169; James Simmons 2010
+<br/><br/><hr/><em>FUN WITH THE JOURNAL</em>
+<br/>&#169; James Simmons 2010
+<hr/><em>GOING INTERNATIONAL WITH POOTLE</em>
+<br/>&#169; James Simmons 2010
+<br/><br/><hr/><em>INHERIT FROM SUGAR.ACTIVITY.ACTIVITY</em>
+<br/>&#169; James Simmons 2009, 2010
+<br/><br/><hr/><em>INTRODUCTION</em>
+<br/>&#169; James Simmons 2009, 2010
+<br/><br/><hr/><em>WHERE TO GO FROM HERE?</em>
+<br/>&#169; James Simmons 2010
+<hr/><em>WHAT DO I NEED TO KNOW TO WRITE A SUGAR ACTIVITY?</em>
+<br/>&#169; James Simmons 2009, 2010
+<br/><br/><hr/><em>MAKING NEW STYLE TOOLBARS</em>
+<br/>&#169; James Simmons 2010
+<hr/><em>PACKAGE THE ACTIVITY</em>
+<br/>&#169; James Simmons 2009, 2010
+<br/><br/><hr/><em>SETTING UP A DEVELOPMENT ENVIRONMENT</em>
+<br/>&#169; James Simmons 2009, 2010
+<br/><br/><hr/><em>A STANDALONE PYTHON PROGRAM FOR READING ETEXTS</em>
+<br/>&#169; James Simmons 2009, 2010
+<br/><br/><hr/><em>MAKING SHARED ACTIVITIES</em>
+<br/>&#169; James Simmons 2010
+<br/><br/><hr/><em>DEBUGGING SUGAR ACTIVITIES</em>
+<br/>&#169; James Simmons 2010
+<hr/><em>ADDING TEXT TO SPEECH</em>
+<br/>&#169; James Simmons 2010
+<br/><br/><hr/><em>ADD YOUR ACTIVITY CODE TO VERSION CONTROL</em>
+<br/>&#169; James Simmons 2010
+<br/><br/><hr/><em>WHAT IS SUGAR?</em>
+<br/>&#169; James Simmons 2009, 2010
+<br/><br/><hr/><em>WHAT IS A SUGAR ACTIVITY?</em>
+<br/>&#169; James Simmons 2009, 2010
+<br/><br/><hr/>&#160;
+<p> <a href="http://www.flossmanuals.net/"><img alt="100.gif" src="static/Floss-100-en.gif" height="54" width="110" border="0"/></a>
+ <br/><strong/>
+</p>
+<p><sub>Free manuals for free software</sub></p>
+<p>
+</p>
+<p>&#160;
+</p>
+<p>&#160;
+</p>
+<h2>General Public License
+</h2>
+<p>Version 2, June 1991
+</p>
+<p>Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ <br/>51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+ <br/><br/>Everyone is permitted to copy and distribute verbatim copies
+ <br/>of this license document, but changing it is not allowed.
+ <br/></p>
+<p><strong>Preamble</strong>
+</p>
+<p> The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too.
+</p>
+<p> When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.
+</p>
+<p> To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.
+</p>
+<p> For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
+</p>
+<p> We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.
+</p>
+<p> Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.
+</p>
+<p> Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.
+</p>
+<p> The precise terms and conditions for copying, distribution and modification follow.
+</p>
+<p><strong>TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION</strong>
+</p>
+<p> <strong>0.</strong> This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you".
+</p>
+<p> Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.
+</p>
+<p> <strong>1.</strong> You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.
+</p>
+<p> You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
+</p>
+<p> <strong>2.</strong> You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
+</p>
+<dl><dt>
+ <br/></dt>
+ <dd> <strong>a)</strong> You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.</dd>
+ <dt>
+ <br/></dt>
+ <dd> <strong>b)</strong> You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.</dd>
+ <dt>
+ <br/></dt>
+ <dd> <strong>c)</strong> If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)</dd>
+</dl><p> These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
+</p>
+<p> Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.
+</p>
+<p> In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
+</p>
+<p> <strong>3.</strong> You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:
+</p>
+<dl><dt>
+ <br/></dt>
+ <dd> <strong>a)</strong> Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,</dd>
+ <dt>
+ <br/></dt>
+ <dd> <strong>b)</strong> Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,</dd>
+ <dt>
+ <br/></dt>
+ <dd> <strong>c)</strong> Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)</dd>
+</dl><p> The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
+</p>
+<p> If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.
+</p>
+<p> <strong>4.</strong> You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
+</p>
+<p> <strong>5.</strong> You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.
+</p>
+<p> <strong>6.</strong> Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.
+</p>
+<p> <strong>7.</strong> If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.
+</p>
+<p> If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.
+</p>
+<p> It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
+</p>
+<p> This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
+</p>
+<p> <strong>8.</strong> If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
+</p>
+<p> <strong>9.</strong> The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
+</p>
+<p> Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.
+</p>
+<p> <strong>10.</strong> If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
+</p>
+<p><strong>NO WARRANTY</strong>
+</p>
+<p> <strong>11.</strong> BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+</p>
+<p> <strong>12.</strong> IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+</p>
+<p><strong>END OF TERMS AND CONDITIONS</strong>
+</p>
+<p>
+</p></body></html> \ No newline at end of file
diff --git a/app/static/doc/myosa/content.opf b/app/static/doc/myosa/content.opf
new file mode 100644
index 0000000..3d6af9f
--- /dev/null
+++ b/app/static/doc/myosa/content.opf
@@ -0,0 +1,126 @@
+<package xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns="http://www.idpf.org/2007/opf" version="2.0" unique-identifier="primary_id">
+ <metadata>
+ <dc:publisher>FLOSS Manuals http://flossmanuals.net</dc:publisher>
+ <dc:rights scheme="License">GPLv2+</dc:rights>
+ <dc:language>en</dc:language>
+ <dc:title>Make Your Own Sugar Activities!</dc:title>
+ <dc:creator>James D. Simmons</dc:creator>
+ <dc:date>2010-11-28</dc:date>
+ <dc:date scheme="start">2010.12.09-07.20</dc:date>
+ <dc:date scheme="last-modified">2011.05.13-00.00</dc:date>
+ <dc:date scheme="published">2011.05.13-18.25</dc:date>
+ <dc:identifier id="primary_id">http://en.flossmanuals.net/epub/ActivitiesGuideSugar/2010.11.28-14.18.35</dc:identifier>
+ <dc:identifier scheme="booki.cc">http://booki.flossmanuals.net/make-your-own-sugar-activities/2011.05.13-18.25</dc:identifier>
+ <meta name="cover" content="att000_MYOSA_Cover" />
+ </metadata>
+ <guide>
+ <reference type="toc" title="Table Of Contents" href="ch000_table_of_contents.xhtml" />
+ </guide>
+ <manifest>
+ <item href="static/ActivitiesGuideSugar-collab6-en.jpg" media-type="image/jpeg" id="att014_ActivitiesGuideSugar-collab6-en"/>
+ <item href="ch015_debugging-sugar-activities.xhtml" media-type="application/xhtml+xml" id="ch015_debugging-sugar-activities"/>
+ <item href="static/ActivitiesGuideSugar-pootle1-en.jpg" media-type="image/jpeg" id="att023_ActivitiesGuideSugar-pootle1-en"/>
+ <item href="static/ActivitiesGuideSugar-collab5-en.jpg" media-type="image/jpeg" id="att007_ActivitiesGuideSugar-collab5-en"/>
+ <item href="ch007_creating-your-first-activity.xhtml" media-type="application/xhtml+xml" id="ch007_creating-your-first-activity"/>
+ <item href="static/ActivitiesGuideSugar-git5-en.jpg" media-type="image/jpeg" id="att002_ActivitiesGuideSugar-git5-en"/>
+ <item href="ch003_what-is-a-sugar-activity.xhtml" media-type="application/xhtml+xml" id="ch003_what-is-a-sugar-activity"/>
+ <item href="static/ActivitiesGuideSugar-inkscape-en.jpg" media-type="image/jpeg" id="att048_ActivitiesGuideSugar-inkscape-en"/>
+ <item href="static/ActivitiesGuideSugar-collab1_1-en.jpg" media-type="image/jpeg" id="att005_ActivitiesGuideSugar-collab1_1-en"/>
+ <item href="static/Floss-100-en.gif" media-type="image/gif" id="att039_Floss-100-en"/>
+ <item href="static/ActivitiesGuideSugar-ReadEtexts_02-en.jpg" media-type="image/jpeg" id="att017_ActivitiesGuideSugar-ReadEtexts_02-en"/>
+ <item href="static/ActivitiesGuideSugar-gimp1-en.jpg" media-type="image/jpeg" id="att011_ActivitiesGuideSugar-gimp1-en"/>
+ <item href="static/ActivitiesGuideSugar-ReadEtexts_01_1-en.jpg" media-type="image/jpeg" id="att050_ActivitiesGuideSugar-ReadEtexts_01_1-en"/>
+ <item href="static/ActivitiesGuideSugar-thegimp-en.jpg" media-type="image/jpeg" id="att052_ActivitiesGuideSugar-thegimp-en"/>
+ <item href="ch008_a-standalone-python-program-for-reading-etexts.xhtml" media-type="application/xhtml+xml" id="ch008_a-standalone-python-program-for-reading-etexts"/>
+ <item href="ch018_adding-text-to-speech.xhtml" media-type="application/xhtml+xml" id="ch018_adding-text-to-speech"/>
+ <item href="static/ActivitiesGuideSugar-journal_main_screen-en.png" media-type="image/png" id="att036_ActivitiesGuideSugar-journal_main_screen-en"/>
+ <item href="static/ActivitiesGuideSugar-SpeakActivity-en.png" media-type="image/png" id="att035_ActivitiesGuideSugar-SpeakActivity-en"/>
+ <item href="static/ActivitiesGuideSugar-git3-en.jpg" media-type="image/jpeg" id="att001_ActivitiesGuideSugar-git3-en"/>
+ <item href="ch025_credits.xhtml" media-type="application/xhtml+xml" id="ch025_credits"/>
+ <item href="static/ActivitiesGuideSugar-collab2_1-en.jpg" media-type="image/jpeg" id="att055_ActivitiesGuideSugar-collab2_1-en"/>
+ <item href="ch012_add-your-activity-code-to-version-control.xhtml" media-type="application/xhtml+xml" id="ch012_add-your-activity-code-to-version-control"/>
+ <item href="ch000_table_of_contents.xhtml" media-type="application/xhtml+xml" id="ch000_table_of_contents"/>
+ <item href="ch001_introduction.xhtml" media-type="application/xhtml+xml" id="ch001_introduction"/>
+ <item href="static/ActivitiesGuideSugar-git10-en.jpg" media-type="image/jpeg" id="att032_ActivitiesGuideSugar-git10-en"/>
+ <item href="ch023_where-to-go-from-here.xhtml" media-type="application/xhtml+xml" id="ch023_where-to-go-from-here"/>
+ <item href="ch021_making-new-style-toolbars.xhtml" media-type="application/xhtml+xml" id="ch021_making-new-style-toolbars"/>
+ <item href="static/ActivitiesGuideSugar-logactivity2-en.jpg" media-type="image/jpeg" id="att054_ActivitiesGuideSugar-logactivity2-en"/>
+ <item href="static/ActivitiesGuideSugar-newtoolbar1_1-en.jpg" media-type="image/jpeg" id="att056_ActivitiesGuideSugar-newtoolbar1_1-en"/>
+ <item href="static/ActivitiesGuideSugar-logactivity1-en.jpg" media-type="image/jpeg" id="att053_ActivitiesGuideSugar-logactivity1-en"/>
+ <item href="ch014_distribute-your-activity.xhtml" media-type="application/xhtml+xml" id="ch014_distribute-your-activity"/>
+ <item href="static/ActivitiesGuideSugar-git1-en.jpg" media-type="image/jpeg" id="att031_ActivitiesGuideSugar-git1-en"/>
+ <item href="ch006_setting-up-a-development-environment.xhtml" media-type="application/xhtml+xml" id="ch006_setting-up-a-development-environment"/>
+ <item href="static/ActivitiesGuideSugar-git13-en.jpg" media-type="image/jpeg" id="att028_ActivitiesGuideSugar-git13-en"/>
+ <item href="static/ActivitiesGuideSugar-git2-en.jpg" media-type="image/jpeg" id="att008_ActivitiesGuideSugar-git2-en"/>
+ <item href="static/ActivitiesGuideSugar-git7-en.jpg" media-type="image/jpeg" id="att033_ActivitiesGuideSugar-git7-en"/>
+ <item href="ch013_going-international-with-pootle.xhtml" media-type="application/xhtml+xml" id="ch013_going-international-with-pootle"/>
+ <item href="ch004_what-do-i-need-to-know-to-write-a-sugar-activity.xhtml" media-type="application/xhtml+xml" id="ch004_what-do-i-need-to-know-to-write-a-sugar-activity"/>
+ <item href="static/ActivitiesGuideSugar-SCommander2-en.jpg" media-type="image/jpeg" id="att013_ActivitiesGuideSugar-SCommander2-en"/>
+ <item href="toc.ncx" media-type="application/x-dtbncx+xml" id="ncx"/>
+ <item href="static/ActivitiesGuideSugar-ReadEtexts_04-en.jpg" media-type="image/jpeg" id="att045_ActivitiesGuideSugar-ReadEtexts_04-en"/>
+ <item href="static/ActivitiesGuideSugar-ReadEtexts_03-en.jpg" media-type="image/jpeg" id="att020_ActivitiesGuideSugar-ReadEtexts_03-en"/>
+ <item href="static/ActivitiesGuideSugar-git8-en.jpg" media-type="image/jpeg" id="att046_ActivitiesGuideSugar-git8-en"/>
+ <item href="static/ActivitiesGuideSugar-espeak-en.jpg" media-type="image/jpeg" id="att030_ActivitiesGuideSugar-espeak-en"/>
+ <item href="static/ActivitiesGuideSugar-CoverImage600x450-en.png" media-type="image/png" id="att034_ActivitiesGuideSugar-CoverImage600x450-en"/>
+ <item href="static/ActivitiesGuideSugar-newtoolbar5-en.jpg" media-type="image/jpeg" id="att041_ActivitiesGuideSugar-newtoolbar5-en"/>
+ <item href="static/MYOSA_Cover.png" media-type="image/png" id="att000_MYOSA_Cover"/>
+ <item href="static/ActivitiesGuideSugar-newtoolbar4-en.jpg" media-type="image/jpeg" id="att010_ActivitiesGuideSugar-newtoolbar4-en"/>
+ <item href="static/ActivitiesGuideSugar-newtoolbar3-en.jpg" media-type="image/jpeg" id="att047_ActivitiesGuideSugar-newtoolbar3-en"/>
+ <item href="static/ActivitiesGuideSugar-pootle2-en.jpg" media-type="image/jpeg" id="att042_ActivitiesGuideSugar-pootle2-en"/>
+ <item href="ch017_making-shared-activities.xhtml" media-type="application/xhtml+xml" id="ch017_making-shared-activities"/>
+ <item href="static/ActivitiesGuideSugar-eric-en.jpg" media-type="image/jpeg" id="att004_ActivitiesGuideSugar-eric-en"/>
+ <item href="static/ActivitiesGuideSugar-git14-en.jpg" media-type="image/jpeg" id="att051_ActivitiesGuideSugar-git14-en"/>
+ <item href="static/ActivitiesGuideSugar-demoiselle1-en.jpg" media-type="image/jpeg" id="att019_ActivitiesGuideSugar-demoiselle1-en"/>
+ <item href="static/ActivitiesGuideSugar-sugargame_1-en.jpg" media-type="image/jpeg" id="att037_ActivitiesGuideSugar-sugargame_1-en"/>
+ <item href="static/ActivitiesGuideSugar-SCommander1-en.jpg" media-type="image/jpeg" id="att009_ActivitiesGuideSugar-SCommander1-en"/>
+ <item href="static/ActivitiesGuideSugar-analyzeactivity_1-en.jpg" media-type="image/jpeg" id="att029_ActivitiesGuideSugar-analyzeactivity_1-en"/>
+ <item href="ch009_inherit-from-sugaractivityactivity.xhtml" media-type="application/xhtml+xml" id="ch009_inherit-from-sugaractivityactivity"/>
+ <item href="static/ActivitiesGuideSugar-espeak3-en.jpg" media-type="image/jpeg" id="att027_ActivitiesGuideSugar-espeak3-en"/>
+ <item href="ch011_add-refinements.xhtml" media-type="application/xhtml+xml" id="ch011_add-refinements"/>
+ <item href="static/ActivitiesGuideSugar-espeak2_1-en.jpg" media-type="image/jpeg" id="att022_ActivitiesGuideSugar-espeak2_1-en"/>
+ <item href="static/ActivitiesGuideSugar-demoiselle2_1-en.jpg" media-type="image/jpeg" id="att049_ActivitiesGuideSugar-demoiselle2_1-en"/>
+ <item href="static/ActivitiesGuideSugar-collab4-en.jpg" media-type="image/jpeg" id="att015_ActivitiesGuideSugar-collab4-en"/>
+ <item href="static/ActivitiesGuideSugar-newtoolbar2-en.jpg" media-type="image/jpeg" id="att026_ActivitiesGuideSugar-newtoolbar2-en"/>
+ <item href="ch010_package-the-activity.xhtml" media-type="application/xhtml+xml" id="ch010_package-the-activity"/>
+ <item href="ch020_making-activities-using-pygame.xhtml" media-type="application/xhtml+xml" id="ch020_making-activities-using-pygame"/>
+ <item href="static/ActivitiesGuideSugar-batallanaval-en.jpg" media-type="image/jpeg" id="att016_ActivitiesGuideSugar-batallanaval-en"/>
+ <item href="static/ActivitiesGuideSugar-ReadEtexts_05-en.jpg" media-type="image/jpeg" id="att038_ActivitiesGuideSugar-ReadEtexts_05-en"/>
+ <item href="static/ActivitiesGuideSugar-pootle3-en.jpg" media-type="image/jpeg" id="att044_ActivitiesGuideSugar-pootle3-en"/>
+ <item href="static/ActivitiesGuideSugar-git12-en.jpg" media-type="image/jpeg" id="att012_ActivitiesGuideSugar-git12-en"/>
+ <item href="static/ActivitiesGuideSugar-git11_1-en.jpg" media-type="image/jpeg" id="att018_ActivitiesGuideSugar-git11_1-en"/>
+ <item href="ch002_what-is-sugar.xhtml" media-type="application/xhtml+xml" id="ch002_what-is-sugar"/>
+ <item href="static/ActivitiesGuideSugar-collab3_1-en.jpg" media-type="image/jpeg" id="att040_ActivitiesGuideSugar-collab3_1-en"/>
+ <item href="static/ActivitiesGuideSugar-git4-en.jpg" media-type="image/jpeg" id="att024_ActivitiesGuideSugar-git4-en"/>
+ <item href="static/ActivitiesGuideSugar-oldtoolbar-en.jpg" media-type="image/jpeg" id="att025_ActivitiesGuideSugar-oldtoolbar-en"/>
+ <item href="ch024_about-the-authors.xhtml" media-type="application/xhtml+xml" id="ch024_about-the-authors"/>
+ <item href="ch019_fun-with-the-journal.xhtml" media-type="application/xhtml+xml" id="ch019_fun-with-the-journal"/>
+ <item href="static/ActivitiesGuideSugar-git6-en.jpg" media-type="image/jpeg" id="att006_ActivitiesGuideSugar-git6-en"/>
+ <item href="static/ActivitiesGuideSugar-git9-en.jpg" media-type="image/jpeg" id="att021_ActivitiesGuideSugar-git9-en"/>
+ <item href="static/ActivitiesGuideSugar-scribble-en.jpg" media-type="image/jpeg" id="att003_ActivitiesGuideSugar-scribble-en"/>
+ <item href="static/ActivitiesGuideSugar-spe-en.jpg" media-type="image/jpeg" id="att043_ActivitiesGuideSugar-spe-en"/>
+ </manifest>
+ <spine toc="ncx">
+ <itemref idref="ch001_introduction"/>
+ <itemref idref="ch002_what-is-sugar"/>
+ <itemref idref="ch003_what-is-a-sugar-activity"/>
+ <itemref idref="ch004_what-do-i-need-to-know-to-write-a-sugar-activity"/>
+ <itemref idref="ch006_setting-up-a-development-environment"/>
+ <itemref idref="ch007_creating-your-first-activity"/>
+ <itemref idref="ch008_a-standalone-python-program-for-reading-etexts"/>
+ <itemref idref="ch009_inherit-from-sugaractivityactivity"/>
+ <itemref idref="ch010_package-the-activity"/>
+ <itemref idref="ch011_add-refinements"/>
+ <itemref idref="ch012_add-your-activity-code-to-version-control"/>
+ <itemref idref="ch013_going-international-with-pootle"/>
+ <itemref idref="ch014_distribute-your-activity"/>
+ <itemref idref="ch015_debugging-sugar-activities"/>
+ <itemref idref="ch017_making-shared-activities"/>
+ <itemref idref="ch018_adding-text-to-speech"/>
+ <itemref idref="ch019_fun-with-the-journal"/>
+ <itemref idref="ch020_making-activities-using-pygame"/>
+ <itemref idref="ch021_making-new-style-toolbars"/>
+ <itemref idref="ch023_where-to-go-from-here"/>
+ <itemref idref="ch024_about-the-authors"/>
+ <itemref idref="ch025_credits"/>
+ </spine>
+</package>
diff --git a/app/static/doc/myosa/mimetype b/app/static/doc/myosa/mimetype
new file mode 100644
index 0000000..bc3a3ea
--- /dev/null
+++ b/app/static/doc/myosa/mimetype
@@ -0,0 +1 @@
+application/x-booki+zip \ No newline at end of file
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-CoverImage600x450-en.png b/app/static/doc/myosa/static/ActivitiesGuideSugar-CoverImage600x450-en.png
new file mode 100644
index 0000000..2734e7b
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-CoverImage600x450-en.png
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-ReadEtexts_01_1-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-ReadEtexts_01_1-en.jpg
new file mode 100644
index 0000000..352effb
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-ReadEtexts_01_1-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-ReadEtexts_02-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-ReadEtexts_02-en.jpg
new file mode 100644
index 0000000..dcf22b5
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-ReadEtexts_02-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-ReadEtexts_03-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-ReadEtexts_03-en.jpg
new file mode 100644
index 0000000..9302035
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-ReadEtexts_03-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-ReadEtexts_04-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-ReadEtexts_04-en.jpg
new file mode 100644
index 0000000..a24fd89
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-ReadEtexts_04-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-ReadEtexts_05-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-ReadEtexts_05-en.jpg
new file mode 100644
index 0000000..e0bbe0c
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-ReadEtexts_05-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-SCommander1-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-SCommander1-en.jpg
new file mode 100644
index 0000000..e68af82
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-SCommander1-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-SCommander2-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-SCommander2-en.jpg
new file mode 100644
index 0000000..4f5c422
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-SCommander2-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-SpeakActivity-en.png b/app/static/doc/myosa/static/ActivitiesGuideSugar-SpeakActivity-en.png
new file mode 100644
index 0000000..d4b8701
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-SpeakActivity-en.png
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-analyzeactivity_1-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-analyzeactivity_1-en.jpg
new file mode 100644
index 0000000..bbd3d97
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-analyzeactivity_1-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-batallanaval-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-batallanaval-en.jpg
new file mode 100644
index 0000000..6e29bc8
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-batallanaval-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-collab1_1-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-collab1_1-en.jpg
new file mode 100644
index 0000000..9411746
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-collab1_1-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-collab2_1-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-collab2_1-en.jpg
new file mode 100644
index 0000000..e8fdf36
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-collab2_1-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-collab3_1-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-collab3_1-en.jpg
new file mode 100644
index 0000000..3c0a319
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-collab3_1-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-collab4-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-collab4-en.jpg
new file mode 100644
index 0000000..c36d275
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-collab4-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-collab5-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-collab5-en.jpg
new file mode 100644
index 0000000..ceebd85
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-collab5-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-collab6-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-collab6-en.jpg
new file mode 100644
index 0000000..43b42d0
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-collab6-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-demoiselle1-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-demoiselle1-en.jpg
new file mode 100644
index 0000000..363910e
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-demoiselle1-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-demoiselle2_1-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-demoiselle2_1-en.jpg
new file mode 100644
index 0000000..29ec5f7
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-demoiselle2_1-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-eric-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-eric-en.jpg
new file mode 100644
index 0000000..2a95c5c
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-eric-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-espeak-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-espeak-en.jpg
new file mode 100644
index 0000000..9a144ad
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-espeak-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-espeak2_1-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-espeak2_1-en.jpg
new file mode 100644
index 0000000..5e00959
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-espeak2_1-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-espeak3-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-espeak3-en.jpg
new file mode 100644
index 0000000..59e4140
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-espeak3-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-gimp1-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-gimp1-en.jpg
new file mode 100644
index 0000000..38f7478
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-gimp1-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-git1-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-git1-en.jpg
new file mode 100644
index 0000000..cae8fba
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-git1-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-git10-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-git10-en.jpg
new file mode 100644
index 0000000..47c3278
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-git10-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-git11_1-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-git11_1-en.jpg
new file mode 100644
index 0000000..23572f2
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-git11_1-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-git12-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-git12-en.jpg
new file mode 100644
index 0000000..bb3aef1
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-git12-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-git13-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-git13-en.jpg
new file mode 100644
index 0000000..7d8a902
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-git13-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-git14-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-git14-en.jpg
new file mode 100644
index 0000000..ca3e795
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-git14-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-git2-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-git2-en.jpg
new file mode 100644
index 0000000..bd80440
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-git2-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-git3-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-git3-en.jpg
new file mode 100644
index 0000000..e9df4a7
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-git3-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-git4-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-git4-en.jpg
new file mode 100644
index 0000000..3eff680
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-git4-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-git5-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-git5-en.jpg
new file mode 100644
index 0000000..cb82bfe
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-git5-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-git6-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-git6-en.jpg
new file mode 100644
index 0000000..d5ba0da
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-git6-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-git7-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-git7-en.jpg
new file mode 100644
index 0000000..5c383bf
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-git7-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-git8-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-git8-en.jpg
new file mode 100644
index 0000000..0940192
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-git8-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-git9-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-git9-en.jpg
new file mode 100644
index 0000000..80cf84f
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-git9-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-inkscape-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-inkscape-en.jpg
new file mode 100644
index 0000000..4128ee7
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-inkscape-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-journal_main_screen-en.png b/app/static/doc/myosa/static/ActivitiesGuideSugar-journal_main_screen-en.png
new file mode 100644
index 0000000..5173660
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-journal_main_screen-en.png
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-logactivity1-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-logactivity1-en.jpg
new file mode 100644
index 0000000..22362de
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-logactivity1-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-logactivity2-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-logactivity2-en.jpg
new file mode 100644
index 0000000..aabacce
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-logactivity2-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-newtoolbar1_1-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-newtoolbar1_1-en.jpg
new file mode 100644
index 0000000..972fb49
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-newtoolbar1_1-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-newtoolbar2-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-newtoolbar2-en.jpg
new file mode 100644
index 0000000..cc2f900
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-newtoolbar2-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-newtoolbar3-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-newtoolbar3-en.jpg
new file mode 100644
index 0000000..e7f441b
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-newtoolbar3-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-newtoolbar4-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-newtoolbar4-en.jpg
new file mode 100644
index 0000000..6ecf697
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-newtoolbar4-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-newtoolbar5-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-newtoolbar5-en.jpg
new file mode 100644
index 0000000..d1d6b15
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-newtoolbar5-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-oldtoolbar-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-oldtoolbar-en.jpg
new file mode 100644
index 0000000..03e8509
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-oldtoolbar-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-pootle1-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-pootle1-en.jpg
new file mode 100644
index 0000000..80845e1
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-pootle1-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-pootle2-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-pootle2-en.jpg
new file mode 100644
index 0000000..42e6695
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-pootle2-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-pootle3-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-pootle3-en.jpg
new file mode 100644
index 0000000..8bdf69e
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-pootle3-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-scribble-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-scribble-en.jpg
new file mode 100644
index 0000000..d1627da
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-scribble-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-spe-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-spe-en.jpg
new file mode 100644
index 0000000..18e47ed
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-spe-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-sugargame_1-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-sugargame_1-en.jpg
new file mode 100644
index 0000000..844029f
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-sugargame_1-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/ActivitiesGuideSugar-thegimp-en.jpg b/app/static/doc/myosa/static/ActivitiesGuideSugar-thegimp-en.jpg
new file mode 100644
index 0000000..c090c83
--- /dev/null
+++ b/app/static/doc/myosa/static/ActivitiesGuideSugar-thegimp-en.jpg
Binary files differ
diff --git a/app/static/doc/myosa/static/Floss-100-en.gif b/app/static/doc/myosa/static/Floss-100-en.gif
new file mode 100644
index 0000000..de26325
--- /dev/null
+++ b/app/static/doc/myosa/static/Floss-100-en.gif
Binary files differ
diff --git a/app/static/doc/myosa/static/MYOSA_Cover.png b/app/static/doc/myosa/static/MYOSA_Cover.png
new file mode 100644
index 0000000..1bce943
--- /dev/null
+++ b/app/static/doc/myosa/static/MYOSA_Cover.png
Binary files differ
diff --git a/app/static/doc/myosa/toc.ncx b/app/static/doc/myosa/toc.ncx
new file mode 100644
index 0000000..a5054b8
--- /dev/null
+++ b/app/static/doc/myosa/toc.ncx
@@ -0,0 +1,170 @@
+<!DOCTYPE ncx PUBLIC "-//NISO//DTD ncx 2005-1//EN" "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">
+<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">
+ <head>
+ <meta content="http://en.flossmanuals.net/epub/ActivitiesGuideSugar/2010.11.28-14.18.35" name="dtb:uid"/>
+ <meta content="2" name="dtb:depth"/>
+ <meta content="0" name="dtb:totalPageCount"/>
+ <meta content="0" name="dtb:maxPageNumber"/>
+ </head>
+ <docTitle>
+ <text>Make Your Own Sugar Activities!</text>
+ </docTitle>
+ <navMap>
+ <navPoint id="chapter0" playOrder="1">
+ <navLabel>
+ <text>SUGAR ACTIVITIES</text>
+ </navLabel>
+ <content src="ch001_introduction.xhtml"/>
+ <navPoint id="chapter1" playOrder="2">
+ <navLabel>
+ <text>INTRODUCTION</text>
+ </navLabel>
+ <content src="ch001_introduction.xhtml"/>
+ </navPoint>
+ <navPoint id="chapter2" playOrder="3">
+ <navLabel>
+ <text>WHAT IS SUGAR?</text>
+ </navLabel>
+ <content src="ch002_what-is-sugar.xhtml"/>
+ </navPoint>
+ <navPoint id="chapter3" playOrder="4">
+ <navLabel>
+ <text>WHAT IS A SUGAR ACTIVITY?</text>
+ </navLabel>
+ <content src="ch003_what-is-a-sugar-activity.xhtml"/>
+ </navPoint>
+ <navPoint id="chapter4" playOrder="5">
+ <navLabel>
+ <text>WHAT DO I NEED TO KNOW TO WRITE A SUGAR ACTIVITY?</text>
+ </navLabel>
+ <content src="ch004_what-do-i-need-to-know-to-write-a-sugar-activity.xhtml"/>
+ </navPoint>
+ </navPoint>
+ <navPoint id="chapter5" playOrder="6">
+ <navLabel>
+ <text>PROGRAMMING</text>
+ </navLabel>
+ <content src="ch006_setting-up-a-development-environment.xhtml"/>
+ <navPoint id="chapter6" playOrder="7">
+ <navLabel>
+ <text>SETTING UP A DEVELOPMENT ENVIRONMENT</text>
+ </navLabel>
+ <content src="ch006_setting-up-a-development-environment.xhtml"/>
+ </navPoint>
+ <navPoint id="chapter7" playOrder="8">
+ <navLabel>
+ <text>CREATING YOUR FIRST ACTIVITY</text>
+ </navLabel>
+ <content src="ch007_creating-your-first-activity.xhtml"/>
+ </navPoint>
+ <navPoint id="chapter8" playOrder="9">
+ <navLabel>
+ <text>A STANDALONE PYTHON PROGRAM FOR READING ETEXTS</text>
+ </navLabel>
+ <content src="ch008_a-standalone-python-program-for-reading-etexts.xhtml"/>
+ </navPoint>
+ <navPoint id="chapter9" playOrder="10">
+ <navLabel>
+ <text>INHERIT FROM SUGAR.ACTIVITY.ACTIVITY</text>
+ </navLabel>
+ <content src="ch009_inherit-from-sugaractivityactivity.xhtml"/>
+ </navPoint>
+ <navPoint id="chapter10" playOrder="11">
+ <navLabel>
+ <text>PACKAGE THE ACTIVITY</text>
+ </navLabel>
+ <content src="ch010_package-the-activity.xhtml"/>
+ </navPoint>
+ <navPoint id="chapter11" playOrder="12">
+ <navLabel>
+ <text>ADD REFINEMENTS</text>
+ </navLabel>
+ <content src="ch011_add-refinements.xhtml"/>
+ </navPoint>
+ <navPoint id="chapter12" playOrder="13">
+ <navLabel>
+ <text>ADD YOUR ACTIVITY CODE TO VERSION CONTROL</text>
+ </navLabel>
+ <content src="ch012_add-your-activity-code-to-version-control.xhtml"/>
+ </navPoint>
+ <navPoint id="chapter13" playOrder="14">
+ <navLabel>
+ <text>GOING INTERNATIONAL WITH POOTLE</text>
+ </navLabel>
+ <content src="ch013_going-international-with-pootle.xhtml"/>
+ </navPoint>
+ <navPoint id="chapter14" playOrder="15">
+ <navLabel>
+ <text>DISTRIBUTE YOUR ACTIVITY</text>
+ </navLabel>
+ <content src="ch014_distribute-your-activity.xhtml"/>
+ </navPoint>
+ <navPoint id="chapter15" playOrder="16">
+ <navLabel>
+ <text>DEBUGGING SUGAR ACTIVITIES</text>
+ </navLabel>
+ <content src="ch015_debugging-sugar-activities.xhtml"/>
+ </navPoint>
+ </navPoint>
+ <navPoint id="chapter16" playOrder="17">
+ <navLabel>
+ <text>ADVANCED TOPICS</text>
+ </navLabel>
+ <content src="ch017_making-shared-activities.xhtml"/>
+ <navPoint id="chapter17" playOrder="18">
+ <navLabel>
+ <text>MAKING SHARED ACTIVITIES</text>
+ </navLabel>
+ <content src="ch017_making-shared-activities.xhtml"/>
+ </navPoint>
+ <navPoint id="chapter18" playOrder="19">
+ <navLabel>
+ <text>ADDING TEXT TO SPEECH</text>
+ </navLabel>
+ <content src="ch018_adding-text-to-speech.xhtml"/>
+ </navPoint>
+ <navPoint id="chapter19" playOrder="20">
+ <navLabel>
+ <text>FUN WITH THE JOURNAL</text>
+ </navLabel>
+ <content src="ch019_fun-with-the-journal.xhtml"/>
+ </navPoint>
+ <navPoint id="chapter20" playOrder="21">
+ <navLabel>
+ <text>MAKING ACTIVITIES USING PYGAME</text>
+ </navLabel>
+ <content src="ch020_making-activities-using-pygame.xhtml"/>
+ </navPoint>
+ <navPoint id="chapter21" playOrder="22">
+ <navLabel>
+ <text>MAKING NEW STYLE TOOLBARS</text>
+ </navLabel>
+ <content src="ch021_making-new-style-toolbars.xhtml"/>
+ </navPoint>
+ </navPoint>
+ <navPoint id="chapter22" playOrder="23">
+ <navLabel>
+ <text>APPENDIX</text>
+ </navLabel>
+ <content src="ch023_where-to-go-from-here.xhtml"/>
+ <navPoint id="chapter23" playOrder="24">
+ <navLabel>
+ <text>WHERE TO GO FROM HERE?</text>
+ </navLabel>
+ <content src="ch023_where-to-go-from-here.xhtml"/>
+ </navPoint>
+ <navPoint id="chapter24" playOrder="25">
+ <navLabel>
+ <text>ABOUT THE AUTHORS</text>
+ </navLabel>
+ <content src="ch024_about-the-authors.xhtml"/>
+ </navPoint>
+ <navPoint id="chapter25" playOrder="26">
+ <navLabel>
+ <text>CREDITS</text>
+ </navLabel>
+ <content src="ch025_credits.xhtml"/>
+ </navPoint>
+ </navPoint>
+ </navMap>
+</ncx>