Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/mmm_modules
diff options
context:
space:
mode:
authorC. Neves <cn@sueste.net>2007-11-07 14:06:33 (GMT)
committer C. Neves <cn@sueste.net>2007-11-07 14:06:33 (GMT)
commitddb53471b4e7dc1d19235672f3080cdc0afb1cf4 (patch)
treeb59338ca3be272ee90a0595e58a664f44ead1067 /mmm_modules
parent394128551fdb03004846c75c3d5b0176dd9bd3a4 (diff)
Removed a little kungfu I had with soft links. This adds some files redundant between my activities but makes it able to build on jhbuild.
Diffstat (limited to 'mmm_modules')
-rw-r--r--mmm_modules/__init__.py9
-rw-r--r--mmm_modules/borderframe.py100
-rw-r--r--mmm_modules/buddy_panel.py146
-rw-r--r--mmm_modules/i18n.py166
-rw-r--r--mmm_modules/image_category.py445
-rw-r--r--mmm_modules/json.py310
-rw-r--r--mmm_modules/notebook_reader.py132
-rw-r--r--mmm_modules/timer.py166
-rw-r--r--mmm_modules/tube_helper.py230
-rw-r--r--mmm_modules/tubeconn.py107
-rw-r--r--mmm_modules/utils.py170
11 files changed, 1981 insertions, 0 deletions
diff --git a/mmm_modules/__init__.py b/mmm_modules/__init__.py
new file mode 100644
index 0000000..50103a5
--- /dev/null
+++ b/mmm_modules/__init__.py
@@ -0,0 +1,9 @@
+from borderframe import *
+from timer import *
+from image_category import *
+from i18n import *
+from notebook_reader import *
+from buddy_panel import *
+from tube_helper import *
+import utils
+import json
diff --git a/mmm_modules/borderframe.py b/mmm_modules/borderframe.py
new file mode 100644
index 0000000..1c02a22
--- /dev/null
+++ b/mmm_modules/borderframe.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python
+
+# 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
+#
+
+
+### borderframe.py
+### TODO: Describe
+### $Id: $
+###
+### author: Carlos Neves (cn (at) sueste.net)
+### (c) 2007 World Wide Workshop Foundation
+
+import pygtk
+pygtk.require('2.0')
+import gtk, gobject, pango
+
+BORDER_LEFT = 1
+BORDER_RIGHT = 2
+BORDER_TOP = 4
+BORDER_BOTTOM = 8
+BORDER_VERTICAL = BORDER_TOP | BORDER_BOTTOM
+BORDER_HORIZONTAL = BORDER_LEFT | BORDER_RIGHT
+BORDER_ALL = BORDER_VERTICAL | BORDER_HORIZONTAL
+BORDER_ALL_BUT_BOTTOM = BORDER_HORIZONTAL | BORDER_TOP
+BORDER_ALL_BUT_TOP = BORDER_HORIZONTAL | BORDER_BOTTOM
+BORDER_ALL_BUT_LEFT = BORDER_VERTICAL | BORDER_RIGHT
+
+class BorderFrame (gtk.EventBox):
+ def __init__ (self, border=BORDER_ALL, size=5, bg_color=None, border_color=None):
+ gtk.EventBox.__init__(self)
+ if border_color is not None:
+ self.set_border_color(gtk.gdk.color_parse(border_color))
+ self.inner = gtk.EventBox()
+ if bg_color is not None:
+ self.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse(bg_color))
+ align = gtk.Alignment(1.0,1.0,1.0,1.0)
+ self.padding = [0,0,0,0]
+ if (border & BORDER_TOP) != 0:
+ self.padding[0] = size
+ if (border & BORDER_BOTTOM) != 0:
+ self.padding[1] = size
+ if (border & BORDER_LEFT) != 0:
+ self.padding[2] = size
+ if (border & BORDER_RIGHT) != 0:
+ self.padding[3] = size
+ align.set_padding(*self.padding)
+ align.add(self.inner)
+ align.show()
+ self.inner.show()
+ gtk.EventBox.add(self, align)
+ self.stack = []
+
+ def set_border_color (self, color):
+ gtk.EventBox.modify_bg(self, gtk.STATE_NORMAL, color)
+
+ def modify_bg (self, state, color):
+ self.inner.modify_bg(state, color)
+
+ def add (self, widget):
+ self.stack.append(widget)
+ self.inner.add(widget)
+ self.inner.child.show_now()
+
+ def push (self, widget):
+ widget.set_size_request(*self.inner.child.get_size_request())
+ self.inner.remove(self.inner.child)
+ self.add(widget)
+
+ def pop (self):
+ if len(self.stack) > 1:
+ self.inner.remove(self.inner.child)
+ del self.stack[-1]
+ self.inner.add(self.stack[-1])
+
+ def get_child (self):
+ return self.inner.child
+
+ def set_size_request (self, w, h):
+ self.inner.set_size_request(w,h)
+ super(BorderFrame, self).set_size_request(w+self.padding[0]+self.padding[2], h+self.padding[1]+self.padding[3])
+
+ def show (self):
+ self.show_all()
+
+# def get_allocation (self):
+# return self.inner.get_allocation()
+
diff --git a/mmm_modules/buddy_panel.py b/mmm_modules/buddy_panel.py
new file mode 100644
index 0000000..dfa8c99
--- /dev/null
+++ b/mmm_modules/buddy_panel.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python
+
+# 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
+#
+
+
+### buddy_panel.py
+### TODO: Describe
+### $Id: $
+###
+### author: Carlos Neves (cn (at) sueste.net)
+### (c) 2007 World Wide Workshop Foundation
+
+import pygtk
+pygtk.require('2.0')
+import gtk
+
+import logging
+
+from tube_helper import GAME_IDLE, GAME_STARTED, GAME_FINISHED, GAME_QUIT
+
+#from sugar.graphics.icon import CanvasIcon
+
+BUDDYMODE_CONTEST = 0
+BUDDYMODE_COLLABORATION = 1
+
+class BuddyPanel (gtk.ScrolledWindow):
+ def __init__ (self, mode=BUDDYMODE_CONTEST):
+ super(BuddyPanel, self).__init__()
+ self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+
+ self.model = gtk.ListStore(str, str, str, str)
+ self.model.set_sort_column_id(0, gtk.SORT_ASCENDING)
+ self.treeview = gtk.TreeView()
+
+ #col = gtk.TreeViewColumn(_("Icon"))
+ #r = gtk.CellRendererText()
+ #col.pack_start(r, True)
+ #col.set_attributes(r, stock_id=0)
+ #self.treeview.append_column(col)
+
+ col = gtk.TreeViewColumn(_("Buddy"))
+ r = gtk.CellRendererText()
+ col.pack_start(r, True)
+ col.set_attributes(r, text=0)
+ self.treeview.append_column(col)
+
+ col = gtk.TreeViewColumn(_("Status"))
+ r = gtk.CellRendererText()
+ col.pack_start(r, True)
+ col.set_attributes(r, text=1)
+ self.treeview.append_column(col)
+ col.set_visible(mode == BUDDYMODE_CONTEST)
+
+ col = gtk.TreeViewColumn(_("Play Time"))
+ r = gtk.CellRendererText()
+ col.pack_start(r, True)
+ col.set_attributes(r, text=2)
+ self.treeview.append_column(col)
+ col.set_visible(mode == BUDDYMODE_CONTEST)
+
+ col = gtk.TreeViewColumn(_("Joined at"))
+ r = gtk.CellRendererText()
+ col.pack_start(r, True)
+ col.set_attributes(r, text=3)
+ self.treeview.append_column(col)
+ col.set_visible(mode == BUDDYMODE_COLLABORATION)
+
+ self.treeview.set_model(self.model)
+
+ self.add(self.treeview)
+ self.show_all()
+
+ self.players = {}
+
+ def add_player (self, buddy, current_clock=0):
+ """ Adds a player to the panel """
+ op = buddy.object_path()
+ if self.players.get(op) is not None:
+ return
+
+# buddy_color = buddy.props.color
+# if not buddy_color:
+# buddy_color = "#000000,#ffffff"
+#
+# icon = CanvasIcon(
+# icon_name='computer-xo',
+# xo_color=XoColor(buddy_color))
+#
+ nick = buddy.props.nick
+ if not nick:
+ nick = ""
+ self.players[op] = (buddy, self.model.append([nick,
+ _('synchronizing'),
+ '',
+ '']))
+ return nick
+
+ def update_player (self, buddy, status, clock_running, time_ellapsed):
+ """Since the current target build (432) does not fully support the contest mode, we are removing this for now. """
+ #return
+ op = buddy.object_path()
+ if self.players.get(op, None) is None:
+ logging.debug("Player %s not found" % op)
+ return
+ print self.players[op]
+ if status == GAME_STARTED[1]:
+ stat = clock_running and _("Playing") or _("Paused")
+ elif status == GAME_FINISHED[1]:
+ stat = _("Finished")
+ elif status == GAME_QUIT[1]:
+ stat = _("Gave up")
+ else:
+ stat = _("Unknown")
+ self.model.set_value(self.players[op][1], 1, stat)
+ self.model.set_value(self.players[op][1], 2, _("%i minutes") % (time_ellapsed/60))
+ self.model.set_value(self.players[op][1], 3, '%i:%0.2i' % (int(time_ellapsed / 60), int(time_ellapsed % 60)))
+ return (self.model.get_value(self.players[op][1], 0), self.model.get_value(self.players[op][1], 1))
+
+ def get_buddy_from_path (self, object_path):
+ logging.debug("op = " + object_path)
+ logging.debug(self.players)
+ return self.players.get(object_path, None)
+
+ def remove_player (self, buddy):
+ op = buddy.object_path()
+ if self.players.get(op) is None:
+ return
+ nick = buddy.props.nick
+ if not nick:
+ nick = ""
+ self.model.remove(self.players[op][1])
+ del self.players[op]
+ return nick
diff --git a/mmm_modules/i18n.py b/mmm_modules/i18n.py
new file mode 100644
index 0000000..bcd7fdf
--- /dev/null
+++ b/mmm_modules/i18n.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# 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
+#
+
+
+### i18n.py
+### TODO: Describe
+### $Id: $
+###
+### author: Carlos Neves (cn (at) sueste.net)
+### (c) 2007 World Wide Workshop Foundation
+
+import os
+import gettext
+import locale
+
+import gtk, gobject
+
+_ = lambda x: x
+
+# Images were taken from http://www.sodipodi.com/
+# except for korea taken from http://zh.wikipedia.org/wiki/Image:Unification_flag_of_Korea.svg
+
+lang_name_mapping = {
+ 'zh_cn':(None, _('Chinese (simplified)'), 'china'),
+ 'zh_tw':(None, _('Chinese (traditional)'), 'china'),
+ 'cs':(None, _('Czech'),'czech_republic'),
+ 'da':(None, _('Danish'),'denmark'),
+ 'nl':(None, _('Dutch'), 'netherlands'),
+ 'en':('English', _('English'),'united_states'),
+ 'en_gb':('English', _('English - Great Britain'),'united_kingdom'),
+ 'en_us':('English', _('English - U.S.'),'united_states'),
+ 'fi':(None, _('Finnish'),'finland'),
+ 'fr':('Français', _('French'),'france'),
+ 'de':(None, _('German'),'germany'),
+ 'hu':(None, _('Hungarian'),'hungary'),
+ 'it':(None, _('Italian'),'italy'),
+ 'ja':(None, _('Japanese'),'japan'),
+ 'ko':(None, _('Korean'),'korea'),
+ 'no':(None, _('Norwegian'),'norway'),
+ 'pl':(None, _('Polish'),'poland'),
+ 'pt':('Português', _('Portuguese'),'portugal'),
+ 'pt_br':('Português do Brasil', _('Portuguese - Brazilian'),'brazil'),
+ 'ru':(None, _('Russian'),'russian_federation'),
+ 'sk':(None, _('Slovak'),'slovenia'),
+ 'es':('Español', _('Spanish'),'spain'),
+ 'sv':(None, _('Swedish'),'sweden'),
+ 'tr':(None, _('Turkish'),'turkey'),
+ }
+
+class LangDetails (object):
+ def __init__ (self, code, name, image, domain):
+ self.code = code
+ self.country_code = self.code.split('_')[0]
+ self.name = name
+ self.image = image
+ self.domain = domain
+
+ def guess_translation (self, fallback=False):
+ self.gnutranslation = gettext.translation(self.domain, 'locale', [self.code], fallback=fallback)
+
+ def install (self):
+ self.gnutranslation.install()
+
+ def matches (self, code, exact=True):
+ if exact:
+ return code.lower() == self.code.lower()
+ return code.split('_')[0].lower() == self.country_code.lower()
+
+def get_lang_details (lang, domain):
+ mapping = lang_name_mapping.get(lang.lower(), None)
+ if mapping is None:
+ # Try just the country code
+ lang = lang.split('_')[0]
+ mapping = lang_name_mapping.get(lang.lower(), None)
+ if mapping is None:
+ return None
+ if mapping[0] is None:
+ return LangDetails(lang, mapping[1], mapping[2], domain)
+ return LangDetails(lang, mapping[0], mapping[2], domain)
+
+def list_available_translations (domain):
+ rv = [get_lang_details('en', domain)]
+ rv[0].guess_translation(True)
+ if not os.path.isdir('locale'):
+ return rv
+ for i,x in enumerate([x for x in os.listdir('locale') if os.path.isdir('locale/' + x) and not x.startswith('.')]):
+ try:
+ details = get_lang_details(x, domain)
+ if details is not None:
+ details.guess_translation()
+ rv.append(details)
+ except:
+ raise
+ pass
+ return rv
+
+class LanguageComboBox (gtk.ComboBox):
+ def __init__ (self, domain):
+ liststore = gtk.ListStore(gobject.TYPE_STRING)
+ gtk.ComboBox.__init__(self, liststore)
+
+ self.cell = gtk.CellRendererText()
+ self.pack_start(self.cell, True)
+ self.add_attribute(self.cell, 'text', 0)
+
+ self.translations = list_available_translations(domain)
+ for i,x in enumerate(self.translations):
+ liststore.insert(i+1, (gettext.gettext(x.name), ))
+ self.connect('changed', self.install)
+
+ def modify_bg (self, state, color):
+ setattr(self.cell, 'background-gdk',color)
+ setattr(self.cell, 'background-set',True)
+
+ def install (self, *args):
+ if self.get_active() > -1:
+ self.translations[self.get_active()].install()
+ else:
+ code, encoding = locale.getdefaultlocale()
+ if code is None:
+ code = 'en'
+ # Try to find the exact translation
+ for i,t in enumerate(self.translations):
+ if t.matches(code):
+ self.set_active(i)
+ break
+ if self.get_active() < 0:
+ # Failed, try to get the translation based only in the country
+ for i,t in enumerate(self.translations):
+ if t.matches(code, False):
+ self.set_active(i)
+ break
+ if self.get_active() < 0:
+ # nothing found, select first translation
+ self.set_active(0)
+ # Allow for other callbacks
+ return False
+
+###
+def gather_other_translations ():
+ from glob import glob
+ lessons = filter(lambda x: os.path.isdir(x), glob('lessons/*'))
+ lessons = map(lambda x: os.path.basename(x), lessons)
+ lessons = map(lambda x: x[0].isdigit() and x[1:] or x, lessons)
+ images = filter(lambda x: os.path.isdir(x), glob('images/*'))
+ images = map(lambda x: os.path.basename(x), images)
+ f = file('i18n_misc_strings.py', 'w')
+ for e in images+lessons:
+ f.write('_("%s")\n' % e)
+ f.close()
+
diff --git a/mmm_modules/image_category.py b/mmm_modules/image_category.py
new file mode 100644
index 0000000..9181436
--- /dev/null
+++ b/mmm_modules/image_category.py
@@ -0,0 +1,445 @@
+import pygtk
+pygtk.require('2.0')
+import gtk, gobject
+
+import os
+from glob import glob
+import logging
+import md5
+
+from sugar.graphics.objectchooser import ObjectChooser
+
+from borderframe import BorderFrame
+from utils import load_image, resize_image, RESIZE_CUT
+
+cwd = os.path.normpath(os.path.join(os.path.split(__file__)[0], '..'))
+
+if os.path.exists(os.path.join(cwd, 'mamamedia_icons')):
+ # Local, no shared code, version
+ mmmpath = cwd
+ iconpath = os.path.join(mmmpath, 'mamamedia_icons')
+else:
+ propfile = os.path.expanduser("~/.sugar/default/org.worldwideworkshop.olpc.MMMPath")
+
+ if os.path.exists(propfile):
+ mmmpath = file(propfile, 'rb').read()
+ else:
+ mmmpath = cwd
+ iconpath = os.path.join(mmmpath, 'icons')
+
+
+from gettext import gettext as _
+
+THUMB_SIZE = 48
+IMAGE_SIZE = 200
+#MYOWNPIC_FOLDER = os.path.expanduser("~/.sugar/default/org.worldwideworkshop.olpc.MyOwnPictures")
+
+def prepare_btn (btn):
+ return btn
+
+def register_category (pixbuf_class, path):
+ pass
+
+class CategoryDirectory (object):
+ def __init__ (self, path, width=-1, height=-1, method=RESIZE_CUT):
+ self.path = path
+ self.method = method
+ self.pb = None
+ if os.path.isdir(path):
+ self.gather_images()
+ else:
+ self.images = [path]
+ self.set_thumb_size(THUMB_SIZE, THUMB_SIZE)
+ self.set_image_size(width, height)
+ self.filename = None
+ self.name = os.path.basename(path)
+
+ def gather_images (self):
+ """ Lists all images in the selected path as per the wildcard expansion of 'image_*'.
+ Adds all linked images from files (*.lnk) """
+ self.images = []
+ links = glob(os.path.join(self.path, "*.lnk"))
+ for link in links:
+ fpath = file(link).readlines()[0].strip()
+ if os.path.isfile(fpath) and not (fpath in self.images):
+ self.images.append(fpath)
+ else:
+ os.remove(link)
+ self.images.extend(glob(os.path.join(self.path, "image_*")))
+ self.images.sort()
+
+ def set_image_size (self, w, h):
+ self.width = w
+ self.height = h
+
+ def set_thumb_size (self, w, h):
+ self.twidth = w
+ self.theight = h
+ self.thumb = self._get_category_thumb()
+
+ def get_image (self, name):
+ if not len(self.images) or name is None or name not in self.images:
+ return None
+ self.pb = load_image(name)
+ if self.pb is not None:
+ rv = resize_image(self.pb, self.width, self.height, method=self.method)
+ self.filename = name
+ return rv
+ return None
+
+ def get_next_image (self):
+ if not len(self.images):
+ return None
+ if self.filename is None or self.filename not in self.images:
+ pos = -1
+ else:
+ pos = self.images.index(self.filename)
+ pos += 1
+ if pos >= len(self.images):
+ pos = 0
+ return self.get_image(self.images[pos])
+
+ def get_previous_image (self):
+ if not len(self.images):
+ return None
+ if self.filename is None or self.filename not in self.images:
+ pos = len(self.images)
+ else:
+ pos = self.images.index(self.filename)
+ pos -= 1
+ if pos < 0:
+ pos = len(self.images) - 1
+ return self.get_image(self.images[pos])
+
+ def has_images (self):
+ print ("IMG", self.images)
+ return len(self.images) > 0
+
+ def count_images (self):
+ return len(self.images)
+
+ def has_image (self):
+ return self.pb is not None
+
+ def _get_category_thumb (self):
+ if os.path.isdir(self.path):
+ thumbs = glob(os.path.join(self.path, "thumb.*"))
+ thumbs.extend(glob(os.path.join(self.path, "default_thumb.*")))
+ thumbs.extend(glob(os.path.join(mmmpath, "mmm_images","default_thumb.*")))
+ print thumbs
+ thumbs = filter(lambda x: os.path.exists(x), thumbs)
+ thumbs.append(None)
+ else:
+ thumbs = [self.path]
+ print (self.path, thumbs)
+ return load_image(thumbs[0], self.twidth, self.theight)
+
+
+class ImageSelectorWidget (gtk.Table):
+ __gsignals__ = {'category_press' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ 'image_press' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),}
+
+ def __init__ (self,
+ width=IMAGE_SIZE,
+ height=IMAGE_SIZE,
+ frame_color=None,
+ prepare_btn_cb=prepare_btn,
+ method=RESIZE_CUT,
+ image_dir=None):
+ gtk.Table.__init__(self, 2,5,False)
+ self._signals = []
+ self.width = width
+ self.height = height
+ self.image = gtk.Image()
+ self.method = method
+ #self.set_myownpath(MYOWNPIC_FOLDER)
+ img_box = BorderFrame(border_color=frame_color)
+ img_box.add(self.image)
+ img_box.set_border_width(5)
+ self._signals.append((img_box, img_box.connect('button_press_event', self.emit_image_pressed)))
+ self.attach(img_box, 0,5,0,1,0,0)
+ self.attach(gtk.Label(), 0,1,1,2)
+ self.bl = gtk.Button()
+
+ il = gtk.Image()
+ il.set_from_pixbuf(load_image(os.path.join(iconpath, 'arrow_left.png')))
+ self.bl.set_image(il)
+
+ self.bl.connect('clicked', self.previous)
+ self.attach(prepare_btn_cb(self.bl), 1,2,1,2,0,0)
+
+ cteb = gtk.EventBox()
+ self.cat_thumb = gtk.Image()
+ self.cat_thumb.set_size_request(THUMB_SIZE, THUMB_SIZE)
+ cteb.add(self.cat_thumb)
+ self._signals.append((cteb, cteb.connect('button_press_event', self.emit_cat_pressed)))
+ self.attach(cteb, 2,3,1,2,0,0,xpadding=10)
+
+ self.br = gtk.Button()
+ ir = gtk.Image()
+ ir.set_from_pixbuf(load_image(os.path.join(iconpath,'arrow_right.png')))
+ self.br.set_image(ir)
+ self.br.connect('clicked', self.next)
+ self.attach(prepare_btn_cb(self.br), 3,4,1,2,0,0)
+ self.attach(gtk.Label(),4,5,1,2)
+ self.filename = None
+ self.show_all()
+ self.image.set_size_request(width, height)
+ if image_dir is None:
+ image_dir = os.path.join(mmmpath, "mmm_images")
+ self.set_image_dir(image_dir)
+
+ def add_image (self, *args):#widget=None, response=None, *args):
+ """ Use to trigger and process the My Own Image selector. """
+
+ chooser = ObjectChooser(_('Choose image'), None, #self._parent,
+ gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT)
+ try:
+ result = chooser.run()
+ if result == gtk.RESPONSE_ACCEPT:
+ jobject = chooser.get_selected_object()
+ if jobject and jobject.file_path:
+ if self.load_image(str(jobject.file_path), True):
+ pass
+ else:
+ err = gtk.MessageDialog(self._parent, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
+ _("Not a valid image file"))
+ err.run()
+ err.destroy()
+ return
+ finally:
+ chooser.destroy()
+ del chooser
+
+
+ #print (widget,response,args)
+ #if response is None:
+ # # My Own Image selector
+ # imgfilter = gtk.FileFilter()
+ # imgfilter.set_name(_("Image Files"))
+ # imgfilter.add_mime_type('image/*')
+ # fd = gtk.FileChooserDialog(title=_("Select Image File"), parent=None,
+ # action=gtk.FILE_CHOOSER_ACTION_OPEN,
+ # buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
+ #
+ # fd.set_current_folder(os.path.expanduser("~/"))
+ # fd.set_modal(True)
+ # fd.add_filter(imgfilter)
+ # fd.connect("response", self.add_image)
+ # fd.resize(800,600)
+ # fd.show()
+ #else:
+ # if response == gtk.RESPONSE_ACCEPT:
+ # if self.load_image(widget.get_filename()):
+ # pass
+ # #self.do_shuffle()
+ # else:
+ # err = gtk.MessageDialog(self._parent, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
+ # _("Not a valid image file"))
+ # err.run()
+ # err.destroy()
+ # return
+ # widget.destroy()
+
+ def set_readonly (self, ro=True):
+ if ro:
+ self.bl.hide()
+ self.br.hide()
+ for w, s in self._signals:
+ w.handler_block(s)
+
+ #def set_myownpath (self, path):
+ # """ Sets the path to My Own Pictures storage, so we know where to add links to new pictures """
+ # if path is None:
+ # self.myownpath = None
+ # else:
+ # if not os.path.exists(path):
+ # os.mkdir(path)
+ # self.myownpath = path
+
+ #def is_myownpath (self):
+ # """ Checks current path against the set custom image path """
+ # return self.myownpath == self.category.path
+ #
+ #def gather_myownpath_images(self):
+ # """ """
+ # rv = []
+ # self.images = []
+ # links = glob(os.path.join(self.myownpath, "*.lnk"))
+ # for link in links:
+ # linfo = filter(None, map(lambda x: x.strip(), file(link).readlines()))
+ # fpath = linfo[0]
+ # if os.path.isfile(fpath) and not (fpath in self.images):
+ # self.images.append(fpath)
+ # if len(linfo) > 1:
+ # digest = linfo[1]
+ # else:
+ # digest = md5.new(file(fpath, 'rb').read()).hexdigest()
+ # rv.append((link, fpath, digest))
+ # for fpath in glob(os.path.join(self.myownpath, "image_*")):
+ # digest = md5.new(file(fpath, 'rb').read()).hexdigest()
+ # rv.append((fpath, fpath, digest))
+ # return rv
+
+ def emit_cat_pressed (self, *args):
+ self.emit('category_press')
+ return True
+
+ def emit_image_pressed (self, *args):
+ self.emit('image_press')
+ return True
+
+ def has_image (self):
+ return self.category.has_image()
+
+ def get_category_name (self):
+ return self.category.name
+
+ def get_filename (self):
+ return self.category.filename
+
+ def get_image (self):
+ return self.category.pb
+
+ def next (self, *args, **kwargs):
+ pb = self.category.get_next_image()
+ if pb is not None:
+ self.image.set_from_pixbuf(pb)
+
+ def previous (self, *args, **kwargs):
+ pb = self.category.get_previous_image()
+ if pb is not None:
+ self.image.set_from_pixbuf(pb)
+
+ def get_image_dir (self):
+ return self.category.path
+
+ def set_image_dir (self, directory):
+ if os.path.exists(directory) and not os.path.isdir(directory):
+ filename = directory
+ directory = os.path.dirname(directory)
+ logging.debug("dir=%s, filename=%s" % (directory, filename))
+ else:
+ logging.debug("dir=%s" % (directory))
+ filename = None
+ self.category = CategoryDirectory(directory, self.width, self.height, self.method)
+ self.cat_thumb.set_from_pixbuf(self.category.thumb)
+ if filename:
+ self.image.set_from_pixbuf(self.category.get_image(filename))
+ else:
+ if self.category.has_images():
+ self.next()
+
+ def load_image(self, filename, fromJournal=False):
+ """ Loads an image from the file """
+ #if self.myownpath is not None and os.path.isdir(self.myownpath) and not fromJournal:
+ # name = os.path.splitext(os.path.basename(filename))[0]
+ # while os.path.exists(os.path.join(self.myownpath, '%s.lnk' % name)):
+ # name = name + '_'
+ # f = file(os.path.join(self.myownpath, '%s.lnk' % name), 'w')
+ # f.write(filename)
+ # image_digest = md5.new(file(filename, 'rb').read()).hexdigest()
+ # f.write('\n%s' % image_digest)
+ # f.close()
+ # self.category = CategoryDirectory(self.myownpath, self.width, self.height, method=self.method)
+ # self.image.set_from_pixbuf(self.category.get_image(filename))
+ #else:
+ self.category = CategoryDirectory(filename, self.width, self.height, method=self.method)
+ self.next()
+ self.cat_thumb.set_from_pixbuf(self.category.thumb)
+ return self.image.get_pixbuf() is not None
+
+ def load_pb (self, pb):
+ self.category.pb = pb
+ self.image.set_from_pixbuf(resize_image(pb, self.width, self.height, method=self.method))
+
+ #def set_game_widget(self, game_widget):
+ # if self.has_image():
+ # game_widget.load_image(self.get_filename())
+
+ def _freeze (self):
+ """ returns a json writable object representation capable of being used to restore our current status """
+ return {'image_dir': self.get_image_dir(),
+ 'filename': self.get_filename()}
+
+ def _thaw (self, obj):
+ """ retrieves a frozen status from a python object, as per _freeze """
+ self.set_image_dir(obj.get('image_dir', None))
+ self.image.set_from_pixbuf(self.category.get_image(obj.get('filename', None)))
+
+class CategorySelector (gtk.ScrolledWindow):
+ __gsignals__ = {'selected' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (str,))}
+
+ def __init__ (self, title=None, selected_category_path=None, path=None, extra=()):
+ gtk.ScrolledWindow.__init__ (self)
+ self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+ if path is None:
+ path = os.path.join(mmmpath, 'mmm_images')
+ self.path = path
+ self.thumbs = []
+ model, selected = self.get_model(path, selected_category_path, extra)
+ self.ignore_first = selected is not None
+
+ self.treeview = gtk.TreeView()
+ col = gtk.TreeViewColumn(title)
+ r1 = gtk.CellRendererPixbuf()
+ r2 = gtk.CellRendererText()
+ col.pack_start(r1, False)
+ col.pack_start(r2, True)
+ col.set_cell_data_func(r1, self.cell_pb)
+ col.set_attributes(r2, text=1)
+ self.treeview.append_column(col)
+ self.treeview.set_model(model)
+
+ self.add(self.treeview)
+ self.show_all()
+ if selected is not None:
+ self.treeview.get_selection().select_path(selected)
+ self.treeview.connect("cursor-changed", self.do_select)
+
+ def grab_focus (self):
+ self.treeview.grab_focus()
+
+ def cell_pb (self, tvcolumn, cell, model, it):
+ # Renders a pixbuf stored in the thumbs cache
+ cell.set_property('pixbuf', self.thumbs[model.get_value(it, 2)])
+
+ def get_pb (self, path):
+ thumbs = glob(os.path.join(path, "thumb.*"))
+ thumbs.extend(glob(os.path.join(self.path, "default_thumb.*")))
+ thumbs = filter(lambda x: os.path.exists(x), thumbs)
+ thumbs.append(None)
+ return load_image(thumbs[0], THUMB_SIZE, THUMB_SIZE)
+
+ def get_model (self, path, selected_path, extra):
+ # Each row is (path/dirname, pretty name, 0 based index)
+ selected = None
+ store = gtk.ListStore(str, str, int)
+ store.set_sort_column_id(1, gtk.SORT_ASCENDING)
+ files = [os.path.join(path, x) for x in os.listdir(path) if not x.startswith('.')]
+ files.extend(extra)
+ for fullpath, prettyname in [(x, _(os.path.basename(x))) for x in files if os.path.isdir(x)]:
+ count = CategoryDirectory(fullpath).count_images()
+ print (fullpath, prettyname, count)
+ store.append([fullpath, prettyname + (" (%i)" % count), len(self.thumbs)])
+ self.thumbs.append(self.get_pb(fullpath))
+ #if os.path.isdir(MYOWNPIC_FOLDER):
+ # count = CategoryDirectory(MYOWNPIC_FOLDER).count_images()
+ # store.append([MYOWNPIC_FOLDER, _("My Pictures") + (" (%i)" % count), len(self.thumbs)])
+ # self.thumbs.append(self.get_pb(MYOWNPIC_FOLDER))
+
+ i = store.get_iter_first()
+ while i:
+ if selected_path == store.get_value(i, 0):
+ selected = store.get_path(i)
+ break
+ i = store.iter_next(i)
+ return store, selected
+
+ def do_select (self, tree, *args, **kwargs):
+ if self.ignore_first:
+ self.ignore_first = False
+ else:
+ tv, it = tree.get_selection().get_selected()
+ self.emit("selected", tv.get_value(it,0))
+
diff --git a/mmm_modules/json.py b/mmm_modules/json.py
new file mode 100644
index 0000000..a28a13e
--- /dev/null
+++ b/mmm_modules/json.py
@@ -0,0 +1,310 @@
+import string
+import types
+
+## json.py implements a JSON (http://json.org) reader and writer.
+## Copyright (C) 2005 Patrick D. Logan
+## Contact mailto:patrickdlogan@stardecisions.com
+##
+## This library is free software; you can redistribute it and/or
+## modify it under the terms of the GNU Lesser General Public
+## License as published by the Free Software Foundation; either
+## version 2.1 of the License, or (at your option) any later version.
+##
+## This library 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
+## Lesser General Public License for more details.
+##
+## You should have received a copy of the GNU Lesser General Public
+## License along with this library; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+
+class _StringGenerator(object):
+ def __init__(self, string):
+ self.string = string
+ self.index = -1
+ def peek(self):
+ i = self.index + 1
+ if i < len(self.string):
+ return self.string[i]
+ else:
+ return None
+ def next(self):
+ self.index += 1
+ if self.index < len(self.string):
+ return self.string[self.index]
+ else:
+ raise StopIteration
+ def all(self):
+ return self.string
+
+class WriteException(Exception):
+ pass
+
+class ReadException(Exception):
+ pass
+
+class JsonReader(object):
+ hex_digits = {'A': 10,'B': 11,'C': 12,'D': 13,'E': 14,'F':15}
+ escapes = {'t':'\t','n':'\n','f':'\f','r':'\r','b':'\b'}
+
+ def read(self, s):
+ self._generator = _StringGenerator(s)
+ result = self._read()
+ return result
+
+ def _read(self):
+ self._eatWhitespace()
+ peek = self._peek()
+ if peek is None:
+ raise ReadException, "Nothing to read: '%s'" % self._generator.all()
+ if peek == '{':
+ return self._readObject()
+ elif peek == '[':
+ return self._readArray()
+ elif peek == '"':
+ return self._readString()
+ elif peek == '-' or peek.isdigit():
+ return self._readNumber()
+ elif peek == 't':
+ return self._readTrue()
+ elif peek == 'f':
+ return self._readFalse()
+ elif peek == 'n':
+ return self._readNull()
+ elif peek == '/':
+ self._readComment()
+ return self._read()
+ else:
+ raise ReadException, "Input is not valid JSON: '%s'" % self._generator.all()
+
+ def _readTrue(self):
+ self._assertNext('t', "true")
+ self._assertNext('r', "true")
+ self._assertNext('u', "true")
+ self._assertNext('e', "true")
+ return True
+
+ def _readFalse(self):
+ self._assertNext('f', "false")
+ self._assertNext('a', "false")
+ self._assertNext('l', "false")
+ self._assertNext('s', "false")
+ self._assertNext('e', "false")
+ return False
+
+ def _readNull(self):
+ self._assertNext('n', "null")
+ self._assertNext('u', "null")
+ self._assertNext('l', "null")
+ self._assertNext('l', "null")
+ return None
+
+ def _assertNext(self, ch, target):
+ if self._next() != ch:
+ raise ReadException, "Trying to read %s: '%s'" % (target, self._generator.all())
+
+ def _readNumber(self):
+ isfloat = False
+ result = self._next()
+ peek = self._peek()
+ while peek is not None and (peek.isdigit() or peek == "."):
+ isfloat = isfloat or peek == "."
+ result = result + self._next()
+ peek = self._peek()
+ try:
+ if isfloat:
+ return float(result)
+ else:
+ return int(result)
+ except ValueError:
+ raise ReadException, "Not a valid JSON number: '%s'" % result
+
+ def _readString(self):
+ result = ""
+ assert self._next() == '"'
+ try:
+ while self._peek() != '"':
+ ch = self._next()
+ if ch == "\\":
+ ch = self._next()
+ if ch in 'brnft':
+ ch = self.escapes[ch]
+ elif ch == "u":
+ ch4096 = self._next()
+ ch256 = self._next()
+ ch16 = self._next()
+ ch1 = self._next()
+ n = 4096 * self._hexDigitToInt(ch4096)
+ n += 256 * self._hexDigitToInt(ch256)
+ n += 16 * self._hexDigitToInt(ch16)
+ n += self._hexDigitToInt(ch1)
+ ch = unichr(n)
+ elif ch not in '"/\\':
+ raise ReadException, "Not a valid escaped JSON character: '%s' in %s" % (ch, self._generator.all())
+ result = result + ch
+ except StopIteration:
+ raise ReadException, "Not a valid JSON string: '%s'" % self._generator.all()
+ assert self._next() == '"'
+ return result
+
+ def _hexDigitToInt(self, ch):
+ try:
+ result = self.hex_digits[ch.upper()]
+ except KeyError:
+ try:
+ result = int(ch)
+ except ValueError:
+ raise ReadException, "The character %s is not a hex digit." % ch
+ return result
+
+ def _readComment(self):
+ assert self._next() == "/"
+ second = self._next()
+ if second == "/":
+ self._readDoubleSolidusComment()
+ elif second == '*':
+ self._readCStyleComment()
+ else:
+ raise ReadException, "Not a valid JSON comment: %s" % self._generator.all()
+
+ def _readCStyleComment(self):
+ try:
+ done = False
+ while not done:
+ ch = self._next()
+ done = (ch == "*" and self._peek() == "/")
+ if not done and ch == "/" and self._peek() == "*":
+ raise ReadException, "Not a valid JSON comment: %s, '/*' cannot be embedded in the comment." % self._generator.all()
+ self._next()
+ except StopIteration:
+ raise ReadException, "Not a valid JSON comment: %s, expected */" % self._generator.all()
+
+ def _readDoubleSolidusComment(self):
+ try:
+ ch = self._next()
+ while ch != "\r" and ch != "\n":
+ ch = self._next()
+ except StopIteration:
+ pass
+
+ def _readArray(self):
+ result = []
+ assert self._next() == '['
+ done = self._peek() == ']'
+ while not done:
+ item = self._read()
+ result.append(item)
+ self._eatWhitespace()
+ done = self._peek() == ']'
+ if not done:
+ ch = self._next()
+ if ch != ",":
+ raise ReadException, "Not a valid JSON array: '%s' due to: '%s'" % (self._generator.all(), ch)
+ assert ']' == self._next()
+ return result
+
+ def _readObject(self):
+ result = {}
+ assert self._next() == '{'
+ done = self._peek() == '}'
+ while not done:
+ key = self._read()
+ if type(key) is not types.StringType:
+ raise ReadException, "Not a valid JSON object key (should be a string): %s" % key
+ self._eatWhitespace()
+ ch = self._next()
+ if ch != ":":
+ raise ReadException, "Not a valid JSON object: '%s' due to: '%s'" % (self._generator.all(), ch)
+ self._eatWhitespace()
+ val = self._read()
+ result[key] = val
+ self._eatWhitespace()
+ done = self._peek() == '}'
+ if not done:
+ ch = self._next()
+ if ch != ",":
+ raise ReadException, "Not a valid JSON array: '%s' due to: '%s'" % (self._generator.all(), ch)
+ assert self._next() == "}"
+ return result
+
+ def _eatWhitespace(self):
+ p = self._peek()
+ while p is not None and p in string.whitespace or p == '/':
+ if p == '/':
+ self._readComment()
+ else:
+ self._next()
+ p = self._peek()
+
+ def _peek(self):
+ return self._generator.peek()
+
+ def _next(self):
+ return self._generator.next()
+
+class JsonWriter(object):
+
+ def _append(self, s):
+ self._results.append(s)
+
+ def write(self, obj, escaped_forward_slash=False):
+ self._escaped_forward_slash = escaped_forward_slash
+ self._results = []
+ self._write(obj)
+ return "".join(self._results)
+
+ def _write(self, obj):
+ ty = type(obj)
+ if ty is types.DictType:
+ n = len(obj)
+ self._append("{")
+ for k, v in obj.items():
+ self._write(k)
+ self._append(":")
+ self._write(v)
+ n = n - 1
+ if n > 0:
+ self._append(",")
+ self._append("}")
+ elif ty is types.ListType or ty is types.TupleType:
+ n = len(obj)
+ self._append("[")
+ for item in obj:
+ self._write(item)
+ n = n - 1
+ if n > 0:
+ self._append(",")
+ self._append("]")
+ elif ty is types.StringType or ty is types.UnicodeType:
+ self._append('"')
+ obj = obj.replace('\\', r'\\')
+ if self._escaped_forward_slash:
+ obj = obj.replace('/', r'\/')
+ obj = obj.replace('"', r'\"')
+ obj = obj.replace('\b', r'\b')
+ obj = obj.replace('\f', r'\f')
+ obj = obj.replace('\n', r'\n')
+ obj = obj.replace('\r', r'\r')
+ obj = obj.replace('\t', r'\t')
+ self._append(obj)
+ self._append('"')
+ elif ty is types.IntType or ty is types.LongType:
+ self._append(str(obj))
+ elif ty is types.FloatType:
+ self._append("%f" % obj)
+ elif obj is True:
+ self._append("true")
+ elif obj is False:
+ self._append("false")
+ elif obj is None:
+ self._append("null")
+ else:
+ raise WriteException, "Cannot write in JSON: %s" % repr(obj)
+
+def write(obj, escaped_forward_slash=False):
+ return JsonWriter().write(obj, escaped_forward_slash)
+
+def read(s):
+ return JsonReader().read(s)
diff --git a/mmm_modules/notebook_reader.py b/mmm_modules/notebook_reader.py
new file mode 100644
index 0000000..25375e9
--- /dev/null
+++ b/mmm_modules/notebook_reader.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python
+
+# 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
+#
+
+
+### notebook_reader.py
+### TODO: Describe
+### $Id: $
+###
+### author: Carlos Neves (cn (at) sueste.net)
+### (c) 2007 World Wide Workshop Foundation
+
+import pygtk
+pygtk.require('2.0')
+import gtk, gobject, pango
+
+import os
+from abiword import Canvas
+
+from gettext import gettext as _
+import locale
+
+class ReaderProvider (object):
+ def __init__ (self, path, lang_details=None):
+ self.lang_details = lang_details
+ self.path = path
+ self.sync()
+
+ def sync (self):
+ """ must be called after language changes """
+ self.lesson_array = []
+ lessons = filter(lambda x: os.path.isdir(os.path.join(self.path, x)), os.listdir(self.path))
+ lessons.sort()
+ for lesson in lessons:
+ if lesson[0].isdigit():
+ name = _(lesson[1:])
+ else:
+ name = _(lesson)
+ self.lesson_array.append((name, self._get_lesson_filename(os.path.join(self.path, lesson))))
+
+ def _get_lesson_filename (self, path):
+ if self.lang_details:
+ code = self.lang_details.code
+ else:
+ code, encoding = locale.getdefaultlocale()
+ if code is None:
+ code = 'en'
+ canvas = Canvas()
+ canvas.show()
+ files = map(lambda x: os.path.join(path, '%s.abw' % x),
+ ('_'+code.lower(), '_'+code.split('_')[0].lower(), 'default'))
+ files = filter(lambda x: os.path.exists(x), files)
+ return os.path.join(os.getcwd(), files[0])
+
+ def get_lessons (self):
+ """ Returns a list of (name, filename) """
+ for name, path in self.lesson_array:
+ yield (name, path)
+
+class BasicReaderWidget (gtk.HBox):
+ def __init__ (self, path, lang_details=None):
+ super(BasicReaderWidget, self).__init__()
+ self._canvas = None
+ self.provider = ReaderProvider(path, lang_details)
+ self._load_lesson(*self.provider.lesson_array[0])
+
+ def get_lessons(self):
+ return self.provider.get_lessons()
+
+ def load_lesson (self, path):
+ print "load_lesson:" + path
+ if self._canvas:
+ self._canvas.hide()
+ #self.remove(self._canvas)
+ #self._canvas.hide()
+ #del self._canvas
+ #if not self._canvas:
+ canvas = Canvas()
+ canvas.show()
+ print "show"
+ self.pack_start(canvas)
+ print "pack"
+ try:
+ canvas.load_file('file://'+path, '')
+ except:
+ canvas.load_file(path)
+ print "load"
+ #canvas.view_online_layout()
+ #canvas.zoom_width()
+ #canvas.set_show_margin(False)
+ #while gtk.events_pending():
+ # gtk.main_iteration(False)
+ if self._canvas:
+ #self.remove(self._canvas)
+ #self._canvas.unparent()
+ del self._canvas
+ self._canvas = canvas
+ def _load_lesson (self, name, path):
+ self.load_lesson(path)
+
+class NotebookReaderWidget (gtk.Notebook):
+ def __init__ (self, path, lang_details=None):
+ super(NotebookReaderWidget, self).__init__()
+ self.provider = ReaderProvider(path, lang_details)
+ self.set_scrollable(True)
+ for name, path in self.provider.get_lessons():
+ self._load_lesson(name, path)
+
+ def _load_lesson (self, name, path):
+ canvas = Canvas()
+ canvas.show()
+ try:
+ canvas.load_file(path, 'text/plain')
+ except:
+ canvas.load_file(path)
+ canvas.view_online_layout()
+ canvas.zoom_width()
+ canvas.set_show_margin(False)
+ self.append_page(canvas, gtk.Label(name))
diff --git a/mmm_modules/timer.py b/mmm_modules/timer.py
new file mode 100644
index 0000000..e572bca
--- /dev/null
+++ b/mmm_modules/timer.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python
+
+# 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
+#
+
+
+### timer.py
+### TODO: Describe
+### $Id: $
+###
+### author: Carlos Neves (cn (at) sueste.net)
+### (c) 2007 World Wide Workshop Foundation
+
+import pygtk
+pygtk.require('2.0')
+import gtk, gobject, pango
+
+import os
+from time import time
+
+cwd = os.path.normpath(os.path.join(os.path.split(__file__)[0], '..'))
+
+if os.path.exists(os.path.join(cwd, 'mamamedia_icons')):
+ # Local, no shared code, version
+ mmmpath = cwd
+ iconpath = os.path.join(mmmpath, 'mamamedia_icons')
+else:
+ propfile = os.path.expanduser("~/.sugar/default/org.worldwideworkshop.olpc.MMMPath")
+
+ if os.path.exists(propfile):
+ mmmpath = file(propfile, 'rb').read()
+ else:
+ mmmpath = cwd
+ iconpath = os.path.join(mmmpath, 'icons')
+
+from utils import load_image
+
+class TimerWidget (gtk.HBox):
+ __gsignals__ = {'timer_toggle' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (bool,)),}
+ def __init__ (self, bg_color="#DD4040", fg_color="#4444FF", lbl_color="#DD4040", can_stop=True):
+ gtk.HBox.__init__(self)
+ self.counter = gtk.EventBox()
+ self.counter.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse(bg_color))
+ self.counter.set_size_request(120, -1)
+ hb = gtk.HBox()
+ self.counter.add(hb)
+ self.lbl_time = gtk.Label()
+ self.lbl_time.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse(lbl_color))
+ self.pack_start(self.lbl_time, False)
+ self.time_label = gtk.Label("--:--")
+ self.time_label.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse(fg_color))
+ hb.pack_start(self.time_label, False, False, 5)
+ self.prepare_icons()
+ self.icon = gtk.Image()
+ self.icon.set_from_pixbuf(self.icons[1])
+ hb.pack_end(self.icon, False, False, 5)
+ self.pack_start(self.counter, False)
+ self.connect("button-press-event", self.process_click)
+ self.start_time = 0
+ self.timer_id = None
+ self.finished = False
+ self.can_stop = can_stop
+
+ def set_label (self, label):
+ self.lbl_time.set_label(label)
+
+ def prepare_icons (self):
+ self.icons = []
+ self.icons.append(load_image(os.path.join(iconpath,"circle-x.svg")))
+ self.icons.append(load_image(os.path.join(iconpath,"circle-check.svg")))
+
+
+ def set_can_stop (self, can_stop):
+ self.can_stop = can_stop
+
+ def modify_bg(self, state, color):
+ self.foreach(lambda x: x is not self.counter and x.modify_bg(state, color))
+
+ def reset (self, auto_start=True):
+ self.set_sensitive(True)
+ self.finished = False
+ self.stop()
+ self.start_time = 0
+ if auto_start:
+ self.start()
+
+ def start (self):
+ if self.finished:
+ return
+ self.set_sensitive(True)
+ self.icon.set_from_pixbuf(self.icons[0])
+ if self.start_time is None:
+ self.start_time = time()
+ else:
+ self.start_time = time() - self.start_time
+ self.do_tick()
+ if self.timer_id is None:
+ self.timer_id = gobject.timeout_add(1000, self.do_tick)
+ self.emit('timer_toggle', True)
+
+ def stop (self, finished=False):
+ if not self.can_stop and not finished:
+ return
+ self.icon.set_from_pixbuf(self.icons[1])
+ if self.timer_id is not None:
+ gobject.source_remove(self.timer_id)
+ self.timer_id = None
+ self.start_time = time() - self.start_time
+ if not finished:
+ self.time_label.set_text("--:--")
+ else:
+ self.finished = True
+ self.emit('timer_toggle', False)
+
+ def process_click (self, btn, event):
+ if self.timer_id is None:
+ self.start()
+ else:
+ self.stop()
+
+ def is_running (self):
+ return self.timer_id is not None
+
+ def ellapsed (self):
+ if self.is_running():
+ return time() - self.start_time
+ else:
+ return self.start_time
+
+ def is_reset (self):
+ return not self.is_running() and self.start_time == 0
+
+ def do_tick (self):
+ t = time() - self.start_time
+ if t > 5999:
+ # wrap timer
+ t = 0
+ self.start_time = time()
+ self.time_label.set_text("%0.2i:%0.2i" % (t/60, t%60))
+ return True
+
+ def _freeze (self):
+ return (self.start_time, time(), self.finished, self.timer_id is None)
+
+ def _thaw (self, obj):
+ self.start_time, t, finished, stopped = obj
+ if self.start_time is not None:
+ if not stopped:
+ self.start_time = t - self.start_time
+ self.start()
+ return
+ self.start_time = time() - self.start_time
+ self.do_tick()
+ self.stop(finished)
diff --git a/mmm_modules/tube_helper.py b/mmm_modules/tube_helper.py
new file mode 100644
index 0000000..adb88e3
--- /dev/null
+++ b/mmm_modules/tube_helper.py
@@ -0,0 +1,230 @@
+#!/usr/bin/env python
+
+# 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
+#
+
+
+### buddy_panel.py
+### TODO: Describe
+### $Id: $
+###
+### author: Carlos Neves (cn (at) sueste.net)
+### (c) 2007 World Wide Workshop Foundation
+
+import telepathy
+import telepathy.client
+from tubeconn import TubeConnection
+from sugar.presence import presenceservice
+import dbus
+import logging
+logger = logging.getLogger('tube_helper')
+
+GAME_IDLE = (10, 'idle')
+GAME_SELECTED = (20, 'selected')
+GAME_STARTED = (30, 'started')
+GAME_FINISHED = (40, 'finished')
+GAME_QUIT = (50, 'quit')
+
+class TubeHelper (object):
+ """ Tube handling mixin for activities """
+ def __init__(self, tube_class, service):
+ """Set up the tubes for this activity."""
+ self.tube_class = tube_class
+ self.service = service
+ self.pservice = presenceservice.get_instance()
+
+ bus = dbus.Bus()
+ name, path = self.pservice.get_preferred_connection()
+ self.tp_conn_name = name
+ self.tp_conn_path = path
+ self.conn = telepathy.client.Connection(name, path)
+ self.game_tube = False
+ self.initiating = None
+
+ self.connect('shared', self._shared_cb)
+
+ # Buddy object for you
+ owner = self.pservice.get_owner()
+ self.owner = owner
+
+ if self._shared_activity:
+ # we are joining the activity
+ self.connect('joined', self._joined_cb)
+ self._shared_activity.connect('buddy-joined',
+ self._buddy_joined_cb)
+ self._shared_activity.connect('buddy-left',
+ self._buddy_left_cb)
+ if self.get_shared():
+ # we've already joined
+ self._joined_cb()
+
+ def _shared_cb(self, activity):
+ logger.debug('My activity was shared')
+ self.initiating = True
+ self.shared_cb()
+ self._setup()
+
+ for buddy in self._shared_activity.get_joined_buddies():
+ pass # Can do stuff with newly acquired buddies here
+
+ self._shared_activity.connect('buddy-joined', self._buddy_joined_cb)
+ self._shared_activity.connect('buddy-left', self._buddy_left_cb)
+
+ logger.debug('This is my activity: making a tube...')
+ id = self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].OfferTube(
+ telepathy.TUBE_TYPE_DBUS, self.service, {})
+
+ def shared_cb (self):
+ """ override this """
+ pass
+
+ # FIXME: presence service should be tubes-aware and give us more help
+ # with this
+ def _setup(self):
+ if self._shared_activity is None:
+ logger.error('Failed to share or join activity')
+ return
+
+ bus_name, conn_path, channel_paths =\
+ self._shared_activity.get_channels()
+
+ # Work out what our room is called and whether we have Tubes already
+ room = None
+ tubes_chan = None
+ text_chan = None
+ for channel_path in channel_paths:
+ channel = telepathy.client.Channel(bus_name, channel_path)
+ htype, handle = channel.GetHandle()
+ if htype == telepathy.HANDLE_TYPE_ROOM:
+ logger.debug('Found our room: it has handle#%d "%s"',
+ handle, self.conn.InspectHandles(htype, [handle])[0])
+ room = handle
+ ctype = channel.GetChannelType()
+ if ctype == telepathy.CHANNEL_TYPE_TUBES:
+ logger.debug('Found our Tubes channel at %s', channel_path)
+ tubes_chan = channel
+ elif ctype == telepathy.CHANNEL_TYPE_TEXT:
+ logger.debug('Found our Text channel at %s', channel_path)
+ text_chan = channel
+
+ if room is None:
+ logger.error("Presence service didn't create a room")
+ return
+ if text_chan is None:
+ logger.error("Presence service didn't create a text channel")
+ return
+
+ # Make sure we have a Tubes channel - PS doesn't yet provide one
+ if tubes_chan is None:
+ logger.debug("Didn't find our Tubes channel, requesting one...")
+ tubes_chan = self.conn.request_channel(telepathy.CHANNEL_TYPE_TUBES,
+ telepathy.HANDLE_TYPE_ROOM, room, True)
+
+ self.tubes_chan = tubes_chan
+ self.text_chan = text_chan
+
+ tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal('NewTube',
+ self._new_tube_cb)
+
+ def _list_tubes_reply_cb(self, tubes):
+ for tube_info in tubes:
+ self._new_tube_cb(*tube_info)
+
+ def _list_tubes_error_cb(self, e):
+ logger.error('ListTubes() failed: %s', e)
+
+ def _joined_cb(self, activity):
+ if not self._shared_activity:
+ return
+
+ for buddy in self._shared_activity.get_joined_buddies():
+ self._buddy_joined_cb(self, buddy)
+
+ logger.debug('Joined an existing shared activity')
+ self.joined_cb()
+ self.initiating = False
+ self._setup()
+
+ logger.debug('This is not my activity: waiting for a tube...')
+ self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes(
+ reply_handler=self._list_tubes_reply_cb,
+ error_handler=self._list_tubes_error_cb)
+
+ def joined_cb (self):
+ """ override this """
+ pass
+
+ 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 == self.service):
+ if state == telepathy.TUBE_STATE_LOCAL_PENDING:
+ self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].AcceptTube(id)
+
+ tube_conn = TubeConnection(self.conn,
+ self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES],
+ id, group_iface=self.text_chan[telepathy.CHANNEL_INTERFACE_GROUP])
+
+ logger.debug("creating game tube")
+ self.game_tube = self.tube_class(tube_conn, self.initiating, self)
+ self.new_tube_cb()
+
+ def new_tube_cb (self):
+ """ override this """
+ pass
+
+ 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() & 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
+
+ # XXX: we're assuming that we have Buddy objects for all contacts -
+ # this might break when the server becomes scalable.
+ return self.pservice.get_buddy_by_telepathy_handle(self.tp_conn_name,
+ self.tp_conn_path, handle)
+
+ def _buddy_joined_cb (self, activity, buddy):
+ logger.debug('Buddy %s joined' % buddy.props.nick)
+ self.buddy_joined_cb(buddy)
+
+ def buddy_joined_cb (self, buddy):
+ """ override this """
+ pass
+
+ def _buddy_left_cb (self, activity, buddy):
+ logger.debug('Buddy %s left' % buddy.props.nick)
+ self.buddy_left_cb(buddy)
+
+ def buddy_left_cb (self, buddy):
+ """ override this """
+ pass
+
diff --git a/mmm_modules/tubeconn.py b/mmm_modules/tubeconn.py
new file mode 100644
index 0000000..d1c1403
--- /dev/null
+++ b/mmm_modules/tubeconn.py
@@ -0,0 +1,107 @@
+# This should eventually land in telepathy-python, so has the same license:
+
+# Copyright (C) 2007 Collabora Ltd. <http://www.collabora.co.uk/>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+
+__all__ = ('TubeConnection',)
+__docformat__ = 'reStructuredText'
+
+
+import logging
+
+from dbus.connection import Connection
+
+
+logger = logging.getLogger('telepathy.tubeconn')
+
+
+class TubeConnection(Connection):
+
+ def __new__(cls, conn, tubes_iface, tube_id, address=None,
+ group_iface=None, mainloop=None):
+ if address is None:
+ address = tubes_iface.GetDBusServerAddress(tube_id)
+ self = super(TubeConnection, cls).__new__(cls, address,
+ mainloop=mainloop)
+
+ self._tubes_iface = tubes_iface
+ self.tube_id = tube_id
+ self.participants = {}
+ self.bus_name_to_handle = {}
+ self._mapping_watches = []
+
+ if group_iface is None:
+ method = conn.GetSelfHandle
+ else:
+ method = group_iface.GetSelfHandle
+ method(reply_handler=self._on_get_self_handle_reply,
+ error_handler=self._on_get_self_handle_error)
+
+ return self
+
+ def _on_get_self_handle_reply(self, handle):
+ self.self_handle = handle
+ match = self._tubes_iface.connect_to_signal('DBusNamesChanged',
+ self._on_dbus_names_changed)
+ self._tubes_iface.GetDBusNames(self.tube_id,
+ reply_handler=self._on_get_dbus_names_reply,
+ error_handler=self._on_get_dbus_names_error)
+ self._dbus_names_changed_match = match
+
+ def _on_get_self_handle_error(self, e):
+ logging.basicConfig()
+ logger.error('GetSelfHandle failed: %s', e)
+
+ def close(self):
+ self._dbus_names_changed_match.remove()
+ self._on_dbus_names_changed(self.tube_id, (), self.participants.keys())
+ super(TubeConnection, self).close()
+
+ def _on_get_dbus_names_reply(self, names):
+ self._on_dbus_names_changed(self.tube_id, names, ())
+
+ def _on_get_dbus_names_error(self, e):
+ logging.basicConfig()
+ logger.error('GetDBusNames failed: %s', e)
+
+ def _on_dbus_names_changed(self, tube_id, added, removed):
+ if tube_id == self.tube_id:
+ for handle, bus_name in added:
+ if handle == self.self_handle:
+ # I've just joined - set my unique name
+ self.set_unique_name(bus_name)
+ self.participants[handle] = bus_name
+ self.bus_name_to_handle[bus_name] = handle
+
+ # call the callback while the removed people are still in
+ # participants, so their bus names are available
+ for callback in self._mapping_watches:
+ callback(added, removed)
+
+ for handle in removed:
+ bus_name = self.participants.pop(handle, None)
+ self.bus_name_to_handle.pop(bus_name, None)
+
+ def watch_participants(self, callback):
+ self._mapping_watches.append(callback)
+ if self.participants:
+ # GetDBusNames already returned: fake a participant add event
+ # immediately
+ added = []
+ for k, v in self.participants.iteritems():
+ added.append((k, v))
+ callback(added, [])
diff --git a/mmm_modules/utils.py b/mmm_modules/utils.py
new file mode 100644
index 0000000..4a36b2c
--- /dev/null
+++ b/mmm_modules/utils.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python
+
+# 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
+#
+
+
+### utils
+### TODO: Describe
+### $Id: $
+###
+### author: Carlos Neves (cn (at) sueste.net)
+### (c) 2007 World Wide Workshop Foundation
+
+import pygtk
+pygtk.require('2.0')
+import gtk
+
+RESIZE_STRETCH = 1
+RESIZE_CUT = 2
+RESIZE_PAD = 3
+
+TYPE_REG = []
+
+def register_image_type (handler):
+ TYPE_REG.append(handler)
+
+def calculate_relative_size (orig_width, orig_height, width, height):
+ """ If any of width or height is -1, the returned width or height will be in the same relative scale as the
+ given part.
+ >>> calculate_relative_size(100, 100, 50, -1)
+ (50, 50)
+ >>> calculate_relative_size(200, 100, -1, 50)
+ (100, 50)
+
+ If both width and height are given, the same values will be returned. If none is given, the orig_* will be returned.
+ >>> calculate_relative_size(200,200,100,150)
+ (100, 150)
+ >>> calculate_relative_size(200,200,-1,-1)
+ (200, 200)
+ """
+ if width < 0:
+ if height >= 0:
+ out_w = int(orig_width * (float(height)/orig_height))
+ out_h = height
+ else:
+ out_w = orig_width
+ out_h = orig_height
+ else:
+ out_w = width
+ if height < 0:
+ out_h = int(orig_height * (float(width)/orig_width))
+ else:
+ out_h = height
+ return out_w, out_h
+
+def load_image (filename, width=-1, height=-1, method=RESIZE_CUT):
+ """ load an image from filename, returning it's gtk.gdk.PixBuf().
+ If any or all of width and height are given, scale the loaded image to fit the given size(s).
+ If both width and height and requested scaling can be achieved in two flavours, as defined by
+ the method argument:
+ RESIZE_CUT : resize so one of width or height fits the requirement and the other fits or overflows,
+ cut the center of the image to fit the request.
+ RESIZE_STRETCH : fit the requested sizes exactly, by scaling with stretching sides if needed.
+ RESIZE_PAD : resize so one of width or height fits the requirement and the other underflows.
+
+ Example: Image with 500x500, requested 200x100
+ - RESIZE_CUT: scale to 200x200, cut 50 off each top and bottom to fit 200x100
+ - RESIZE STRETCH : scale to 200x100, by changing the image WxH ratio from 1:1 to 2:1, thus distorting it.
+ - RESIZE_PAD: scale to 100x100, add 50 pixel padding for top and bottom to fit 200x100
+ """
+ for ht in TYPE_REG:
+ if ht.can_handle(filename):
+ return ht(width, height, filename)
+# if filename.lower().endswith('.sequence'):
+# slider = None
+# cmds = file(filename).readlines()
+# if len(cmds) > 1:
+# _x_ = eval(cmds[0])
+# items = []
+# for i in range(16):
+# items.append(_x_)
+# _x_ = eval(cmds[1])
+# slider = SliderCreator(width, height, items)
+# slider.prepare_stringed(2,2)
+# return slider
+#
+ img = gtk.Image()
+ try:
+ img.set_from_file(filename)
+ pb = img.get_pixbuf()
+ except:
+ return None
+ return resize_image(pb, width, height, method)
+
+def resize_image (pb, width=-1, height=-1, method=RESIZE_CUT):
+ if pb is None:
+ return None
+ print "utils: method=%i" % method
+ if method == RESIZE_STRETCH or width == -1 or height == -1:
+ w,h = calculate_relative_size(pb.get_width(), pb.get_height(), width, height)
+ scaled_pb = pb.scale_simple(w,h, gtk.gdk.INTERP_BILINEAR)
+ elif method == RESIZE_PAD:
+ w,h = pb.get_width(), pb.get_height()
+ hr = float(height)/h
+ wr = float(width)/w
+ factor = min(hr, wr)
+ w = w * factor
+ h = h * factor
+ print "RESIZE_PAD: %i,%i,%f" % (w,h,factor)
+ scaled_pb = pb.scale_simple(int(w), int(h), gtk.gdk.INTERP_BILINEAR)
+ else: # RESIZE_CUT / default
+ w,h = pb.get_width(), pb.get_height()
+ if width > w:
+ if height > h:
+ #calc which side needs more scaling up as both are smaller
+ hr = float(height)/h
+ wr = float(width)/w
+ if hr < wr:
+ w = width
+ h = -1
+ else:
+ h = height
+ w = -1
+ else:
+ # requested height smaller than image, scale width up and cut on height
+ h = -1
+ w = width
+ else:
+ if height > h:
+ #requested width smaller than image, scale height up and cut on width
+ h = height
+ w = -1
+ else:
+ # calc which side needs less scaling down as both are bigger
+ hr = float(height)/h
+ wr = float(width)/w
+ if hr < wr:
+ w = width
+ h = -1
+ else:
+ h = height
+ w = -1
+ # w, h now have -1 for the side that should be relatively scaled, to keep the aspect ratio and
+ # assuring that the image is at least as big as the request.
+ w,h = calculate_relative_size(pb.get_width(), pb.get_height(), w,h)
+ scaled_pb = pb.scale_simple(w,h, gtk.gdk.INTERP_BILINEAR)
+ # now we cut whatever is left to make the requested size
+ scaled_pb = scaled_pb.subpixbuf(abs((width-w)/2),abs((height-h)/2), width, height)
+ return scaled_pb
+
+### Helper decorators
+
+def trace (func):
+ def wrapped (*args, **kwargs):
+ print ("TRACE", func.func_name, args, kwargs)
+ return func(*args, **kwargs)
+ return wrapped
+