diff options
author | C. Neves <cn@sueste.net> | 2007-11-07 14:06:33 (GMT) |
---|---|---|
committer | C. Neves <cn@sueste.net> | 2007-11-07 14:06:33 (GMT) |
commit | ddb53471b4e7dc1d19235672f3080cdc0afb1cf4 (patch) | |
tree | b59338ca3be272ee90a0595e58a664f44ead1067 | |
parent | 394128551fdb03004846c75c3d5b0176dd9bd3a4 (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.
-rw-r--r-- | mamamedia_icons/arrow_left.png | bin | 0 -> 442 bytes | |||
-rw-r--r-- | mamamedia_icons/arrow_right.png | bin | 0 -> 453 bytes | |||
-rw-r--r-- | mamamedia_icons/circle-check.svg | 66 | ||||
-rw-r--r-- | mamamedia_icons/circle-x.svg | 73 | ||||
-rw-r--r-- | mamamedia_icons/circle.svg | 9 | ||||
-rw-r--r-- | mamamedia_icons/circle.svg.tmpl | 9 | ||||
-rw-r--r-- | mmm_modules/__init__.py | 9 | ||||
-rw-r--r-- | mmm_modules/borderframe.py | 100 | ||||
-rw-r--r-- | mmm_modules/buddy_panel.py | 146 | ||||
-rw-r--r-- | mmm_modules/i18n.py | 166 | ||||
-rw-r--r-- | mmm_modules/image_category.py | 445 | ||||
-rw-r--r-- | mmm_modules/json.py | 310 | ||||
-rw-r--r-- | mmm_modules/notebook_reader.py | 132 | ||||
-rw-r--r-- | mmm_modules/timer.py | 166 | ||||
-rw-r--r-- | mmm_modules/tube_helper.py | 230 | ||||
-rw-r--r-- | mmm_modules/tubeconn.py | 107 | ||||
-rw-r--r-- | mmm_modules/utils.py | 170 |
17 files changed, 2138 insertions, 0 deletions
diff --git a/mamamedia_icons/arrow_left.png b/mamamedia_icons/arrow_left.png Binary files differnew file mode 100644 index 0000000..9c38787 --- /dev/null +++ b/mamamedia_icons/arrow_left.png diff --git a/mamamedia_icons/arrow_right.png b/mamamedia_icons/arrow_right.png Binary files differnew file mode 100644 index 0000000..7e2dfb0 --- /dev/null +++ b/mamamedia_icons/arrow_right.png diff --git a/mamamedia_icons/circle-check.svg b/mamamedia_icons/circle-check.svg new file mode 100644 index 0000000..2c14ef1 --- /dev/null +++ b/mamamedia_icons/circle-check.svg @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Generator: Adobe Illustrator 12.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 51448) --> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://web.resource.org/cc/" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.0" + id="Icon" + width="19.997831" + height="19.999567" + viewBox="0 0 46.115 46.121" + overflow="visible" + enable-background="new 0 0 46.115 46.121" + xml:space="preserve" + sodipodi:version="0.32" + inkscape:version="0.45" + sodipodi:docname="circle-check.svg" + sodipodi:docbase="/home/cn/work/clients/wwworkshop/pygtk/SliderPuzzle.activity/icons" + inkscape:output_extension="org.inkscape.output.svg.inkscape" + sodipodi:modified="true"><metadata + id="metadata8260"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs + id="defs8258" /><sodipodi:namedview + inkscape:window-height="621" + inkscape:window-width="872" + inkscape:pageshadow="2" + inkscape:pageopacity="1" + guidetolerance="10.0" + gridtolerance="10.0" + objecttolerance="10.0" + borderopacity="1.0" + bordercolor="#666666" + pagecolor="#5affff" + id="base" + inkscape:zoom="8.8246138" + inkscape:cx="23.057501" + inkscape:cy="23.060499" + inkscape:window-x="79" + inkscape:window-y="202" + inkscape:current-layer="Icon" + width="20px" + height="20px" /> +<path + d="M 23.056,1.75 C 34.826,1.75 44.363,11.293 44.363,23.058 C 44.363,34.826 34.826,44.37 23.056,44.37 C 11.286,44.37 1.747,34.826 1.747,23.058 C 1.748,11.293 11.287,1.75 23.056,1.75 z " + id="path8251" + style="fill:none;stroke:#ffffff;stroke-width:3.5" /> +<line + x1="12.000999" + y1="24.509001" + x2="19.605999" + y2="32.112999" + id="line8253" + style="fill:none;stroke:#ffffff;stroke-width:3.5;stroke-linecap:round" /> +<line + x1="19.588999" + y1="32.155998" + x2="34.802002" + y2="16.941" + id="line8255" + style="fill:none;stroke:#ffffff;stroke-width:3.5;stroke-linecap:round" /> +</svg>
\ No newline at end of file diff --git a/mamamedia_icons/circle-x.svg b/mamamedia_icons/circle-x.svg new file mode 100644 index 0000000..a7333c9 --- /dev/null +++ b/mamamedia_icons/circle-x.svg @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Generator: Adobe Illustrator 12.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 51448) --> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://web.resource.org/cc/" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.0" + id="Icon" + width="19.997644" + height="19.997644" + viewBox="0 0 46.119 46.121" + overflow="visible" + enable-background="new 0 0 46.119 46.121" + xml:space="preserve" + sodipodi:version="0.32" + inkscape:version="0.45" + sodipodi:docname="circle-x.svg" + sodipodi:docbase="/home/cn/work/clients/wwworkshop/pygtk/SliderPuzzle.activity/icons" + inkscape:output_extension="org.inkscape.output.svg.inkscape" + sodipodi:modified="true"><metadata + id="metadata3355"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs + id="defs3353" /><sodipodi:namedview + inkscape:window-height="621" + inkscape:window-width="872" + inkscape:pageshadow="2" + inkscape:pageopacity="1" + guidetolerance="10.0" + gridtolerance="10.0" + objecttolerance="10.0" + borderopacity="1.0" + bordercolor="#666666" + pagecolor="#0dffff" + id="base" + inkscape:zoom="8.8246138" + inkscape:cx="23.0595" + inkscape:cy="23.060499" + inkscape:window-x="20" + inkscape:window-y="134" + inkscape:current-layer="Icon" + width="20px" + height="20px" /> +<circle + cx="23.059999" + cy="23.061001" + r="21.311001" + id="circle3346" + sodipodi:cx="23.059999" + sodipodi:cy="23.061001" + sodipodi:rx="21.311001" + sodipodi:ry="21.311001" + transform="matrix(0.7071068,-0.7071068,0.7071068,0.7071068,-9.5556897,23.057075)" + style="fill:#ff0000;fill-opacity:0;stroke:#ffffff;stroke-width:3.5;stroke-opacity:1" /> +<line + x1="30.564133" + y1="30.765953" + x2="15.350733" + y2="15.552551" + id="line3348" + style="fill:#aaaaaa;stroke:#ffffff;stroke-width:3.5;stroke-linecap:round;stroke-opacity:1" /> +<line + x1="15.352855" + y1="30.764538" + x2="30.564133" + y2="15.551845" + id="line3350" + style="fill:#aaaaaa;fill-opacity:0;stroke:#ffffff;stroke-width:3.5;stroke-linecap:round;stroke-opacity:1" /> +</svg>
\ No newline at end of file diff --git a/mamamedia_icons/circle.svg b/mamamedia_icons/circle.svg new file mode 100644 index 0000000..752fed1 --- /dev/null +++ b/mamamedia_icons/circle.svg @@ -0,0 +1,9 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg + xmlns="http://www.w3.org/2000/svg" version="1.1"> + <desc>Circle</desc> + <circle cx="150" cy="150" r="50" + fill="none" stroke="yellow" stroke-width="10" /> +</svg>
\ No newline at end of file diff --git a/mamamedia_icons/circle.svg.tmpl b/mamamedia_icons/circle.svg.tmpl new file mode 100644 index 0000000..0162b41 --- /dev/null +++ b/mamamedia_icons/circle.svg.tmpl @@ -0,0 +1,9 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg + xmlns="http://www.w3.org/2000/svg" version="1.1"> + <desc>Circle</desc> + <circle cx="%(cx)s" cy="%(cy)s" r="%(radius)s" + fill="none" stroke="yellow" stroke-width="10" /> +</svg>
\ No newline at end of file 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 + |